Compare commits

...

15 Commits

Author SHA1 Message Date
zsviczian
a69fefffdc beta-2 2023-11-19 08:42:41 +01:00
zsviczian
1d0466dae7 2.0.1-beta-1 2023-11-18 17:43:47 +01:00
zsviczian
6e5a853d0f 2.0.2-beta-1 2023-11-18 17:40:59 +01:00
zsviczian
0c702ddf7b Merge branch 'master' of https://github.com/zsviczian/obsidian-excalidraw-plugin 2023-11-18 17:16:26 +01:00
zsviczian
fdbffce1f9 Embeddable Settings 2023-11-18 17:16:24 +01:00
zsviczian
2872b4e3ce Merge pull request #1441 from heinrich26/patch-1
Fixed some typos
2023-11-18 17:14:45 +01:00
Hendrik Horstmann
0ba55e51e9 Fixed some typos 2023-11-15 10:50:16 +01:00
zsviczian
5887bf377d 2.0.1 2023-11-14 20:35:47 +01:00
zsviczian
c440dd9cf0 2.0.0 2023-11-13 22:16:43 +01:00
zsviczian
21bc1f7fa6 Merge branch 'master' of https://github.com/zsviczian/obsidian-excalidraw-plugin 2023-11-12 17:29:40 +01:00
zsviczian
4279b13554 fix svg export 2023-11-12 17:29:37 +01:00
zsviczian
0e106b7c7b Merge pull request #1429 from 7flash/patch-5
Update Slideshow.md
2023-11-12 10:59:33 +01:00
zsviczian
4d7d1fba3a Image cache initialization watchdog 2023-11-12 09:43:34 +01:00
zsviczian
a35ea5e9da Adding the Assistant font, dynamic styling improvements, frame export colors, Removing ExcalidrawRef 2023-11-12 07:19:09 +01:00
Igor Berlenko
f306b20449 Update Slideshow.md 2023-11-11 18:28:57 +08:00
28 changed files with 1116 additions and 252 deletions

View File

@@ -288,7 +288,7 @@ let busy = false;
const scrollToNextRect = async ({left,top,right,bottom,nextZoom},steps = TRANSITION_STEP_COUNT) => {
const startTimer = Date.now();
let watchdog = 0;
while(busy && watchdog++<15) await(100);
while(busy && watchdog++<15) await sleep(100);
if(busy && watchdog >= 15) return;
busy = true;
excalidrawAPI.updateScene({appState:{shouldCacheIgnoreZoom:true}});
@@ -746,4 +746,4 @@ if(window.ExcalidrawSlideshow && (window.ExcalidrawSlideshow.script === utils.sc
timestamp
};
window.ExcalidrawSlideshowStartTimer = window.setTimeout(start,500);
}
}

View File

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

View File

@@ -1,11 +1,12 @@
{
"id": "obsidian-excalidraw-plugin",
"name": "Excalidraw",
"version": "1.9.28",
"version": "2.0.1",
"minAppVersion": "1.1.6",
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
"author": "Zsolt Viczian",
"authorUrl": "https://zsolt.blog",
"fundingUrl": "https://ko-fi.com/zsolt",
"helpUrl": "https://github.com/zsviczian/obsidian-excalidraw-plugin#readme",
"isDesktopOnly": false
}

View File

@@ -5,7 +5,11 @@ import { ExcalidrawElement, ExcalidrawImageElement, FileId } from "@zsviczian/ex
import { BinaryFileData, DataURL } from "@zsviczian/excalidraw/types/types";
import { App, MarkdownRenderer, Notice, TFile } from "obsidian";
import {
ASSISTANT_FONT,
CASCADIA_FONT,
VIRGIL_FONT,
} from "./constFonts";
import {
DEFAULT_MD_EMBED_CSS,
fileid,
FRONTMATTER_KEY_BORDERCOLOR,
@@ -15,7 +19,6 @@ import {
IMAGE_TYPES,
nanoid,
THEME_FILTER,
VIRGIL_FONT,
} from "./constants";
import { createSVG } from "./ExcalidrawAutomate";
import { ExcalidrawData, getTransclusion } from "./ExcalidrawData";
@@ -743,6 +746,9 @@ export class EmbeddedFilesLoader {
case "Cascadia":
fontDef = CASCADIA_FONT;
break;
case "Assistant":
fontDef = ASSISTANT_FONT;
break;
case "":
fontDef = "";
break;

View File

@@ -80,6 +80,7 @@ import { emulateKeysForLinkClick, KeyEvent, PaneTarget } from "src/utils/Modifie
import { Mutable } from "@zsviczian/excalidraw/types/utility-types";
import PolyBool from "polybooljs";
import { compressToBase64, decompressFromBase64 } from "lz-string";
import { EmbeddableMDCustomProps } from "./dialogs/EmbeddableSettings";
extendPlugins([
HarmonyPlugin,
@@ -725,6 +726,7 @@ export class ExcalidrawAutomate {
w: number,
h: number,
link: string | null = null,
scale?: [number, number],
) {
return {
id,
@@ -755,6 +757,7 @@ export class ExcalidrawAutomate {
boundElements: [] as any,
link,
locked: false,
...scale ? {scale} : {},
};
}
@@ -770,7 +773,15 @@ export class ExcalidrawAutomate {
* @param height
* @returns
*/
public addEmbeddable(topX: number, topY: number, width: number, height: number, url?: string, file?: TFile): string {
public addEmbeddable(
topX: number,
topY: number,
width: number,
height: number,
url?: string,
file?: TFile,
embeddableCustomData?: EmbeddableMDCustomProps,
): string {
//@ts-ignore
if (!this.targetView || !this.targetView?._loaded) {
errorMessage("targetView not set", "addEmbeddable()");
@@ -797,7 +808,9 @@ export class ExcalidrawAutomate {
false, //file.extension === "md", //changed this to false because embedable link navigation in ExcaliBrain
)
}]]` : "",
[1,1],
);
this.elementsDict[id].customData = {mdProps: embeddableCustomData ?? this.plugin.settings.embeddableMarkdownDefaults};
return id;
};
@@ -1570,12 +1583,12 @@ export class ExcalidrawAutomate {
errorMessage("targetView not set", "deleteViewElements()");
return false;
}
const current = this.targetView?.excalidrawRef?.current;
if (!current) {
const api = this.targetView?.excalidrawAPI as ExcalidrawImperativeAPI;
if (!api) {
return false;
}
const el: ExcalidrawElement[] = current.getSceneElements();
const st: AppState = current.getAppState();
const el: ExcalidrawElement[] = api.getSceneElements() as ExcalidrawElement[];
const st: AppState = api.getAppState();
this.targetView.updateScene({
elements: el.filter((e: ExcalidrawElement) => !elToDelete.includes(e)),
appState: st,

View File

@@ -1,7 +1,7 @@
import { RestoredDataState } from "@zsviczian/excalidraw/types/data/restore";
import { ImportedDataState } from "@zsviczian/excalidraw/types/data/types";
import { BoundingBox } from "@zsviczian/excalidraw/types/element/bounds";
import { ExcalidrawBindableElement, ExcalidrawElement, ExcalidrawTextElement, FontFamilyValues, FontString, NonDeleted, Theme } from "@zsviczian/excalidraw/types/element/types";
import { ExcalidrawBindableElement, ExcalidrawElement, ExcalidrawFrameElement, ExcalidrawTextElement, FontFamilyValues, FontString, NonDeleted, NonDeletedExcalidrawElement, Theme } from "@zsviczian/excalidraw/types/element/types";
import { AppState, BinaryFiles, ExportOpts, Point, Zoom } from "@zsviczian/excalidraw/types/types";
import { Mutable } from "@zsviczian/excalidraw/types/utility-types";
@@ -44,6 +44,7 @@ declare namespace ExcalidrawLib {
appState?: AppState;
files?: any;
exportPadding?: number;
exportingFrame: ExcalidrawFrameElement | null | undefined;
renderEmbeddables?: boolean;
}): Promise<SVGSVGElement>;

View File

@@ -238,7 +238,7 @@ export default class ExcalidrawView extends TextFileView {
public addText: Function = null;
public addLink: Function = null;
private refresh: Function = null;
public excalidrawRef: React.MutableRefObject<any> = null;
//public excalidrawRef: React.MutableRefObject<any> = null;
public excalidrawAPI: any = null;
public excalidrawWrapperRef: React.MutableRefObject<any> = null;
public toolsPanelRef: React.MutableRefObject<any> = null;
@@ -1165,7 +1165,7 @@ export default class ExcalidrawView extends TextFileView {
const api = this.excalidrawAPI;
if (
!this.plugin.settings.zoomToFitOnResize ||
!this.excalidrawRef ||
!this.excalidrawAPI ||
this.semaphores.isEditingText ||
!api
) {
@@ -1369,7 +1369,7 @@ export default class ExcalidrawView extends TextFileView {
public setTheme(theme: string) {
const api = this.excalidrawAPI;
if (!this.excalidrawRef || !api) {
if (!api) {
return;
}
if (this.file) {
@@ -1451,7 +1451,7 @@ export default class ExcalidrawView extends TextFileView {
st.draggingElement === null //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/630
) {
this.autosaveTimer = null;
if (this.excalidrawRef) {
if (this.excalidrawAPI) {
this.semaphores.autosaving = true;
const self = this;
//changed from await to then to avoid lag during saving of large file
@@ -1544,7 +1544,7 @@ export default class ExcalidrawView extends TextFileView {
return;
}
const api = this.excalidrawAPI;
if (!this.excalidrawRef || !this.file || !api) {
if (!this.file || !api) {
return;
}
const loadOnModifyTrigger = file && file === this.file;
@@ -1686,7 +1686,7 @@ export default class ExcalidrawView extends TextFileView {
delete this.exportDialog;
const api = this.excalidrawAPI;
if (!this.excalidrawRef || !api) {
if (!api) {
return;
}
if (this.activeLoader) {
@@ -2110,7 +2110,7 @@ export default class ExcalidrawView extends TextFileView {
files: excalidrawData.files,
libraryItems: await this.getLibrary(),
});
//files are loaded on excalidrawRef readyPromise
//files are loaded when excalidrawAPI is mounted
}
const isCompressed = this.data.match(/```compressed\-json\n/gm) !== null;
@@ -2448,6 +2448,8 @@ export default class ExcalidrawView extends TextFileView {
this.obsidianMenu = new ObsidianMenu(this.plugin, toolsPanelRef, this);
this.embeddableMenu = new EmbeddableMenu(this, embeddableMenuRef);
/*
//excalidrawRef readypromise based on
//https://codesandbox.io/s/eexcalidraw-resolvable-promise-d0qg3?file=/src/App.js:167-760
const resolvablePromise = () => {
@@ -2473,8 +2475,19 @@ export default class ExcalidrawView extends TextFileView {
}),
[],
);
*/
React.useEffect(() => {
const setExcalidrawAPI = (api: ExcalidrawImperativeAPI) => {
this.excalidrawAPI = api;
api.setLocalFont(this.plugin.settings.experimentalEnableFourthFont);
setTimeout(() => {
this.onAfterLoadScene();
this.excalidrawContainer = this.excalidrawWrapperRef?.current?.firstElementChild;
this.excalidrawContainer?.focus();
});
};
/* React.useEffect(() => {
excalidrawRef.current.readyPromise.then(
(api: ExcalidrawImperativeAPI) => {
this.excalidrawAPI = api;
@@ -2486,14 +2499,14 @@ export default class ExcalidrawView extends TextFileView {
});
},
);
}, [excalidrawRef]);
}, [excalidrawRef]);*/
this.excalidrawRef = excalidrawRef;
// this.excalidrawRef = excalidrawRef;
this.excalidrawWrapperRef = excalidrawWrapperRef;
const setCurrentPositionToCenter = () => {
const api = this.excalidrawAPI;
if (!excalidrawRef || !excalidrawRef.current || !api) {
if (!api) {
return;
}
const st = api.getAppState();
@@ -2538,7 +2551,7 @@ export default class ExcalidrawView extends TextFileView {
this.getSelectedTextElement = (): SelectedElementWithLink => {
const api = this.excalidrawAPI;
if (!excalidrawRef?.current || !api) {
if (!api) {
return { id: null, text: null };
}
if (api.getAppState().viewModeEnabled) {
@@ -2720,7 +2733,7 @@ export default class ExcalidrawView extends TextFileView {
save: boolean = true
): Promise<string> => {
const api = this.excalidrawAPI as ExcalidrawImperativeAPI;
if (!excalidrawRef?.current || !api) {
if (!api) {
return;
}
const st: AppState = api.getAppState();
@@ -2763,7 +2776,7 @@ export default class ExcalidrawView extends TextFileView {
shouldRestoreElements: boolean = false,
): Promise<boolean> => {
const api = this.excalidrawAPI as ExcalidrawImperativeAPI;
if (!excalidrawRef?.current || !api) {
if (!api) {
return false;
}
const textElements = newElements.filter((el) => el.type == "text");
@@ -2869,7 +2882,7 @@ export default class ExcalidrawView extends TextFileView {
this.getScene = (selectedOnly?: boolean) => {
const api = this.excalidrawAPI;
if (!excalidrawRef?.current || !api) {
if (!api) {
return null;
}
const el: ExcalidrawElement[] = selectedOnly ? this.getViewSelectedElements() : api.getSceneElements();
@@ -2925,7 +2938,7 @@ export default class ExcalidrawView extends TextFileView {
this.refresh = () => {
if(this.contentEl.clientWidth === 0 || this.contentEl.clientHeight === 0) return;
const api = this.excalidrawAPI;
if (!excalidrawRef?.current || !api) {
if (!api) {
return;
}
api.refresh();
@@ -4431,7 +4444,7 @@ export default class ExcalidrawView extends TextFileView {
React.createElement(
Excalidraw,
{
ref: excalidrawRef,
excalidrawAPI: ((api: ExcalidrawImperativeAPI) =>{setExcalidrawAPI(api)}),
width: dimensions.width,
height: dimensions.height,
UIOptions:
@@ -4522,7 +4535,7 @@ export default class ExcalidrawView extends TextFileView {
const modalContainer = document.body.querySelector("div.modal-container");
if(modalContainer) return; //do not autozoom when the command palette or other modal container is envoked on iPad
const api = this.excalidrawAPI;
if (!api || !this.excalidrawRef || this.semaphores.isEditingText || this.semaphores.preventAutozoom) {
if (!api || this.semaphores.isEditingText || this.semaphores.preventAutozoom) {
return;
}
const maxZoom = this.plugin.settings.zoomToFitMaxLevel;

View File

@@ -19,20 +19,18 @@ import {
getWithBackground,
hasExportTheme,
convertSVGStringToElement,
getFileCSSClasses,
} from "./utils/Utils";
import { getParentOfClass, isObsidianThemeDark } from "./utils/ObsidianUtils";
import { getParentOfClass, isObsidianThemeDark, getFileCSSClasses } from "./utils/ObsidianUtils";
import { linkClickModifierType } from "./utils/ModifierkeyHelper";
import { ImageKey, imageCache } from "./utils/ImageCache";
import { FILENAMEPARTS, PreviewImageType } from "./utils/UtilTypes";
import { css } from "chroma-js";
interface imgElementAttributes {
file?: TFile;
fname: string; //Excalidraw filename
fwidth: string; //Display width of image
fheight: string; //Display height of image
style: string; //css style to apply to IMG element
style: string[]; //css style to apply to IMG element
}
let plugin: ExcalidrawPlugin;
@@ -125,8 +123,16 @@ const setStyle = ({element,imgAttributes,onCanvas}:{
style += `height:${imgAttributes.fheight}px;`;
}
if(!onCanvas) element.setAttribute("style", style);
element.addClass(imgAttributes.style);
element.addClass("excalidraw-embedded-img");
element.classList.add(...Array.from(imgAttributes.style))
if(!element.hasClass("excalidraw-embedded-img")) {
element.addClass("excalidraw-embedded-img");
}
if(
window.ExcalidrawAutomate.plugin.settings.canvasImmersiveEmbed &&
!element.hasClass("excalidraw-canvas-immersive")
) {
element.addClass("excalidraw-canvas-immersive");
}
}
const _getSVGIMG = async ({filenameParts,theme,cacheReady,img,file,exportSettings,loader}:{
@@ -256,7 +262,7 @@ const getIMG = async (
const filenameParts = getEmbeddedFilenameParts(imgAttributes.fname);
// https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/387
imgAttributes.style = imgAttributes.style.replaceAll(" ", "-");
imgAttributes.style = imgAttributes.style.map(s=>s.replaceAll(" ", "-"));
const forceTheme = hasExportTheme(plugin, file)
? getExportTheme(plugin, file, "light")
@@ -391,7 +397,7 @@ const createImgElement = async (
fname: fileSource,
fwidth: imgOrDiv.getAttribute("w"),
fheight: imgOrDiv.getAttribute("h"),
style: imgOrDiv.getAttribute("class"),
style: [...Array.from(imgOrDiv.classList)],
}, onCanvas);
parent.empty();
if(!onCanvas) {
@@ -401,8 +407,20 @@ const createImgElement = async (
newImg.setAttribute("fileSource",fileSource);
parent.append(newImg);
});
const cssClasses = getFileCSSClasses(plugin, attr.file);
cssClasses.forEach((cssClass) => imgOrDiv.addClass(cssClass));
const cssClasses = getFileCSSClasses(attr.file);
cssClasses.forEach((cssClass) => {
if(imgOrDiv.hasClass(cssClass)) return;
imgOrDiv.addClass(cssClass);
});
if(window.ExcalidrawAutomate.plugin.settings.canvasImmersiveEmbed) {
if(!imgOrDiv.hasClass("excalidraw-canvas-immersive")) {
imgOrDiv.addClass("excalidraw-canvas-immersive");
}
} else {
if(imgOrDiv.hasClass("excalidraw-canvas-immersive")) {
imgOrDiv.removeClass("excalidraw-canvas-immersive");
}
}
return imgOrDiv;
}
@@ -411,7 +429,7 @@ const createImageDiv = async (
onCanvas: boolean = false
): Promise<HTMLDivElement> => {
const img = await createImgElement(attr, onCanvas);
return createDiv(attr.style, (el) => el.append(img));
return createDiv(attr.style.join(" "), (el) => el.append(img));
};
const processReadingMode = async (
@@ -453,7 +471,7 @@ const processInternalEmbed = async (internalEmbedEl: Element, file: TFile ):Prom
fname: "",
fheight: "",
fwidth: "",
style: "",
style: [],
};
const src = internalEmbedEl.getAttribute("src");
@@ -470,7 +488,7 @@ const processInternalEmbed = async (internalEmbedEl: Element, file: TFile ):Prom
: getDefaultWidth(plugin);
attr.fheight = internalEmbedEl.getAttribute("height");
let alt = internalEmbedEl.getAttribute("alt");
attr.style = "excalidraw-svg";
attr.style = ["excalidraw-svg"];
processAltText(src.split("#")[0],alt,attr);
const fnameParts = getEmbeddedFilenameParts(src);
attr.fname = file?.path + (fnameParts.hasBlockref||fnameParts.hasSectionref?fnameParts.linkpartReference:"");
@@ -489,14 +507,14 @@ const processAltText = (
attr.fwidth = parts[2] ?? attr.fwidth;
attr.fheight = parts[3] ?? attr.fheight;
if (parts[4] && !parts[4].startsWith(fname)) {
attr.style = `excalidraw-svg${`-${parts[4]}`}`;
attr.style = [`excalidraw-svg${`-${parts[4]}`}`];
}
if (
(!parts[4] || parts[4]==="") &&
(!parts[2] || parts[2]==="") &&
parts[0] && parts[0] !== ""
) {
attr.style = `excalidraw-svg${`-${parts[0]}`}`;
attr.style = [`excalidraw-svg${`-${parts[0]}`}`];
}
}
}
@@ -554,7 +572,7 @@ const tmpObsidianWYSIWYG = async (
fname: ctx.sourcePath,
fheight: "",
fwidth: getDefaultWidth(plugin),
style: "excalidraw-svg",
style: ["excalidraw-svg"],
};
attr.file = file;
@@ -733,7 +751,7 @@ export const observer = new MutationObserver(async (m) => {
fname: file.path,
fwidth: "300",
fheight: null,
style: "excalidraw-svg",
style: ["excalidraw-svg"],
});
const div = createDiv("", async (el) => {
el.appendChild(img);

6
src/constFonts.ts Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,4 @@
import { NonDeletedExcalidrawElement } from "@zsviczian/excalidraw/types/element/types";
import { NonDeletedExcalidrawElement } from "@zsviczian/excalidraw/types/element/types";
import ExcalidrawView from "./ExcalidrawView";
import { Notice, WorkspaceLeaf, WorkspaceSplit } from "obsidian";
import * as React from "react";
@@ -7,6 +7,7 @@ import { DEVICE, EXTENDED_EVENT_TYPES, KEYBOARD_EVENT_TYPES } from "./constants"
import { ExcalidrawImperativeAPI, UIAppState } from "@zsviczian/excalidraw/types/types";
import { ObsidianCanvasNode } from "./utils/CanvasNodeFactory";
import { processLinkText, patchMobileView } from "./utils/CustomEmbeddableUtils";
import { EmbeddableMDCustomProps } from "./dialogs/EmbeddableSettings";
declare module "obsidian" {
interface Workspace {
@@ -18,6 +19,14 @@ declare module "obsidian" {
}
}
const getTheme = (view: ExcalidrawView, theme:string): string => view.excalidrawData.embeddableTheme === "dark"
? "theme-dark"
: view.excalidrawData.embeddableTheme === "light"
? "theme-light"
: view.excalidrawData.embeddableTheme === "auto"
? theme === "dark" ? "theme-dark" : "theme-light"
: isObsidianThemeDark() ? "theme-dark" : "theme-light";
//--------------------------------------------------------------------------------
//Render webview for anything other than Vimeo and Youtube
//Vimeo and Youtube are rendered by Excalidraw because of the window messaging
@@ -59,13 +68,15 @@ export const renderWebView = (src: string, view: ExcalidrawView, id: string, app
//Render WorkspaceLeaf or CanvasNode
//--------------------------------------------------------------------------------
function RenderObsidianView(
{ element, linkText, view, containerRef, appState, theme }:{
{ mdProps, element, linkText, view, containerRef, activeEmbeddable, theme, canvasColor }:{
mdProps: EmbeddableMDCustomProps;
element: NonDeletedExcalidrawElement;
linkText: string;
view: ExcalidrawView;
containerRef: React.RefObject<HTMLDivElement>;
appState: UIAppState;
activeEmbeddable: {element: NonDeletedExcalidrawElement; state: string};
theme: string;
canvasColor: string;
}): JSX.Element {
const { subpath, file } = processLinkText(linkText, view);
@@ -79,8 +90,19 @@ function RenderObsidianView(
const leafRef = react.useRef<{leaf: WorkspaceLeaf; node?: ObsidianCanvasNode} | null>(null);
const isEditingRef = react.useRef(false);
const isActiveRef = react.useRef(false);
const themeRef = react.useRef(theme);
const elementRef = react.useRef(element);
// Update themeRef when theme changes
react.useEffect(() => {
themeRef.current = theme;
}, [theme]);
// Update elementRef when element changes
react.useEffect(() => {
elementRef.current = element;
}, [element]);
//--------------------------------------------------------------------------------
//block propagation of events to the parent if the iframe element is active
//--------------------------------------------------------------------------------
@@ -192,6 +214,7 @@ function RenderObsidianView(
//This runs only when the file is added, thus should not be a major performance issue
await leafRef.current.leaf.setViewState({state: {file:null}})
leafRef.current.node = view.canvasNodeFactory.createFileNote(file, subpath, containerRef.current, element.id);
setColors(containerRef.current, element, mdProps, canvasColor);
} else {
const workspaceLeaf:HTMLDivElement = rootSplit.containerEl.querySelector("div.workspace-leaf");
if(workspaceLeaf) workspaceLeaf.style.borderRadius = "var(--embeddable-radius)";
@@ -205,9 +228,69 @@ function RenderObsidianView(
return () => {}; //cleanup on unmount
}, [linkText, subpath, containerRef]);
const setColors = (canvasNode: HTMLDivElement, element: NonDeletedExcalidrawElement, mdProps: EmbeddableMDCustomProps, canvas: string) => {
if(!mdProps) return;
if (!leafRef.current?.hasOwnProperty("node")) return;
const canvasNodeContainer = containerRef.current?.firstElementChild as HTMLElement;
if(mdProps.useObsidianDefaults) {
canvasNode?.style.removeProperty("--canvas-background");
canvasNodeContainer?.style.removeProperty("background-color");
canvasNode?.style.removeProperty("--canvas-border");
canvasNodeContainer?.style.removeProperty("border-color");
return;
}
const ea = view.plugin.ea;
if(mdProps.backgroundMatchElement) {
const opacity = (mdProps?.backgroundOpacity ?? 50)/100;
const color = element?.backgroundColor
? ea.getCM(element.backgroundColor).alphaTo(opacity).stringHEX()
: "transparent";
canvasNode?.style.setProperty("--canvas-background", color);
canvasNodeContainer?.style.setProperty("background-color", color);
} else if (!(mdProps?.backgroundMatchElement ?? true )) {
const color = mdProps.backgroundMatchCanvas
? ea.getCM(canvasColor).alphaTo((mdProps.backgroundOpacity??100)/100).stringHEX()
: ea.getCM(mdProps.backgroundColor).alphaTo((mdProps.backgroundOpacity??100)/100).stringHEX();
containerRef.current?.style.setProperty("--canvas-background", color);
canvasNodeContainer?.style.setProperty("background-color", color);
}
if(mdProps.borderMatchElement) {
const opacity = (mdProps?.borderOpacity ?? 50)/100;
const color = element?.strokeColor
? ea.getCM(element?.strokeColor).alphaTo(opacity).stringHEX()
: "transparent";
canvasNode?.style.setProperty("--canvas-border", color);
canvasNodeContainer?.style.setProperty("border-color", color);
} else if(!(mdProps?.borderMatchElement ?? true)) {
const color = ea.getCM(mdProps.borderColor).alphaTo((mdProps.borderOpacity??100)/100).stringHEX();
canvasNode?.style.setProperty("--canvas-border", color);
canvasNodeContainer?.style.setProperty("border-color", color);
}
}
react.useEffect(() => {
if(!containerRef.current) {
return;
}
const element = elementRef.current;
const canvasNode = containerRef.current;
if(!canvasNode.hasClass("canvas-node")) return;
setColors(canvasNode, element, mdProps, canvasColor);
}, [
mdProps,
elementRef.current,
containerRef.current,
canvasColor,
])
react.useEffect(() => {
if(isEditingRef.current) {
if(leafRef.current?.node) {
containerRef.current?.addClasses(["is-editing", "is-focused"]);
view.canvasNodeFactory.stopEditing(leafRef.current.node);
}
isEditingRef.current = false;
@@ -242,10 +325,12 @@ function RenderObsidianView(
patchMobileView(view);
} else if (leafRef.current?.node) {
//Handle canvas node
view.canvasNodeFactory.startEditing(leafRef.current.node, theme);
const newTheme = getTheme(view, themeRef.current);
containerRef.current?.addClasses(["is-editing", "is-focused"]);
view.canvasNodeFactory.startEditing(leafRef.current.node, newTheme);
}
}
}, [leafRef.current?.leaf, element.id]);
}, [leafRef.current?.leaf, element.id, view, themeRef.current]);
//--------------------------------------------------------------------------------
// Set isActiveRef and switch to preview mode when the iframe is not active
@@ -256,7 +341,7 @@ function RenderObsidianView(
}
const previousIsActive = isActiveRef.current;
isActiveRef.current = (appState.activeEmbeddable?.element.id === element.id) && (appState.activeEmbeddable?.state === "active");
isActiveRef.current = (activeEmbeddable?.element.id === element.id) && (activeEmbeddable?.state === "active");
if (previousIsActive === isActiveRef.current) {
return;
@@ -278,20 +363,17 @@ function RenderObsidianView(
}
} else if (leafRef.current?.node) {
//Handle canvas node
containerRef.current?.removeClasses(["is-editing", "is-focused"]);
view.canvasNodeFactory.stopEditing(leafRef.current.node);
}
}, [
containerRef,
leafRef,
isActiveRef,
appState.activeEmbeddable?.element,
appState.activeEmbeddable?.state,
activeEmbeddable?.element,
activeEmbeddable?.state,
element,
view,
linkText,
subpath,
file,
theme,
isEditingRef,
view.canvasNodeFactory
]);
@@ -299,16 +381,12 @@ function RenderObsidianView(
return null;
};
export const CustomEmbeddable: React.FC<{element: NonDeletedExcalidrawElement; view: ExcalidrawView; appState: UIAppState; linkText: string}> = ({ element, view, appState, linkText }) => {
const react = view.plugin.getPackage(view.ownerWindow).react;
const containerRef: React.RefObject<HTMLDivElement> = react.useRef(null);
const theme = view.excalidrawData.embeddableTheme === "dark"
? "theme-dark"
: view.excalidrawData.embeddableTheme === "light"
? "theme-light"
: view.excalidrawData.embeddableTheme === "auto"
? appState.theme === "dark" ? "theme-dark" : "theme-light"
: isObsidianThemeDark() ? "theme-dark" : "theme-light";
const theme = getTheme(view, appState.theme);
const mdProps: EmbeddableMDCustomProps = element.customData?.mdProps || null;
return (
<div
@@ -319,15 +397,19 @@ export const CustomEmbeddable: React.FC<{element: NonDeletedExcalidrawElement; v
borderRadius: "var(--embeddable-radius)",
color: `var(--text-normal)`,
}}
className={theme}
className={`${theme} canvas-node ${
mdProps?.filenameVisible ? "" : "excalidraw-mdEmbed-hideFilename"}`}
>
<RenderObsidianView
mdProps={mdProps}
element={element}
linkText={linkText}
view={view}
containerRef={containerRef}
appState={appState}
theme={theme}/>
activeEmbeddable={appState.activeEmbeddable}
theme={appState.theme}
canvasColor={appState.viewBackgroundColor}
/>
</div>
)
}

View File

@@ -0,0 +1,175 @@
import { Setting, ToggleComponent } from "obsidian";
import { EmbeddableMDCustomProps } from "./EmbeddableSettings";
import { fragWithHTML } from "src/utils/Utils";
import { t } from "src/lang/helpers";
export class EmbeddalbeMDFileCustomDataSettingsComponent {
constructor (
private contentEl: HTMLElement,
private mdCustomData: EmbeddableMDCustomProps,
private update?: Function,
) {
if(!update) this.update = () => {};
}
render() {
let detailsDIV: HTMLDivElement;
new Setting(this.contentEl)
.setName(t("ES_USE_OBSIDIAN_DEFAULTS"))
.addToggle(toggle =>
toggle
.setValue(this.mdCustomData.useObsidianDefaults)
.onChange(value => {
this.mdCustomData.useObsidianDefaults = value;
detailsDIV.style.display = value ? "none" : "block";
this.update();
})
);
this.contentEl.createEl("hr", { cls: "excalidraw-setting-hr" });
detailsDIV = this.contentEl.createDiv();
detailsDIV.style.display = this.mdCustomData.useObsidianDefaults ? "none" : "block";
const contentEl = detailsDIV
new Setting(contentEl)
.setName(t("ES_FILENAME_VISIBLE"))
.addToggle(toggle =>
toggle
.setValue(this.mdCustomData.filenameVisible)
.onChange(value => {
this.mdCustomData.filenameVisible = value;
})
);
contentEl.createEl("h4",{text: t("ES_BACKGROUND_HEAD")});
let bgSetting: Setting;
let bgMatchElementToggle: ToggleComponent;
let bgMatchCanvasToggle: ToggleComponent;
new Setting(contentEl)
.setName(t("ES_BACKGROUND_MATCH_ELEMENT"))
.addToggle(toggle => {
bgMatchElementToggle = toggle;
toggle
.setValue(this.mdCustomData.backgroundMatchElement)
.onChange(value => {
this.mdCustomData.backgroundMatchElement = value;
if(value) {
bgSetting.settingEl.style.display = "none";
if(this.mdCustomData.backgroundMatchCanvas) {
bgMatchCanvasToggle.setValue(false);
}
} else {
if(!this.mdCustomData.backgroundMatchCanvas) {
bgSetting.settingEl.style.display = "";
}
}
this.update();
})
});
new Setting(contentEl)
.setName(t("ES_BACKGROUND_MATCH_CANVAS"))
.addToggle(toggle => {
bgMatchCanvasToggle = toggle;
toggle
.setValue(this.mdCustomData.backgroundMatchCanvas)
.onChange(value => {
this.mdCustomData.backgroundMatchCanvas = value;
if(value) {
bgSetting.settingEl.style.display = "none";
if(this.mdCustomData.backgroundMatchElement) {
bgMatchElementToggle.setValue(false);
}
} else {
if(!this.mdCustomData.backgroundMatchElement) {
bgSetting.settingEl.style.display = "";
}
}
this.update();
})
});
if(this.mdCustomData.backgroundMatchElement && this.mdCustomData.backgroundMatchCanvas) {
bgMatchCanvasToggle.setValue(false);
}
bgSetting = new Setting(contentEl)
.setName(t("ES_BACKGROUND_COLOR"))
.addColorPicker(colorPicker =>
colorPicker
.setValue(this.mdCustomData.backgroundColor)
.onChange((value) => {
this.mdCustomData.backgroundColor = value;
this.update();
})
);
bgSetting.settingEl.style.display = (this.mdCustomData.backgroundMatchElement || this.mdCustomData.backgroundMatchCanvas) ? "none" : "";
const opacity = (value:number):DocumentFragment => {
return fragWithHTML(`Current transparency is <b>${value}%</b>`);
}
const bgOpacitySetting = new Setting(contentEl)
.setName(t("ES_BACKGROUND_OPACITY"))
.setDesc(opacity(this.mdCustomData.backgroundOpacity))
.addSlider(slider =>
slider
.setLimits(0,100,5)
.setValue(this.mdCustomData.backgroundOpacity)
.onChange(value => {
this.mdCustomData.backgroundOpacity = value;
bgOpacitySetting.setDesc(opacity(value));
this.update();
})
);
contentEl.createEl("h4",{text: t("ES_BORDER_HEAD")});
let borderSetting: Setting;
new Setting(contentEl)
.setName(t("ES_BORDER_MATCH_ELEMENT"))
.addToggle(toggle =>
toggle
.setValue(this.mdCustomData.borderMatchElement)
.onChange(value => {
this.mdCustomData.borderMatchElement = value;
if(value) {
borderSetting.settingEl.style.display = "none";
} else {
borderSetting.settingEl.style.display = "";
}
this.update();
})
);
borderSetting = new Setting(contentEl)
.setName(t("ES_BORDER_COLOR"))
.addColorPicker(colorPicker =>
colorPicker
.setValue(this.mdCustomData.borderColor)
.onChange((value) => {
this.mdCustomData.borderColor = value;
this.update();
})
);
borderSetting.settingEl.style.display = this.mdCustomData.borderMatchElement ? "none" : "";
const borderOpacitySetting = new Setting(contentEl)
.setName(t("ES_BORDER_OPACITY"))
.setDesc(opacity(this.mdCustomData.borderOpacity))
.addSlider(slider =>
slider
.setLimits(0,100,5)
.setValue(this.mdCustomData.borderOpacity)
.onChange(value => {
this.mdCustomData.borderOpacity = value;
borderOpacitySetting.setDesc(opacity(value));
this.update();
})
);
}
}

View File

@@ -0,0 +1,180 @@
import { ExcalidrawEmbeddableElement } from "@zsviczian/excalidraw/types/element/types";
import { Mutable } from "@zsviczian/excalidraw/types/utility-types";
import { Modal, Notice, Setting, TFile, ToggleComponent } from "obsidian";
import { getEA } from "src";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
import ExcalidrawView from "src/ExcalidrawView";
import { t } from "src/lang/helpers";
import ExcalidrawPlugin from "src/main";
import { getNewUniqueFilepath, getPathWithoutExtension, splitFolderAndFilename } from "src/utils/FileUtils";
import { addAppendUpdateCustomData, fragWithHTML } from "src/utils/Utils";
import { getYouTubeStartAt, isValidYouTubeStart, isYouTube, updateYouTubeStartTime } from "src/utils/YoutTubeUtils";
import { EmbeddalbeMDFileCustomDataSettingsComponent } from "./EmbeddableMDFileCustomDataSettingsComponent";
export type EmbeddableMDCustomProps = {
useObsidianDefaults: boolean;
backgroundMatchCanvas: boolean;
backgroundMatchElement: boolean;
backgroundColor: string;
backgroundOpacity: number;
borderMatchElement: boolean;
borderColor: string;
borderOpacity: number;
filenameVisible: boolean;
}
export class EmbeddableSettings extends Modal {
private ea: ExcalidrawAutomate;
private updatedFilepath: string = null;
private zoomValue: number;
private isYouTube: boolean;
private youtubeStart: string = null;
private isMDFile: boolean;
private mdCustomData: EmbeddableMDCustomProps;
constructor(
private plugin: ExcalidrawPlugin,
private view: ExcalidrawView,
private file: TFile,
private element: ExcalidrawEmbeddableElement
) {
super(plugin.app);
this.ea = getEA(this.view);
this.ea.copyViewElementsToEAforEditing([this.element]);
this.zoomValue = element.scale[0];
this.isYouTube = isYouTube(this.element.link);
this.isMDFile = this.file && this.file.extension === "md" && !this.view.plugin.isExcalidrawFile(this.file)
if(isYouTube) this.youtubeStart = getYouTubeStartAt(this.element.link);
this.mdCustomData = element.customData?.mdProps ?? view.plugin.settings.embeddableMarkdownDefaults;
if(!element.customData?.mdProps) {
const bgCM = this.plugin.ea.getCM(element.backgroundColor);
this.mdCustomData.backgroundColor = bgCM.stringHEX({alpha: false});
this.mdCustomData.backgroundOpacity = element.opacity;
const borderCM = this.plugin.ea.getCM(element.strokeColor);
this.mdCustomData.borderColor = borderCM.stringHEX({alpha: false});
this.mdCustomData.borderOpacity = element.opacity;
}
}
onOpen(): void {
this.containerEl.classList.add("excalidraw-release");
this.titleEl.setText(t("ES_TITLE"));
this.createForm();
}
onClose() {
}
async createForm() {
this.contentEl.createEl("h1",{text: t("ES_TITLE")});
if(this.file) {
new Setting(this.contentEl)
.setName(t("ES_RENAME"))
.addText(text =>
text
.setValue(getPathWithoutExtension(this.file))
.onChange(async (value) => {
this.updatedFilepath = value;
})
)
}
const zoomValue = ():DocumentFragment => {
return fragWithHTML(`Current zoom is <b>${Math.round(this.zoomValue*100)}%</b>`);
}
const zoomSetting = new Setting(this.contentEl)
.setName(t("ES_ZOOM"))
.setDesc(zoomValue())
.addSlider(slider =>
slider
.setLimits(10,400,5)
.setValue(this.zoomValue*100)
.onChange(value => {
this.zoomValue = value/100;
zoomSetting.setDesc(zoomValue());
})
)
if(this.isYouTube) {
new Setting(this.contentEl)
.setName(t("ES_YOUTUBE_START"))
.setDesc(t("ES_YOUTUBE_START_DESC"))
.addText(text =>
text
.setValue(this.youtubeStart)
.onChange(async (value) => {
this.youtubeStart = value;
})
)
}
if(this.isMDFile) {
this.contentEl.createEl("h3",{text: t("ES_EMBEDDABLE_SETTINGS")});
new EmbeddalbeMDFileCustomDataSettingsComponent(this.contentEl,this.mdCustomData).render();
}
const div = this.contentEl.createDiv({cls: "excalidraw-prompt-buttons-div"});
const bOk = div.createEl("button", { text: t("PROMPT_BUTTON_OK"), cls: "excalidraw-prompt-button"});
bOk.onclick = async () => {
let dirty = false;
const el = this.ea.getElement(this.element.id) as Mutable<ExcalidrawEmbeddableElement>;
if(this.updatedFilepath) {
const newPathWithExt = `${this.updatedFilepath}.${this.file.extension}`;
if(newPathWithExt !== this.file.path) {
const fnparts = splitFolderAndFilename(newPathWithExt);
const newPath = getNewUniqueFilepath(
this.app.vault,
fnparts.folderpath,
fnparts.filename,
);
await this.app.vault.rename(this.file,newPath);
el.link = this.element.link.replace(
/(\[\[)([^#\]]*)([^\]]*]])/,`$1${
this.plugin.app.metadataCache.fileToLinktext(
this.file,this.view.file.path,true)
}$3`);
dirty = true;
}
}
if(this.isYouTube && this.youtubeStart !== getYouTubeStartAt(this.element.link)) {
dirty = true;
if(isValidYouTubeStart(this.youtubeStart)) {
el.link = updateYouTubeStartTime(el.link,this.youtubeStart);
} else {
new Notice(t("ES_YOUTUBE_START_INVALID"));
}
}
if(
this.isMDFile && (
this.mdCustomData.backgroundColor !== this.element.customData?.backgroundColor ||
this.mdCustomData.borderColor !== this.element.customData?.borderColor ||
this.mdCustomData.backgroundOpacity !== this.element.customData?.backgroundOpacity ||
this.mdCustomData.borderOpacity !== this.element.customData?.borderOpacity ||
this.mdCustomData.filenameVisible !== this.element.customData?.filenameVisible)
) {
addAppendUpdateCustomData(el,{mdProps: this.mdCustomData});
dirty = true;
}
if(this.zoomValue !== this.element.scale[0]) {
dirty = true;
el.scale = [this.zoomValue,this.zoomValue];
}
if(dirty) {
this.ea.addElementsToView();
}
this.close();
};
const bCancel = div.createEl("button", { text: t("PROMPT_BUTTON_CANCEL"), cls: "excalidraw-prompt-button" });
bCancel.onclick = () => {
this.close();
};
}
}

View File

@@ -16,6 +16,39 @@ export const RELEASE_NOTES: { [k: string]: string } = {
I develop this plugin as a hobby, spending my free time doing this. If you find it valuable, then please say THANK YOU or...
<div class="ex-coffee-div"><a href="https://ko-fi.com/zsolt"><img src="https://cdn.ko-fi.com/cdn/kofi3.png?v=3" height=45></a></div>
`,
"2.0.1":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/xmqiBTrlbEM" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## Fixed
- bug with cssclasses in frontmatter
- styling of help screen keyboard shortcuts [#1437](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1437)
`,
"2.0.0":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/JC1E-jeiWhI" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## New
- Added support for applying CSS classes in frontmatter. Now, when embedding Excalidraw drawings into Obsidian Canvas, you can use [Canvas Candy](https://tfthacker.com/canvas-candy) classes. For instance, ${String.fromCharCode(96)}cssclasses: cc-border-none${String.fromCharCode(96)} removes the canvas node border around the drawing.
- Introduced new context menu actions:
- Navigate to link or embedded image.
- Add any file from the vault to the canvas.
- Convert the selected text element or sticky note to an embedded markdown file.
- Add a link from the Vault to the selected element.
- Frames are now rendered in exported images.
- SVG Export includes the ${String.fromCharCode(96)}.excalidraw-svg${String.fromCharCode(96)} class, enabling post-processing of SVGs using publish.js when using custom domains with Obsidian Publish. Also, added a command palette action ${String.fromCharCode(96)}Obsidian Publish: Find SVG and PNG exports that are out of date${String.fromCharCode(96)}.
- Added a new Command palette action to open the corresponding Excalidraw file based on the embedded SVG or PNG file. ${String.fromCharCode(96)}Open Excalidraw Drawing${String.fromCharCode(96)} [Issue #1411](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1411)
## Fixed and Improved
- Resolved issue with the Mermaid Timeline graph displaying all black. [Issue #1424](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1424)
- Enabled toggling pen mode off after activation by a pen touch.
- Now you are able to unlock elements on mobile; previously, locked elements couldn't be selected.
- Fixed the disabled ${String.fromCharCode(96)}complete line button${String.fromCharCode(96)} for multipoint lines on mobile.
![Mobile Editing Image](https://github.com/zsviczian/obsidian-excalidraw-plugin/assets/14358394/e7051c75-818f-4800-ba16-ac276e229184)
`,
"1.9.28":`
## Fixed & Improved

View File

@@ -21,7 +21,7 @@ const haveLinkedFilesChanged = (depth: number, mtime: number, path: string, sour
return false;
}
const listOfOutOfSyncSVGExports = async(plugin: ExcalidrawPlugin, recursive: boolean):Promise<TFile[]> => {
const listOfOutOfSyncImgExports = async(plugin: ExcalidrawPlugin, recursive: boolean, statusEl: HTMLParagraphElement):Promise<TFile[]> => {
const app = plugin.app;
const publish = app.internalPlugins.plugins["publish"].instance;
@@ -29,13 +29,15 @@ const listOfOutOfSyncSVGExports = async(plugin: ExcalidrawPlugin, recursive: boo
const list = await app.internalPlugins.plugins["publish"].instance.apiList();
if(!list || !list.files) return;
const outOfSyncFiles = new Set<TFile>();
list.files.filter((f:any)=>f.path.endsWith(".svg")).forEach((f:any)=>{
const allFiles = list.files.filter((f:any)=>(f.path.endsWith(".svg") || f.path.endsWith(".png")))
const totalCount = allFiles.length;
allFiles.forEach((f:any, idx:number)=>{
const maybeExcalidraFilePath = getIMGFilename(f.path,"md");
const svgFile = app.vault.getAbstractFileByPath(f.path);
const imgFile = app.vault.getAbstractFileByPath(f.path);
const excalidrawFile = app.vault.getAbstractFileByPath(maybeExcalidraFilePath);
if(!excalidrawFile || !svgFile || !(excalidrawFile instanceof TFile) || !(svgFile instanceof TFile)) return;
console.log(excalidrawFile, {mtimeEx: excalidrawFile.stat.mtime, mtimeSVG: svgFile.stat.mtime});
if(excalidrawFile.stat.mtime <= svgFile.stat.mtime) {
statusEl.innerText = `Status: ${idx+1}/${totalCount} ${imgFile ? imgFile.name : f.path}`;
if(!excalidrawFile || !imgFile || !(excalidrawFile instanceof TFile) || !(imgFile instanceof TFile)) return;
if(excalidrawFile.stat.mtime <= imgFile.stat.mtime) {
if(!recursive) return;
if(!haveLinkedFilesChanged(0, excalidrawFile.stat.mtime, excalidrawFile.path, new Set<string>(), plugin)) return;
}
@@ -64,10 +66,12 @@ export class PublishOutOfDateFilesDialog extends Modal {
detailsEl.createEl("summary", {
text: "Video about Obsidian Publish support",
});
addIframe(detailsEl, "OX5_UYjXEvc");
detailsEl.createEl("br");
addIframe(detailsEl, "JC1E-jeiWhI");
const p = this.contentEl.createEl("p",{text: "Collecting data..."});
const files = await listOfOutOfSyncSVGExports(this.plugin, recursive);
const statusEl = this.contentEl.createEl("p", {text: "Status: "});
const files = await listOfOutOfSyncImgExports(this.plugin, recursive, statusEl);
statusEl.style.display = "none";
if(!files || files.length === 0) {
p.innerText = "No out of date files found.";

View File

@@ -9,7 +9,9 @@ import { labelALT, labelCTRL, labelMETA, labelSHIFT } from "src/utils/Modifierke
// English
export default {
// main.ts
PUBLISH_SVG_CHECK: "Obsidian Publish: Find SVGs that are out of date",
PUBLISH_SVG_CHECK: "Obsidian Publish: Find SVG and PNG exports that are out of date",
EMBEDDABLE_PROPERTIES: "Embeddable Properties",
OPEN_IMAGE_SOURCE: "Open Excalidraw drawing",
INSTALL_SCRIPT: "Install the script",
UPDATE_SCRIPT: "Update available - Click to install",
CHECKING_SCRIPT:
@@ -323,8 +325,11 @@ FILENAME_HEAD: "Filename",
PDF_TO_IMAGE_SCALE_NAME: "PDF to Image conversion scale",
PDF_TO_IMAGE_SCALE_DESC: "Sets the resolution of the image that is generated from the PDF page. Higher resolution will result in bigger images in memory and consequently a higher load on your system (slower performance), but sharper imagee. " +
"Additionally, if you want to copy PDF pages (as images) to Excalidraw.com, the bigger image size may result in exceeding the 2MB limit on Excalidraw.com.",
EMBED_TOEXCALIDRAW_HEAD: "Embed files into Excalidraw",
EMBED_TOEXCALIDRAW_DESC: "In the Embed Files section of Excalidraw Settings, you can configure how various files are embedded into Excalidraw. This includes options for embedding interactive markdown files, PDFs, and markdown files as images.",
MD_HEAD: "Embed markdown into Excalidraw as image",
MD_HEAD_DESC: `In the "Embed markdown as image settings," you can configure various options for how markdown documents are embedded as images within Excalidraw. These settings allow you to control the default width and maximum height of embedded markdown files, choose the font typeface, font color, and border color for embedded markdown content. Additionally, you can specify a custom CSS file to style the embedded markdown content. Note you can also embed markdown documents as interactive frames. The color setting of frames is under the Display Settings section.`,
MD_EMBED_CUSTOMDATA_HEAD_NAME: "Interactive Markdown Files",
MD_EMBED_CUSTOMDATA_HEAD_DESC: `These settings will only effect future embeds. Current embeds remain unchanged. The theme setting of embedded frames is under the "Excalidraw appearance and behavior" section.`,
MD_TRANSCLUDE_WIDTH_NAME: "Default width of a transcluded markdown document",
MD_TRANSCLUDE_WIDTH_DESC:
"The width of the markdown page. This affects the word wrapping when transcluding longer paragraphs, and the width of " +
@@ -363,6 +368,11 @@ FILENAME_HEAD: "Filename",
EMBED_HEAD: "Embedding Excalidraw into your Notes and Exporting",
EMBED_DESC: `In the "Embed & Export" settings, you can configure how images and Excalidraw drawings are embedded and exported within your documents. Key settings include choosing the image type for markdown preview (such as Native SVG or PNG), specifying the type of file to insert into the document (original Excalidraw, PNG, or SVG), and managing image caching for embedding in markdown. You can also control image sizing, whether to embed drawings using wiki links or markdown links, and adjust settings related to image themes, background colors, and Obsidian integration.
Additionally, there are settings for auto-export, which automatically generates SVG and/or PNG files to match the title of your Excalidraw drawings, keeping them in sync with file renames and deletions.`,
EMBED_CANVAS: "Obsidian Canvas support",
EMBED_CANVAS_NAME: "Immersive embedding",
EMBED_CANVAS_DESC:
"Hide canvas node border and background when embedding an Excalidraw drawing to Canvas. " +
"Note that for a full transparent background for your image, you will still need to configure Excalidraw to export images with transparent background.",
EMBED_CACHING: "Image caching",
EXPORT_SUBHEAD: "Export Settings",
EMBED_SIZING: "Image sizing",
@@ -405,6 +415,11 @@ FILENAME_HEAD: "Filename",
"or a PNG or an SVG copy. You need to enable auto-export PNG / SVG (see below under Export Settings) for those image types to be available in the dropdown. For drawings that do not have a " +
"a corresponding PNG or SVG readily available the command palette action will insert a broken link. You need to open the original drawing and initiate export manually. " +
"This option will not autogenerate PNG/SVG files, but will simply reference the already existing files.",
EMBED_MARKDOWN_COMMENT_NAME: "Embed link to drawing as comment",
EMBED_MARKDOWN_COMMENT_DESC:
"Embed the link to the original Excalidraw file as a markdown link under the image, e.g.:<code>%%[[drawing.excalidraw]]%%</code>.<br>" +
"Instead of adding a markdown comment you may also select the embedded SVG or PNG line and use the command palette action: " +
"'<code>Excalidraw: Open Excalidraw drawing</code>' to open the drawing.",
EMBED_WIKILINK_NAME: "Embed Drawing using Wiki link",
EMBED_WIKILINK_DESC:
"<b><u>Toggle ON:</u></b> Excalidraw will embed a [[wiki link]].<br><b><u>Toggle OFF:</u></b> Excalidraw will embed a [markdown](link).",
@@ -457,10 +472,10 @@ FILENAME_HEAD: "Filename",
"and the file explorer are going to be all legacy *.excalidraw files. This setting will also turn off the reminder message " +
"when you open a legacy file for editing.",
MATHJAX_NAME: "MathJax (LaTeX) javascript library host",
MATHJAX_DESC: "If you are using LaTeX equiations in Excalidraw then the plugin needs to load a javascript library for that. " +
"Some users are unable to access certain host servers. If you are experiencing issues try changing the host here. You may need to "+
MATHJAX_DESC: "If you are using LaTeX equations in Excalidraw, then the plugin needs to load a javascript library for that. " +
"Some users are unable to access certain host servers. If you are experiencing issues, try changing the host here. You may need to "+
"restart Obsidian after closing settings, for this change to take effect.",
LATEX_DEFAULT_NAME: "Default LaTeX formual for new equations",
LATEX_DEFAULT_NAME: "Default LaTeX formula for new equations",
LATEX_DEFAULT_DESC: "Leave empty if you don't want a default formula. You can add default formatting here such as <code>\\color{white}</code>.",
NONSTANDARD_HEAD: "Non-Excalidraw.com supported features",
NONSTANDARD_DESC: `These settings in the "Non-Excalidraw.com Supported Features" section provide customization options beyond the default Excalidraw.com features. These features are not available on excalidraw.com. When exporting the drawing to Excalidraw.com these features will appear different.
@@ -557,6 +572,27 @@ FILENAME_HEAD: "Filename",
ZOOM_TO_FIT: "Zoom to fit",
RELOAD: "Reload original link",
OPEN_IN_BROWSER: "Open current link in browser",
PROPERTIES: "Properties",
//EmbeddableSettings.tsx
ES_TITLE: "Element Settings",
ES_RENAME: "Rename File",
ES_ZOOM: "Embedded Content Zoom",
ES_YOUTUBE_START: "YouTube Start Time",
ES_YOUTUBE_START_DESC: "ss, mm:ss, hh:mm:ss",
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_MATCH_ELEMENT: "Match Element Background Color",
ES_BACKGROUND_MATCH_CANVAS: "Match Canvas Background Color",
ES_BACKGROUND_COLOR: "Background Color",
ES_BORDER_HEAD: "Embedded note border color",
ES_BORDER_COLOR: "Border Color",
ES_BORDER_MATCH_ELEMENT: "Match Element Border Color",
ES_BACKGROUND_OPACITY: "Background Transparency",
ES_BORDER_OPACITY: "Border Transparency",
ES_EMBEDDABLE_SETTINGS: "Embeddable Markdown Settings",
ES_USE_OBSIDIAN_DEFAULTS: "Use Obsidian Defaults",
//Prompts.ts
PROMPT_FILE_DOES_NOT_EXIST: "File does not exist. Do you want to create it?",

View File

@@ -21,7 +21,6 @@ import {
Editor,
MarkdownFileInfo,
loadMermaid,
moment,
} from "obsidian";
import {
BLANK_DRAWING,
@@ -38,13 +37,15 @@ import {
DARK_BLANK_DRAWING,
SCRIPT_INSTALL_CODEBLOCK,
SCRIPT_INSTALL_FOLDER,
VIRGIL_FONT,
VIRGIL_DATAURL,
EXPORT_TYPES,
EXPORT_IMG_ICON_NAME,
EXPORT_IMG_ICON,
LOCALE,
} from "./constants";
import {
VIRGIL_FONT,
VIRGIL_DATAURL,
} from "./constFonts";
import ExcalidrawView, { TextMode, getTextMode } from "./ExcalidrawView";
import {
changeThemeOfExcalidrawMD,
@@ -90,9 +91,9 @@ import {
getExportTheme,
isCallerFromTemplaterPlugin,
} from "./utils/Utils";
import { getAttachmentsFolderAndFilePath, getNewOrAdjacentLeaf, getParentOfClass, isObsidianThemeDark } from "./utils/ObsidianUtils";
import { extractSVGPNGFileName, getAttachmentsFolderAndFilePath, getNewOrAdjacentLeaf, getParentOfClass, isObsidianThemeDark } from "./utils/ObsidianUtils";
//import { OneOffs } from "./OneOffs";
import { ExcalidrawElement, ExcalidrawImageElement, ExcalidrawTextElement, FileId } from "@zsviczian/excalidraw/types/element/types";
import { ExcalidrawElement, ExcalidrawEmbeddableElement, ExcalidrawImageElement, ExcalidrawTextElement, FileId } from "@zsviczian/excalidraw/types/element/types";
import { ScriptEngine } from "./Scripts";
import {
hoverEvent,
@@ -115,8 +116,8 @@ import { UniversalInsertFileModal } from "./dialogs/UniversalInsertFileModal";
import { imageCache } from "./utils/ImageCache";
import { StylesManager } from "./utils/StylesManager";
import { MATHJAX_SOURCE_LZCOMPRESSED } from "./constMathJaxSource";
import { getEA } from "src";
import { PublishOutOfDateFilesDialog } from "./dialogs/PublishOutOfDateFiles";
import { EmbeddableSettings } from "./dialogs/EmbeddableSettings";
declare const EXCALIDRAW_PACKAGES:string;
declare const react:any;
@@ -693,7 +694,7 @@ export default class ExcalidrawPlugin extends Plugin {
const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
leaves.forEach((leaf: WorkspaceLeaf) => {
const excalidrawView = leaf.view as ExcalidrawView;
if (excalidrawView.file && excalidrawView.excalidrawRef) {
if (excalidrawView.file && excalidrawView.excalidrawAPI) {
excalidrawView.setTheme(theme);
}
});
@@ -853,6 +854,55 @@ export default class ExcalidrawPlugin extends Plugin {
}
})
this.addCommand({
id: "excalidraw-embeddable-poroperties",
name: t("EMBEDDABLE_PROPERTIES"),
checkCallback: (checking: boolean) => {
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
if(!view) return false;
if(!view.excalidrawAPI) return false;
const els = view.getViewSelectedElements().filter(el=>el.type==="embeddable") as ExcalidrawEmbeddableElement[];
if(els.length !== 1) {
if(checking) return false;
new Notice("Select a single embeddable element and try again");
return false;
}
if(checking) return true;
new EmbeddableSettings(view.plugin,view,null,els[0]).open();
}
})
this.addCommand({
id: "open-image-excalidraw-source",
name: t("OPEN_IMAGE_SOURCE"),
checkCallback: (checking: boolean) => {
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if(!view) return false;
if(view.leaf !== this.app.workspace.activeLeaf) return false;
const editor = view.editor;
if(!editor) return false;
const cursor = editor.getCursor();
const line = editor.getLine(cursor.line);
const fname = extractSVGPNGFileName(line);
if(!fname) return false;
const imgFile = this.app.metadataCache.getFirstLinkpathDest(fname, view.file.path);
if(!imgFile) return false;
const excalidrawFname = getIMGFilename(imgFile.path, "md");
let excalidrawFile = this.app.metadataCache.getFirstLinkpathDest(excalidrawFname, view.file.path);
if(!excalidrawFile) {
if(excalidrawFname.endsWith(".dark.md")) {
excalidrawFile = this.app.metadataCache.getFirstLinkpathDest(excalidrawFname.replace(/\.dark\.md$/,".md"), view.file.path);
}
if(excalidrawFname.endsWith(".light.md")) {
excalidrawFile = this.app.metadataCache.getFirstLinkpathDest(excalidrawFname.replace(/\.light\.md$/,".md"), view.file.path);
}
if(!excalidrawFile) return false;
}
if(checking) return true;
this.openDrawing(excalidrawFile, "new-tab", true);
}
});
this.addCommand({
id: "excalidraw-disable-autosave",
name: t("TEMPORARY_DISABLE_AUTOSAVE"),
@@ -1488,7 +1538,7 @@ export default class ExcalidrawPlugin extends Plugin {
checkCallback: (checking: boolean) => {
if (checking) {
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
if (!view || !view.excalidrawRef) {
if (!view || !view.excalidrawAPI) {
return false;
}
const st = view.excalidrawAPI.getAppState();
@@ -2360,7 +2410,9 @@ export default class ExcalidrawPlugin extends Plugin {
)
: "";
theme = theme===""?"":theme+".";
theme = (theme === "")
? ""
: theme + ".";
const imageRelativePath = getIMGFilename(
excalidrawRelativePath,
@@ -2372,12 +2424,13 @@ export default class ExcalidrawPlugin extends Plugin {
);
//will hold incorrect value if theme==="", however in that case it won't be used
const otherTheme = theme === "dark." ? "light.":"dark.";
const otherImageRelativePath = getIMGFilename(
excalidrawRelativePath,
otherTheme+this.settings.embedType.toLowerCase(),
);
const otherTheme = theme === "dark." ? "light." : "dark.";
const otherImageRelativePath = theme === ""
? null
: getIMGFilename(
excalidrawRelativePath,
otherTheme+this.settings.embedType.toLowerCase(),
);
const imgFile = this.app.vault.getAbstractFileByPath(imageFullpath);
if (!imgFile) {
@@ -2385,13 +2438,21 @@ export default class ExcalidrawPlugin extends Plugin {
await sleep(200); //wait for metadata cache to update
}
const inclCom = this.settings.embedMarkdownCommentLinks;
editor.replaceSelection(
this.settings.embedWikiLink
? `![[${imageRelativePath}]]\n%%[[${excalidrawRelativePath}|🖋 Edit in Excalidraw]]${
otherImageRelativePath ? ", and the [["+otherImageRelativePath+"|"+otherTheme.split(".")[0]+" exported image]]":""}%%`
: `![](${encodeURI(imageRelativePath)})\n%%[🖋 Edit in Excalidraw](${encodeURI(
excalidrawRelativePath,
)})${otherImageRelativePath?", and the ["+otherTheme.split(".")[0]+" exported image]("+encodeURI(otherImageRelativePath)+")":""}%%`,
? `![[${imageRelativePath}]]\n` +
(inclCom
? `%%[[${excalidrawRelativePath}|🖋 Edit in Excalidraw]]${
otherImageRelativePath
? ", and the [["+otherImageRelativePath+"|"+otherTheme.split(".")[0]+" exported image]]"
: ""
}%%`
: "")
: `![](${encodeURI(imageRelativePath)})\n` +
(inclCom ? `%%[🖋 Edit in Excalidraw](${encodeURI(excalidrawRelativePath,
)})${otherImageRelativePath?", and the ["+otherTheme.split(".")[0]+" exported image]("+encodeURI(otherImageRelativePath)+")":""}%%` : ""),
);
editor.focus();
}

View File

@@ -1,4 +1,4 @@
import { Globe, RotateCcw, Scan } from "lucide-react";
import { Cog, Globe, RotateCcw, Scan, Settings } from "lucide-react";
import * as React from "react";
import { PenStyle } from "src/PenTypes";
@@ -29,6 +29,7 @@ export const ICONS = {
Reload: (<RotateCcw />),
Globe: (<Globe />),
ZoomToSelectedElement: (<Scan />),
Properties: (<Settings />),
ZoomToSection: (
<svg
xmlns="http://www.w3.org/2000/svg"

View File

@@ -11,6 +11,7 @@ import { ROOTELEMENTSIZE, mutateElement, nanoid, sceneCoordsToViewportCoords } f
import { REGEX_LINK, REG_LINKINDEX_HYPERLINK } from "src/ExcalidrawData";
import { processLinkText, useDefaultExcalidrawFrame } from "src/utils/CustomEmbeddableUtils";
import { cleanSectionHeading } from "src/utils/ObsidianUtils";
import { EmbeddableSettings } from "src/dialogs/EmbeddableSettings";
export class EmbeddableMenu {
@@ -91,7 +92,8 @@ export class EmbeddableMenu {
if(!isObsidianiFrame) {
const { subpath, file } = processLinkText(link, view);
if(!file || file.extension!=="md") return null;
if(!file) return;
const isMD = file.extension==="md";
const { x, y } = sceneCoordsToViewportCoords( { sceneX: element.x, sceneY: element.y }, appState);
const top = `${y-2.5*ROOTELEMENTSIZE-appState.offsetTop}px`;
const left = `${x-appState.offsetLeft}px`;
@@ -116,69 +118,73 @@ export class EmbeddableMenu {
display: "block",
}}
>
<ActionButton
key={"MarkdownSection"}
title={t("NARROW_TO_HEADING")}
action={async () => {
const sections = (await app.metadataCache.blockCache
.getForFile({ isCancelled: () => false },file))
.blocks.filter((b: any) => b.display && b.node?.type === "heading");
const values = [""].concat(
sections.map((b: any) => `#${cleanSectionHeading(b.display)}`)
);
const display = [t("SHOW_ENTIRE_FILE")].concat(
sections.map((b: any) => b.display)
);
const newSubpath = await ScriptEngine.suggester(
app, display, values, "Select section from document"
);
if(!newSubpath && newSubpath!=="") return;
if (newSubpath !== subpath) {
this.updateElement(newSubpath, element, file);
}
}}
icon={ICONS.ZoomToSection}
view={view}
/>
<ActionButton
key={"MarkdownBlock"}
title={t("NARROW_TO_BLOCK")}
action={async () => {
if(!file) return;
const paragrphs = (await app.metadataCache.blockCache
.getForFile({ isCancelled: () => false },file))
.blocks.filter((b: any) => b.display && b.node?.type === "paragraph");
const values = ["entire-file"].concat(paragrphs);
const display = [t("SHOW_ENTIRE_FILE")].concat(
paragrphs.map((b: any) => `${b.node?.id ? `#^${b.node.id}: ` : ``}${b.display.trim()}`));
const selectedBlock = await ScriptEngine.suggester(
app, display, values, "Select section from document"
);
if(!selectedBlock) return;
{isMD && (
<ActionButton
key={"MarkdownSection"}
title={t("NARROW_TO_HEADING")}
action={async () => {
const sections = (await app.metadataCache.blockCache
.getForFile({ isCancelled: () => false },file))
.blocks.filter((b: any) => b.display && b.node?.type === "heading");
const values = [""].concat(
sections.map((b: any) => `#${cleanSectionHeading(b.display)}`)
);
const display = [t("SHOW_ENTIRE_FILE")].concat(
sections.map((b: any) => b.display)
);
const newSubpath = await ScriptEngine.suggester(
app, display, values, "Select section from document"
);
if(!newSubpath && newSubpath!=="") return;
if (newSubpath !== subpath) {
this.updateElement(newSubpath, element, file);
}
}}
icon={ICONS.ZoomToSection}
view={view}
/>
)}
{isMD && (
<ActionButton
key={"MarkdownBlock"}
title={t("NARROW_TO_BLOCK")}
action={async () => {
if(!file) return;
const paragrphs = (await app.metadataCache.blockCache
.getForFile({ isCancelled: () => false },file))
.blocks.filter((b: any) => b.display && b.node?.type === "paragraph");
const values = ["entire-file"].concat(paragrphs);
const display = [t("SHOW_ENTIRE_FILE")].concat(
paragrphs.map((b: any) => `${b.node?.id ? `#^${b.node.id}: ` : ``}${b.display.trim()}`));
const selectedBlock = await ScriptEngine.suggester(
app, display, values, "Select section from document"
);
if(!selectedBlock) return;
if(selectedBlock==="entire-file") {
if(subpath==="") return;
this.updateElement("", element, file);
return;
}
let blockID = selectedBlock.node.id;
if(blockID && (`#^${blockID}` === subpath)) return;
if (!blockID) {
const offset = selectedBlock.node?.position?.end?.offset;
if(!offset) return;
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 sleep(200); //wait for cache to update
}
this.updateElement(`#^${blockID}`, element, file);
}}
icon={ICONS.ZoomToBlock}
view={view}
/>
if(selectedBlock==="entire-file") {
if(subpath==="") return;
this.updateElement("", element, file);
return;
}
let blockID = selectedBlock.node.id;
if(blockID && (`#^${blockID}` === subpath)) return;
if (!blockID) {
const offset = selectedBlock.node?.position?.end?.offset;
if(!offset) return;
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 sleep(200); //wait for cache to update
}
this.updateElement(`#^${blockID}`, element, file);
}}
icon={ICONS.ZoomToBlock}
view={view}
/>
)}
<ActionButton
key={"ZoomToElement"}
title={t("ZOOM_TO_FIT")}
@@ -189,6 +195,16 @@ export class EmbeddableMenu {
icon={ICONS.ZoomToSelectedElement}
view={view}
/>
<ActionButton
key={"Properties"}
title={t("PROPERTIES")}
action={() => {
if(!element) return;
new EmbeddableSettings(view.plugin,view,file,element).open();
}}
icon={ICONS.Properties}
view={view}
/>
</div>
</div>
);
@@ -256,6 +272,16 @@ export class EmbeddableMenu {
icon={ICONS.ZoomToSelectedElement}
view={view}
/>
<ActionButton
key={"Properties"}
title={t("PROPERTIES")}
action={() => {
if(!element) return;
new EmbeddableSettings(view.plugin,view,null,element).open();
}}
icon={ICONS.Properties}
view={view}
/>
</div>
</div>
);

View File

@@ -28,6 +28,8 @@ import {
import { imageCache } from "./utils/ImageCache";
import { ConfirmationPrompt } from "./dialogs/Prompt";
import de from "./lang/locale/de";
import { EmbeddableMDCustomProps } from "./dialogs/EmbeddableSettings";
import { EmbeddalbeMDFileCustomDataSettingsComponent } from "./dialogs/EmbeddableMDFileCustomDataSettingsComponent";
export interface ExcalidrawSettings {
folder: string;
@@ -89,6 +91,7 @@ export interface ExcalidrawSettings {
autoExportLightAndDark: boolean;
autoexportExcalidraw: boolean;
embedType: "excalidraw" | "PNG" | "SVG";
embedMarkdownCommentLinks: boolean;
embedWikiLink: boolean;
syncExcalidraw: boolean;
compatibilityMode: boolean;
@@ -148,6 +151,8 @@ export interface ExcalidrawSettings {
DECAY_LENGTH: number,
COLOR: string,
};
embeddableMarkdownDefaults: EmbeddableMDCustomProps;
canvasImmersiveEmbed: boolean,
}
declare const PLUGIN_VERSION:string;
@@ -212,6 +217,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
autoExportLightAndDark: false,
autoexportExcalidraw: false,
embedType: "excalidraw",
embedMarkdownCommentLinks: true,
embedWikiLink: true,
syncExcalidraw: false,
experimentalFileType: false,
@@ -276,7 +282,19 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
DECAY_LENGTH: 50,
DECAY_TIME: 1000,
COLOR: "#ff0000",
}
},
embeddableMarkdownDefaults: {
useObsidianDefaults: false,
backgroundMatchCanvas: false,
backgroundMatchElement: true,
backgroundColor: "#fff",
backgroundOpacity: 60,
borderMatchElement: true,
borderColor: "#fff",
borderOpacity: 0,
filenameVisible: false,
},
canvasImmersiveEmbed: true,
};
export class ExcalidrawSettingTab extends PluginSettingTab {
@@ -1200,7 +1218,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
addIframe(detailsEl, "opLd1SqaH_I",8);
let dropdown: DropdownComponent;
let embedComment: Setting;
new Setting(detailsEl)
.setName(t("EMBED_TYPE_NAME"))
.setDesc(fragWithHTML(t("EMBED_TYPE_DESC")))
@@ -1224,9 +1242,24 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.onChange(async (value) => {
//@ts-ignore
this.plugin.settings.embedType = value;
embedComment.settingEl.style.display = value === "excalidraw" ? "none":"";
this.applySettingsUpdate();
});
});
embedComment = new Setting(detailsEl)
.setName(t("EMBED_MARKDOWN_COMMENT_NAME"))
.setDesc(fragWithHTML(t("EMBED_MARKDOWN_COMMENT_DESC")))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.embedMarkdownCommentLinks)
.onChange(async (value) => {
this.plugin.settings.embedMarkdownCommentLinks = value;
this.applySettingsUpdate();
}),
);
embedComment.settingEl.style.display = this.plugin.settings.embedType === "excalidraw" ? "none":"";
new Setting(detailsEl)
.setName(t("EMBED_WIKILINK_NAME"))
@@ -1240,6 +1273,24 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
}),
);
detailsEl = embedDetailsEl.createEl("details");
detailsEl.createEl("summary", {
text: t("EMBED_CANVAS"),
cls: "excalidraw-setting-h3",
});
new Setting(detailsEl)
.setName(t("EMBED_CANVAS_NAME"))
.setDesc(fragWithHTML(t("EMBED_CANVAS_DESC")))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.canvasImmersiveEmbed)
.onChange(async (value) => {
this.plugin.settings.canvasImmersiveEmbed = value;
this.applySettingsUpdate();
}),
);
detailsEl = embedDetailsEl.createEl("details");
detailsEl.createEl("summary", {
text: t("EMBED_CACHING"),
@@ -1483,9 +1534,22 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
this.plugin.settings.autoExportLightAndDark = value;
this.applySettingsUpdate();
}),
);
);
detailsEl = embedDetailsEl.createEl("details");
// ------------------------------------------------
// Embedding settings
// ------------------------------------------------
containerEl.createEl("hr", { cls: "excalidraw-setting-hr" });
containerEl.createDiv({ text: t("EMBED_TOEXCALIDRAW_DESC"), cls: "setting-item-description" });
detailsEl = this.containerEl.createEl("details");
const embedFilesDetailsEl = detailsEl;
detailsEl.createEl("summary", {
text: t("EMBED_TOEXCALIDRAW_HEAD"),
cls: "excalidraw-setting-h1",
});
detailsEl = embedFilesDetailsEl.createEl("details");
detailsEl.createEl("summary", {
text: t("PDF_TO_IMAGE"),
cls: "excalidraw-setting-h3",
@@ -1510,20 +1574,28 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
this.applySettingsUpdate();
}),
);
// ------------------------------------------------
// Markdown embedding settings
// ------------------------------------------------
containerEl.createEl("hr", { cls: "excalidraw-setting-hr" });
containerEl.createDiv({ text: t("MD_HEAD_DESC"), cls: "setting-item-description" });
detailsEl = this.containerEl.createEl("details");
detailsEl = embedFilesDetailsEl.createEl("details");
detailsEl.createEl("summary", {
text: t("MD_EMBED_CUSTOMDATA_HEAD_NAME"),
cls: "excalidraw-setting-h3",
});
detailsEl.createEl("span", {text: t("MD_EMBED_CUSTOMDATA_HEAD_DESC")});
new EmbeddalbeMDFileCustomDataSettingsComponent(
detailsEl,
this.plugin.settings.embeddableMarkdownDefaults,
this.applySettingsUpdate,
).render();
detailsEl = embedFilesDetailsEl.createEl("details");
detailsEl.createEl("summary", {
text: t("MD_HEAD"),
cls: "excalidraw-setting-h1",
cls: "excalidraw-setting-h3",
});
new Setting(detailsEl)
.setName(t("MD_TRANSCLUDE_WIDTH_NAME"))
.setDesc(fragWithHTML(t("MD_TRANSCLUDE_WIDTH_DESC")))
@@ -1582,6 +1654,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.addDropdown(async (d: DropdownComponent) => {
d.addOption("Virgil", "Virgil");
d.addOption("Cascadia", "Cascadia");
d.addOption("Assistant", "Assistant");
this.app.vault
.getFiles()
.filter((f) => ["ttf", "woff", "woff2"].contains(f.extension))

View File

@@ -72,8 +72,9 @@ export class CanvasNodeFactory {
const node = this.canvas.createFileNode({pos: {x:0,y:0}, file, subpath, save: false});
node.setFilePath(file.path,subpath);
node.render();
containerEl.style.background = "var(--background-primary)";
containerEl.appendChild(node.contentEl)
//containerEl.style.background = "var(--background-primary)";
node.containerEl.querySelector(".canvas-node-content-blocker")?.remove();
containerEl.appendChild(node.containerEl)
this.nodes.set(elementId, node);
return node;
}

View File

@@ -1,7 +1,12 @@
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/types";
import { ColorMaster } from "colormaster";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
import ExcalidrawView from "src/ExcalidrawView";
import { DynamicStyle } from "src/types";
import { cloneElement } from "src/ExcalidrawAutomate";
import { ExcalidrawFrameElement } from "@zsviczian/excalidraw/types/element/types";
import { addAppendUpdateCustomData } from "./Utils";
import { mutateElement } from "src/constants";
export const setDynamicStyle = (
ea: ExcalidrawAutomate,
@@ -55,57 +60,59 @@ export const setDynamicStyle = (
const cmBlack = () => ea.getCM("#000000").lightnessTo(bgLightness);
const gray1 = isGray
const gray1 = () => isGray
? isDark ? cmBlack().lighterBy(10) : cmBlack().darkerBy(10)
: isDark ? cmBG().lighterBy(10).mix({color:cmBlack(),ratio:0.5}) : cmBG().darkerBy(10).mix({color:cmBlack(),ratio:0.5});
const gray2 = isGray
const gray2 = () => isGray
? isDark ? cmBlack().lighterBy(4) : cmBlack().darkerBy(4)
: isDark ? cmBG().lighterBy(4).mix({color:cmBlack(),ratio:0.5}) : cmBG().darkerBy(4).mix({color:cmBlack(),ratio:0.5});
const text = cmBG().mix({color:isDark?lighter:darker, ratio:mixRatio});
const str = (cm: ColorMaster) => cm.stringHEX({alpha:false});
const styleObject:{[x: string]: string;} = {
[`--color-primary`]: str(accent()),
[`--color-surface-low`]: str(gray1),
[`--color-surface-mid`]: str(gray1),
[`--color-surface-lowest`]: str(gray2),
[`--color-surface-high`]: str(gray1.lighterBy(step)),
[`--color-surface-low`]: str(gray1()),
[`--color-surface-mid`]: str(gray1()),
[`--color-surface-lowest`]: str(gray2()),
[`--color-surface-high`]: str(gray1().lighterBy(step)),
[`--color-on-primary-container`]: str(!isDark?accent().darkerBy(15):accent().lighterBy(15)),
[`--color-surface-primary-container`]: str(isDark?accent().darkerBy(step):accent().lighterBy(step)),
//[`--color-primary-darker`]: str(accent().darkerBy(step)),
//[`--color-primary-darkest`]: str(accent().darkerBy(step)),
[`--button-gray-1`]: str(gray1),
[`--button-gray-2`]: str(gray2),
[`--input-border-color`]: str(gray1),
[`--input-bg-color`]: str(gray2),
[`--button-gray-1`]: str(gray1()),
[`--button-gray-2`]: str(gray2()),
[`--input-border-color`]: str(gray1()),
[`--input-bg-color`]: str(gray2()),
[`--input-label-color`]: str(text),
[`--island-bg-color`]: gray2.alphaTo(0.93).stringHEX(),
[`--popup-secondary-bg-color`]: gray2.alphaTo(0.93).stringHEX(),
[`--island-bg-color`]: gray2().alphaTo(0.93).stringHEX(),
[`--popup-secondary-bg-color`]: gray2().alphaTo(0.93).stringHEX(),
[`--icon-fill-color`]: str(text),
[`--text-primary-color`]: str(text),
[`--overlay-bg-color`]: gray2.alphaTo(0.6).stringHEX(),
[`--popup-bg-color`]: str(gray1),
[`--overlay-bg-color`]: gray2().alphaTo(0.6).stringHEX(),
[`--popup-bg-color`]: str(gray1()),
[`--color-on-surface`]: str(text),
//[`--color-gray-100`]: str(text),
[`--color-gray-40`]: str(text), //frame
[`--color-gray-50`]: str(text), //frame
[`--color-surface-highlight`]: str(gray1),
[`--color-surface-highlight`]: str(gray1()),
//[`--color-gray-30`]: str(gray1),
[`--color-gray-80`]: str(isDark?text.lighterBy(15):text.darkerBy(15)), //frame
[`--sidebar-border-color`]: str(gray1),
[`--sidebar-border-color`]: str(gray1()),
[`--color-primary-light`]: str(accent().lighterBy(step)),
[`--button-hover-bg`]: str(gray1),
[`--sidebar-bg-color`]: gray2.alphaTo(0.93).stringHEX(),
[`--sidebar-shadow`]: str(gray1),
[`--button-hover-bg`]: str(gray1()),
[`--sidebar-bg-color`]: gray2().alphaTo(0.93).stringHEX(),
[`--sidebar-shadow`]: str(gray1()),
[`--popup-text-color`]: str(text),
[`--code-normal`]: str(text),
[`--code-background`]: str(gray2),
[`--code-background`]: str(gray2()),
[`--h1-color`]: str(text),
[`--h2-color`]: str(text),
[`--h3-color`]: str(text),
[`--h4-color`]: str(text),
[`color`]: str(text),
[`--select-highlight-color`]: str(gray1),
[`--select-highlight-color`]: str(gray1()),
};
const styleString = Object.keys(styleObject)
@@ -117,13 +124,36 @@ export const setDynamicStyle = (
styleString
)*/
setTimeout(()=>view.updateScene({appState:{
frameColor: {
stroke: isDark?str(gray2.lighterBy(15)):str(gray2.darkerBy(15)),
fill: str((isDark?gray2.lighterBy(30):gray2.darkerBy(30)).alphaTo(0.2)),
},
dynamicStyle: styleObject
}}));
setTimeout(()=>{
const api = view.excalidrawAPI as ExcalidrawImperativeAPI;
if(!api) return;
const frameColor = {
stroke: str(isDark?gray2().lighterBy(15):gray2().darkerBy(15)),
fill: str((isDark?gray2().lighterBy(30):gray2().darkerBy(30)).alphaTo(0.2)),
nameColor: str(isDark?gray2().lighterBy(40):gray2().darkerBy(40)),
}
const scene = api.getSceneElements();
scene.filter(el=>el.type==="frame").forEach((e:ExcalidrawFrameElement)=>{
const f = cloneElement(e);
addAppendUpdateCustomData(f,{frameColor});
if(
e.customData && e.customData.frameColor &&
e.customData.frameColor.stroke === frameColor.stroke &&
e.customData.frameColor.fill === frameColor.fill &&
e.customData.frameColor.nameColor === frameColor.nameColor
) {
return;
}
mutateElement(e,{customData: f.customData});
});
view.updateScene({
appState:{
frameColor,
dynamicStyle: styleObject
}
});
});
const toolspanel = view.toolsPanelRef?.current?.containerRef?.current;
if(toolspanel) {
let toolsStyle = toolspanel.getAttribute("style");

View File

@@ -313,4 +313,9 @@ export const readLocalFileBinary = async (filePath:string): Promise<ArrayBuffer>
}
});
});
}
export const getPathWithoutExtension = (f:TFile): string => {
if(!f) return null;
return f.path.substring(0, f.path.lastIndexOf("."));
}

View File

@@ -270,27 +270,42 @@ class ImageCache {
return !!this.db && !this.isInitializing && !!this.plugin && this.plugin.settings.allowImageCache;
}
private fullyInitialized = false;
public async getImageFromCache(key_: ImageKey): Promise<string | SVGSVGElement | undefined> {
if (!this.isReady()) {
return null; // Database not initialized yet
}
const key = getKey(key_);
const cachedData = await this.getCacheData(key);
const file = this.app.vault.getAbstractFileByPath(key_.filepath.split("#")[0]);
if (!file || !(file instanceof TFile)) return undefined;
if (cachedData && cachedData.mtime === file.stat.mtime) {
if(cachedData.svg) {
return convertSVGStringToElement(cachedData.svg);
try {
const cachedData = this.fullyInitialized
? await this.getCacheData(key)
: await Promise.race([
this.getCacheData(key),
new Promise<undefined>((_,reject) => setTimeout(() => reject(undefined), 100))
]);
this.fullyInitialized = true;
if(!cachedData) return undefined;
const file = this.app.vault.getAbstractFileByPath(key_.filepath.split("#")[0]);
if (!file || !(file instanceof TFile)) return undefined;
if (cachedData && cachedData.mtime === file.stat.mtime) {
if(cachedData.svg) {
return convertSVGStringToElement(cachedData.svg);
}
if(this.obsidanURLCache.has(key)) {
return this.obsidanURLCache.get(key);
}
const obsidianURL = URL.createObjectURL(cachedData.blob);
this.obsidanURLCache.set(key, obsidianURL);
return obsidianURL;
}
if(this.obsidanURLCache.has(key)) {
return this.obsidanURLCache.get(key);
}
const obsidianURL = URL.createObjectURL(cachedData.blob);
this.obsidanURLCache.set(key, obsidianURL);
return obsidianURL;
return undefined;
} catch(e) {
return undefined;
}
return undefined;
}
public async getBAKFromCache(filepath: string): Promise<BackupData | null> {

View File

@@ -1,6 +1,6 @@
import {
App,
normalizePath, Workspace, WorkspaceLeaf, WorkspaceSplit
normalizePath, parseFrontMatterEntry, TFile, Workspace, WorkspaceLeaf, WorkspaceSplit
} from "obsidian";
import ExcalidrawPlugin from "../main";
import { checkAndCreateFolder, splitFolderAndFilename } from "./FileUtils";
@@ -233,3 +233,24 @@ export const obsidianPDFQuoteWithRef = (text:string):{quote: string, link: strin
}
return null;
}
export const extractSVGPNGFileName = (text:string) => {
const regex = /\[\[([^\]|#^]+\.(?:svg|png))(?:[^\]]+)?\]\]|\[[^\]]+\]\(([^\)]+\.(?:svg|png))\)/;
const match = text.match(regex);
return match ? (match[1] || match[2]) : null;
}
export const getFileCSSClasses = (
file: TFile,
): string[] => {
if (file) {
const plugin = window.ExcalidrawAutomate.plugin;
const fileCache = plugin.app.metadataCache.getFileCache(file);
if(!fileCache?.frontmatter) return [];
const x = parseFrontMatterEntry(fileCache.frontmatter, "cssclasses");
if (Array.isArray(x)) return x
if (typeof x === "string") return Array.from(new Set(x.split(/[, ]+/).filter(Boolean)));
return [];
}
return [];
}

View File

@@ -10,8 +10,11 @@ import {
import { Random } from "roughjs/bin/math";
import { BinaryFileData, DataURL} from "@zsviczian/excalidraw/types/types";
import {
ASSISTANT_FONT,
CASCADIA_FONT,
VIRGIL_FONT,
} from "src/constFonts";
import {
FRONTMATTER_KEY_EXPORT_DARK,
FRONTMATTER_KEY_EXPORT_TRANSPARENT,
FRONTMATTER_KEY_EXPORT_SVGPADDING,
@@ -30,7 +33,7 @@ import { generateEmbeddableLink } from "./CustomEmbeddableUtils";
import ExcalidrawScene from "src/svgToExcalidraw/elements/ExcalidrawScene";
import { FILENAMEPARTS } from "./UtilTypes";
import { Mutable } from "@zsviczian/excalidraw/types/utility-types";
import { cleanBlockRef, cleanSectionHeading } from "./ObsidianUtils";
import { cleanBlockRef, cleanSectionHeading, getFileCSSClasses } from "./ObsidianUtils";
import { updateElementLinksToObsidianLinks } from "src/ExcalidrawAutomate";
@@ -287,9 +290,15 @@ export const getSVG = async (
},
files: scene.files,
exportPadding: padding,
exportingFrame: null,
renderEmbeddables: true,
});
if(svg) {
svg.addClass("excalidraw-svg");
if(srcFile instanceof TFile) {
const cssClasses = getFileCSSClasses(srcFile);
cssClasses.forEach((cssClass) => svg.addClass(cssClass));
}
}
return svg;
} catch (error) {
@@ -371,12 +380,15 @@ export const embedFontsInSVG = (
svg.querySelector("text[font-family^='Virgil']") != null;
const includesCascadia = !localOnly &&
svg.querySelector("text[font-family^='Cascadia']") != null;
const includesAssistant = !localOnly &&
svg.querySelector("text[font-family^='Assistant']") != null;
const includesLocalFont =
svg.querySelector("text[font-family^='LocalFont']") != null;
const defs = svg.querySelector("defs");
if (defs && (includesCascadia || includesVirgil || includesLocalFont)) {
if (defs && (includesCascadia || includesVirgil || includesLocalFont || includesAssistant)) {
defs.innerHTML = `<style>${includesVirgil ? VIRGIL_FONT : ""}${
includesCascadia ? CASCADIA_FONT : ""
includesCascadia ? CASCADIA_FONT : ""}${
includesAssistant ? ASSISTANT_FONT : ""
}${includesLocalFont ? plugin.fourthFontDef : ""}</style>`;
}
return svg;
@@ -608,21 +620,6 @@ export const getExportPadding = (
return plugin.settings.exportPaddingSVG;
};
export const getFileCSSClasses = (
plugin: ExcalidrawPlugin,
file: TFile,
): string[] => {
if (file) {
const fileCache = plugin.app.metadataCache.getFileCache(file);
if(!fileCache?.frontmatter) return [];
const x = parseFrontMatterEntry(fileCache.frontmatter, "cssclasses");
if (Array.isArray(x)) return x
if (typeof x === "string") return Array.from(new Set(x.split(/[, ]+/).filter(Boolean)));
return [];
}
return [];
}
export const getPNGScale = (plugin: ExcalidrawPlugin, file: TFile): number => {
if (file) {
const fileCache = plugin.app.metadataCache.getFileCache(file);
@@ -796,8 +793,8 @@ export const convertSVGStringToElement = (svg: string): SVGSVGElement => {
export const escapeRegExp = (str:string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
export const addIframe = (containerEl: HTMLElement, link:string, startAt?: number) => {
const wrapper = containerEl.createDiv({cls: "excalidraw-videoWrapper settings"})
export const addIframe = (containerEl: HTMLElement, link:string, startAt?: number, style:string = "settings") => {
const wrapper = containerEl.createDiv({cls: `excalidraw-videoWrapper ${style}`})
wrapper.createEl("iframe", {
attr: {
allowfullscreen: true,

View File

@@ -0,0 +1,57 @@
const REG_YOUTUBE = /^(?:http(?:s)?:\/\/)?(?:www\.)?youtu(?:be\.com|\.be)\/(embed\/|watch\?v=|shorts\/|playlist\?list=|embed\/videoseries\?list=)?([a-zA-Z0-9_-]+)(?:\?t=|.*&t=|\?start=|.*&start=)?([a-zA-Z0-9_-]+)?[^\s]*$/;
export const isYouTube = (url: string): boolean => {
return Boolean(
url.match(REG_YOUTUBE)
);
}
export const getYouTubeStartAt = (url: string): string => {
const ytLink = url.match(REG_YOUTUBE);
if (ytLink?.[2]) {
const time = ytLink[3] ? parseInt(ytLink[3]) : 0;
const hours = Math.floor(time / 3600);
const minutes = Math.floor((time - hours * 3600) / 60);
const seconds = time - hours * 3600 - minutes * 60;
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
}
return "00:00:00";
};
export const isValidYouTubeStart = (value: string): boolean => {
if(/^[0-9]+$/.test(value)) return true; // Matches only numbers (seconds)
if(/^[0-9]+:[0-9]+$/.test(value)) return true; // Matches only numbers (minutes and seconds)
if(/^[0-9]+:[0-9]+:[0-9]+$/.test(value)) return true; // Matches only numbers (hours, minutes, and seconds
};
export const updateYouTubeStartTime = (link: string, startTime: string): string => {
const match = link.match(REG_YOUTUBE);
if (match?.[2]) {
const startTimeParam = `t=${timeStringToSeconds(startTime)}`;
let updatedLink = link;
if (match[3]) {
// If start time already exists, update it
updatedLink = link.replace(/([?&])t=[a-zA-Z0-9_-]+/, `$1${startTimeParam}`);
updatedLink = updatedLink.replace(/([?&])start=[a-zA-Z0-9_-]+/, `$1${startTimeParam}`);
} else {
// If no start time exists, add it to the link
updatedLink += (link.includes('?') ? '&' : '?') + startTimeParam;
}
return updatedLink;
}
return link;
};
const timeStringToSeconds = (time: string): number => {
const timeParts = time.split(':').map(Number);
const totalParts = timeParts.length;
if (totalParts === 1) {
return timeParts[0]; // Only seconds provided (ss)
} else if (totalParts === 2) {
return timeParts[0] * 60 + timeParts[1]; // Minutes and seconds provided (mm:ss)
} else if (totalParts === 3) {
return timeParts[0] * 3600 + timeParts[1] * 60 + timeParts[2]; // Hours, minutes, and seconds provided (hh:mm:ss)
}
return 0; // Invalid format, return 0 or handle accordingly
};

View File

@@ -345,7 +345,7 @@ label.color-input-container > input {
padding: 0;
}
.excalidraw-settings input {
.excalidraw-settings input:not([type="color"]) {
min-width: 10em;
}
@@ -371,10 +371,6 @@ div.excalidraw-draginfo {
background: initial;
}
.excalidraw .HelpDialog__key {
background-color: var(--color-gray-80) !important;
}
.excalidraw .embeddable-menu {
width: fit-content;
height: fit-content;
@@ -475,4 +471,21 @@ summary.excalidraw-setting-h4 {
hr.excalidraw-setting-hr {
margin: 1rem 0rem 0rem 0rem;
}
.excalidraw-mdEmbed-hideFilename .mod-header {
display: none;
}
.canvas-node:not(.is-editing):has(.excalidraw-canvas-immersive) {
::-webkit-scrollbar,
::-webkit-scrollbar-horizontal {
display: none;
}
background-color: transparent !important;
}
.canvas-node:not(.is-editing) .canvas-node-container:has(.excalidraw-canvas-immersive) {
border: unset;
box-shadow: unset;
}