mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4830983e2 | ||
|
|
a69fefffdc | ||
|
|
1d0466dae7 | ||
|
|
6e5a853d0f | ||
|
|
0c702ddf7b | ||
|
|
fdbffce1f9 | ||
|
|
2872b4e3ce | ||
|
|
0ba55e51e9 |
@@ -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",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.2",
|
||||
"minAppVersion": "1.1.6",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -127,6 +127,12 @@ const setStyle = ({element,imgAttributes,onCanvas}:{
|
||||
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}:{
|
||||
@@ -406,6 +412,15 @@ const createImgElement = async (
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,75 @@ function RenderObsidianView(
|
||||
return () => {}; //cleanup on unmount
|
||||
}, [linkText, subpath, containerRef]);
|
||||
|
||||
const setColors = (canvasNode: HTMLDivElement, element: NonDeletedExcalidrawElement, mdProps: EmbeddableMDCustomProps, canvasColor: 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
|
||||
? (element.backgroundColor.toLowerCase() === "transparent"
|
||||
? "transparent"
|
||||
: 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
|
||||
? (canvasColor.toLowerCase() === "transparent"
|
||||
? "transparent"
|
||||
: 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
|
||||
? (element.strokeColor.toLowerCase() === "transparent"
|
||||
? "transparent"
|
||||
: 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 +331,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 +347,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 +369,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 +387,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 +403,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 && !mdProps.useObsidianDefaults ? "" : "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>
|
||||
)
|
||||
}
|
||||
175
src/dialogs/EmbeddableMDFileCustomDataSettingsComponent.ts
Normal file
175
src/dialogs/EmbeddableMDFileCustomDataSettingsComponent.ts
Normal 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 opacity 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();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
216
src/dialogs/EmbeddableSettings.ts
Normal file
216
src/dialogs/EmbeddableSettings.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
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";
|
||||
import { isCTRL } from "src/utils/ModifierkeyHelper";
|
||||
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/types";
|
||||
|
||||
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;
|
||||
private onKeyDown: (ev: KeyboardEvent) => void;
|
||||
|
||||
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() {
|
||||
this.containerEl.removeEventListener("keydown",this.onKeyDown);
|
||||
}
|
||||
|
||||
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(`${t("ES_ZOOM_100_RELATIVE_DESC")}<br>Current zoom is <b>${Math.round(this.zoomValue*100)}%</b>`);
|
||||
}
|
||||
|
||||
const zoomSetting = new Setting(this.contentEl)
|
||||
.setName(t("ES_ZOOM"))
|
||||
.setDesc(zoomValue())
|
||||
.addButton(button =>
|
||||
button
|
||||
.setButtonText(t("ES_ZOOM_100"))
|
||||
.onClick(() => {
|
||||
const api = this.view.excalidrawAPI as ExcalidrawImperativeAPI;
|
||||
this.zoomValue = 1/api.getAppState().zoom.value;
|
||||
zoomSetting.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();
|
||||
}
|
||||
|
||||
new Setting(this.contentEl)
|
||||
.addButton(button =>
|
||||
button
|
||||
.setButtonText(t("PROMPT_BUTTON_CANCEL"))
|
||||
.setTooltip("ESC")
|
||||
.onClick(() => {
|
||||
this.close();
|
||||
})
|
||||
)
|
||||
.addButton(button =>
|
||||
button
|
||||
.setButtonText(t("PROMPT_BUTTON_OK"))
|
||||
.setTooltip("CTRL/Opt+Enter")
|
||||
.setCta()
|
||||
.onClick(()=>this.applySettings())
|
||||
)
|
||||
|
||||
|
||||
const onKeyDown = (ev: KeyboardEvent) => {
|
||||
if(isCTRL(ev) && ev.key === "Enter") {
|
||||
this.applySettings();
|
||||
}
|
||||
}
|
||||
|
||||
this.onKeyDown = onKeyDown;
|
||||
this.containerEl.ownerDocument.addEventListener("keydown",onKeyDown);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private async applySettings() {
|
||||
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(this.youtubeStart === "" || 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();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,6 +17,28 @@ I develop this plugin as a hobby, spending my free time doing this. If you find
|
||||
|
||||
<div class="ex-coffee-div"><a href="https://ko-fi.com/zsolt"><img src="https://cdn.ko-fi.com/cdn/kofi3.png?v=3" height=45></a></div>
|
||||
`,
|
||||
"2.0.2":`
|
||||
<div class="excalidraw-videoWrapper"><div>
|
||||
<iframe src="https://www.youtube.com/embed/502swdqvZ2A" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
</div></div>
|
||||
|
||||
## Fixed
|
||||
- Resolved an issue where the Command Palette's "Toggle between Excalidraw and Markdown mode" failed to uncompress the Excalidraw JSON for editing.
|
||||
|
||||
## New
|
||||
- Scaling feature for embedded objects (markdown documents, pdfs, YouTube, etc.): Hold down the SHIFT key while resizing elements to adjust their size.
|
||||
- Expanded support for Canvas Candy. Regardless of Canvas Candy, you can apply CSS classes to embedded markdown documents for transparency, shape adjustments, text orientation, and more.
|
||||
- Added new functionalities to the active embeddable top-left menu:
|
||||
- Document Properties (cog icon)
|
||||
- File renaming
|
||||
- Basic styling options for embedded markdown documents
|
||||
- Setting YouTube start time
|
||||
- Zoom to full screen for PDFs
|
||||
- Improved immersive embedding of Excalidraw into Obsidian Canvas.
|
||||
- Introduced new Command Palette Actions:
|
||||
- Embeddable Properties
|
||||
- Scaling selected embeddable elements to 100% relative to the current canvas zoom.
|
||||
`,
|
||||
"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>
|
||||
|
||||
@@ -10,6 +10,8 @@ import { labelALT, labelCTRL, labelMETA, labelSHIFT } from "src/utils/Modifierke
|
||||
export default {
|
||||
// main.ts
|
||||
PUBLISH_SVG_CHECK: "Obsidian Publish: Find SVG and PNG exports that are out of date",
|
||||
EMBEDDABLE_PROPERTIES: "Embeddable Properties",
|
||||
EMBEDDABLE_RELATIVE_ZOOM: "Scale selected embeddable elements to 100% relative to the current canvas zoom",
|
||||
OPEN_IMAGE_SOURCE: "Open Excalidraw drawing",
|
||||
INSTALL_SCRIPT: "Install the script",
|
||||
UPDATE_SCRIPT: "Update available - Click to install",
|
||||
@@ -324,8 +326,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 " +
|
||||
@@ -364,6 +369,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",
|
||||
@@ -463,10 +473,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.
|
||||
@@ -563,6 +573,29 @@ 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: "Embeddable Element Settings",
|
||||
ES_RENAME: "Rename File",
|
||||
ES_ZOOM: "Embedded Content Scaling",
|
||||
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 Opacity",
|
||||
ES_BORDER_OPACITY: "Border Opacity",
|
||||
ES_EMBEDDABLE_SETTINGS: "Embeddable Markdown Settings",
|
||||
ES_USE_OBSIDIAN_DEFAULTS: "Use Obsidian Defaults",
|
||||
ES_ZOOM_100_RELATIVE_DESC: "The button will adjust the element scale so it will show the content at 100% relative to the current zoom level of your canvas",
|
||||
ES_ZOOM_100: "Relative 100%",
|
||||
|
||||
//Prompts.ts
|
||||
PROMPT_FILE_DOES_NOT_EXIST: "File does not exist. Do you want to create it?",
|
||||
|
||||
71
src/main.ts
71
src/main.ts
@@ -50,7 +50,8 @@ import ExcalidrawView, { TextMode, getTextMode } from "./ExcalidrawView";
|
||||
import {
|
||||
changeThemeOfExcalidrawMD,
|
||||
getMarkdownDrawingSection,
|
||||
ExcalidrawData
|
||||
ExcalidrawData,
|
||||
REGEX_LINK
|
||||
} from "./ExcalidrawData";
|
||||
import {
|
||||
ExcalidrawSettings,
|
||||
@@ -93,7 +94,7 @@ import {
|
||||
} from "./utils/Utils";
|
||||
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,
|
||||
@@ -117,6 +118,11 @@ import { imageCache } from "./utils/ImageCache";
|
||||
import { StylesManager } from "./utils/StylesManager";
|
||||
import { MATHJAX_SOURCE_LZCOMPRESSED } from "./constMathJaxSource";
|
||||
import { PublishOutOfDateFilesDialog } from "./dialogs/PublishOutOfDateFiles";
|
||||
import { EmbeddableSettings } from "./dialogs/EmbeddableSettings";
|
||||
import { processLinkText } from "./utils/CustomEmbeddableUtils";
|
||||
import { getEA } from "src";
|
||||
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/types";
|
||||
import { Mutable } from "@zsviczian/excalidraw/types/utility-types";
|
||||
|
||||
declare const EXCALIDRAW_PACKAGES:string;
|
||||
declare const react:any;
|
||||
@@ -853,6 +859,58 @@ 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;
|
||||
const getFile = (el:ExcalidrawEmbeddableElement):TFile => {
|
||||
const res = REGEX_LINK.getRes(el.link).next();
|
||||
if(!res || (!res.value && res.done)) {
|
||||
return null;
|
||||
}
|
||||
const link = REGEX_LINK.getLink(res);
|
||||
const { file } = processLinkText(link, view);
|
||||
return file;
|
||||
}
|
||||
new EmbeddableSettings(view.plugin,view,getFile(els[0]),els[0]).open();
|
||||
}
|
||||
})
|
||||
|
||||
this.addCommand({
|
||||
id: "excalidraw-embeddables-relative-scale",
|
||||
name: t("EMBEDDABLE_RELATIVE_ZOOM"),
|
||||
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 === 0) {
|
||||
if(checking) return false;
|
||||
new Notice("Select at least one embeddable element and try again");
|
||||
return false;
|
||||
}
|
||||
if(checking) return true;
|
||||
const ea = getEA(view) as ExcalidrawAutomate;
|
||||
const api = ea.getExcalidrawAPI() as ExcalidrawImperativeAPI;
|
||||
ea.copyViewElementsToEAforEditing(els);
|
||||
const scale = 1/api.getAppState().zoom.value;
|
||||
ea.getElements().forEach((el: Mutable<ExcalidrawEmbeddableElement>)=>{
|
||||
el.scale = [scale,scale];
|
||||
})
|
||||
ea.addElementsToView();
|
||||
}
|
||||
})
|
||||
|
||||
this.addCommand({
|
||||
id: "open-image-excalidraw-source",
|
||||
name: t("OPEN_IMAGE_SOURCE"),
|
||||
@@ -1625,10 +1683,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
|
||||
const excalidrawView = this.app.workspace.getActiveViewOfType(ExcalidrawView)
|
||||
if (excalidrawView) {
|
||||
const activeLeaf = excalidrawView.leaf;
|
||||
this.excalidrawFileModes[(activeLeaf as any).id || activeFile.path] =
|
||||
"markdown";
|
||||
this.setMarkdownView(activeLeaf);
|
||||
excalidrawView.openAsMarkdown();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2069,14 +2124,14 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
|
||||
//!Temporary hack
|
||||
//https://discord.com/channels/686053708261228577/817515900349448202/1031101635784613968
|
||||
if (app.isMobile && newActiveviewEV && !previouslyActiveEV) {
|
||||
if (this.app.isMobile && newActiveviewEV && !previouslyActiveEV) {
|
||||
const navbar = document.querySelector("body>.app-container>.mobile-navbar");
|
||||
if(navbar && navbar instanceof HTMLDivElement) {
|
||||
navbar.style.position="relative";
|
||||
}
|
||||
}
|
||||
|
||||
if (app.isMobile && !newActiveviewEV && previouslyActiveEV) {
|
||||
if (this.app.isMobile && !newActiveviewEV && previouslyActiveEV) {
|
||||
const navbar = document.querySelector("body>.app-container>.mobile-navbar");
|
||||
if(navbar && navbar instanceof HTMLDivElement) {
|
||||
navbar.style.position="";
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,79 +118,93 @@ 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")}
|
||||
action={() => {
|
||||
if(!element) return;
|
||||
api.zoomToFit([element], view.plugin.settings.zoomToFitMaxLevel, 0.1);
|
||||
api.zoomToFit([element], 30, 0.1);
|
||||
}}
|
||||
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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
@@ -149,6 +151,8 @@ export interface ExcalidrawSettings {
|
||||
DECAY_LENGTH: number,
|
||||
COLOR: string,
|
||||
};
|
||||
embeddableMarkdownDefaults: EmbeddableMDCustomProps;
|
||||
canvasImmersiveEmbed: boolean,
|
||||
}
|
||||
|
||||
declare const PLUGIN_VERSION:string;
|
||||
@@ -278,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 {
|
||||
@@ -1257,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"),
|
||||
@@ -1500,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",
|
||||
@@ -1527,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")))
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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("."));
|
||||
}
|
||||
62
src/utils/YoutTubeUtils.ts
Normal file
62
src/utils/YoutTubeUtils.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
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;
|
||||
if(hours === 0 && minutes === 0 && seconds === 0) return "";
|
||||
if(hours === 0 && minutes === 0) return `${String(seconds).padStart(2, '0')}`;
|
||||
if(hours === 0) return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
||||
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
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 = startTime === ""
|
||||
? ``
|
||||
: `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
|
||||
};
|
||||
19
styles.css
19
styles.css
@@ -345,7 +345,7 @@ label.color-input-container > input {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.excalidraw-settings input {
|
||||
.excalidraw-settings input:not([type="color"]) {
|
||||
min-width: 10em;
|
||||
}
|
||||
|
||||
@@ -471,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;
|
||||
}
|
||||
Reference in New Issue
Block a user