mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
frame menu and section zoom ready
This commit is contained in:
@@ -18,7 +18,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/lz-string": "^1.3.34",
|
||||
"@zsviczian/excalidraw": "0.15.2-obsidian-5",
|
||||
"@zsviczian/excalidraw": "0.15.2-obsidian-6",
|
||||
"chroma-js": "^2.4.2",
|
||||
"clsx": "^1.2.1",
|
||||
"colormaster": "^1.2.1",
|
||||
@@ -31,7 +31,8 @@
|
||||
"roughjs": "^4.5.2",
|
||||
"html2canvas": "^1.4.1",
|
||||
"@popperjs/core": "^2.11.6",
|
||||
"nanoid": "^4.0.0"
|
||||
"nanoid": "^4.0.0",
|
||||
"lucide-react": "0.259.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.12",
|
||||
|
||||
@@ -23,6 +23,12 @@ import {
|
||||
COLOR_NAMES,
|
||||
fileid,
|
||||
GITHUB_RELEASES,
|
||||
determineFocusDistance,
|
||||
getCommonBoundingBox,
|
||||
getDefaultLineHeight,
|
||||
getMaximumGroups,
|
||||
intersectElementWithLine,
|
||||
measureText,
|
||||
} from "./Constants";
|
||||
import { getDrawingFilename, getNewUniqueFilepath, } from "./utils/FileUtils";
|
||||
import {
|
||||
@@ -42,7 +48,6 @@ import { getAttachmentsFolderAndFilePath, getNewOrAdjacentLeaf, isObsidianThemeD
|
||||
import { AppState, BinaryFileData, DataURL, ExcalidrawImperativeAPI, Point } from "@zsviczian/excalidraw/types/types";
|
||||
import { EmbeddedFile, EmbeddedFilesLoader, FileData } from "./EmbeddedFileLoader";
|
||||
import { tex2dataURL } from "./LaTeX";
|
||||
//import Excalidraw from "@zsviczian/excalidraw";
|
||||
import { Prompt } from "./dialogs/Prompt";
|
||||
import { t } from "./lang/helpers";
|
||||
import { ScriptEngine } from "./Scripts";
|
||||
@@ -83,16 +88,6 @@ extendPlugins([
|
||||
|
||||
declare const PLUGIN_VERSION:string;
|
||||
|
||||
const {
|
||||
determineFocusDistance,
|
||||
intersectElementWithLine,
|
||||
getCommonBoundingBox,
|
||||
getMaximumGroups,
|
||||
measureText,
|
||||
getDefaultLineHeight,
|
||||
//@ts-ignore
|
||||
} = excalidrawLib;
|
||||
|
||||
const GAP = 4;
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -20,6 +20,10 @@ import {
|
||||
FRONTMATTER_KEY_IFRAME_THEME,
|
||||
DEVICE,
|
||||
IFRAME_THEME_FRONTMATTER_VALUES,
|
||||
getBoundTextMaxWidth,
|
||||
getDefaultLineHeight,
|
||||
getFontString,
|
||||
wrapText,
|
||||
} from "./Constants";
|
||||
import { _measureText } from "./ExcalidrawAutomate";
|
||||
import ExcalidrawPlugin from "./main";
|
||||
@@ -58,14 +62,6 @@ declare module "obsidian" {
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
wrapText,
|
||||
getFontString,
|
||||
getBoundTextMaxWidth,
|
||||
getDefaultLineHeight,
|
||||
//@ts-ignore
|
||||
} = excalidrawLib;
|
||||
|
||||
export enum AutoexportPreference {
|
||||
none,
|
||||
both,
|
||||
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
DEVICE,
|
||||
GITHUB_RELEASES,
|
||||
EXPORT_IMG_ICON_NAME,
|
||||
viewportCoordsToSceneCoords,
|
||||
} from "./Constants";
|
||||
import ExcalidrawPlugin from "./main";
|
||||
import {
|
||||
@@ -87,7 +88,6 @@ import {
|
||||
hasExportTheme,
|
||||
scaleLoadedImage,
|
||||
svgToBase64,
|
||||
viewportCoordsToSceneCoords,
|
||||
updateFrontmatterInString,
|
||||
hyperlinkIsImage,
|
||||
hyperlinkIsYouTubeLink,
|
||||
@@ -117,9 +117,12 @@ import { getEA } from "src";
|
||||
import { emulateCTRLClickForLinks, externalDragModifierType, internalDragModifierType, isALT, isCTRL, isMETA, isSHIFT, linkClickModifierType, mdPropModifier, ModifierKeys } from "./utils/ModifierkeyHelper";
|
||||
import { setDynamicStyle } from "./utils/DynamicStyling";
|
||||
import { InsertPDFModal } from "./dialogs/InsertPDFModal";
|
||||
import { CustomIFrame, renderWebView, useDefaultExcalidrawFrame } from "./customIFrame";
|
||||
import { CustomIFrame, renderWebView } from "./customIFrame";
|
||||
import { insertIFrameToView, insertImageToView } from "./utils/ExcalidrawViewUtils";
|
||||
import { imageCache } from "./utils/ImageCache";
|
||||
import { CanvasNodeFactory } from "./utils/CanvasNodeFactory";
|
||||
import { IFrameMenu } from "./menu/IFrameActionsMenu";
|
||||
import { useDefaultExcalidrawFrame } from "./utils/CustomIFrameUtils";
|
||||
|
||||
declare const PLUGIN_VERSION:string;
|
||||
|
||||
@@ -232,6 +235,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
public excalidrawAPI: any = null;
|
||||
public excalidrawWrapperRef: React.MutableRefObject<any> = null;
|
||||
public toolsPanelRef: React.MutableRefObject<any> = null;
|
||||
public iframeMenuRef: React.MutableRefObject<any> = null;
|
||||
private parentMoveObserver: MutationObserver;
|
||||
public linksAlwaysOpenInANewPane: boolean = false; //override the need for SHIFT+CTRL+click (used by ExcaliBrain)
|
||||
private hookServer: ExcalidrawAutomate;
|
||||
@@ -246,6 +250,8 @@ export default class ExcalidrawView extends TextFileView {
|
||||
public ownerWindow: Window;
|
||||
public ownerDocument: Document;
|
||||
private draginfoDiv: HTMLDivElement;
|
||||
public canvasNodeFactory: CanvasNodeFactory;
|
||||
private iFrameRefs = new Map<ExcalidrawElement["id"], HTMLIFrameElement | HTMLWebViewElement>();
|
||||
|
||||
public semaphores: {
|
||||
popoutUnload: boolean; //the unloaded Excalidraw view was the last leaf in the popout window
|
||||
@@ -305,6 +311,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
private linkAction_Element: HTMLElement;
|
||||
public compatibilityMode: boolean = false;
|
||||
private obsidianMenu: ObsidianMenu;
|
||||
private iframeMenu: IFrameMenu;
|
||||
|
||||
//https://stackoverflow.com/questions/27132796/is-there-any-javascript-event-fired-when-the-on-screen-keyboard-on-mobile-safari
|
||||
private isEditingTextResetTimer: NodeJS.Timeout = null;
|
||||
@@ -316,6 +323,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
this.plugin = plugin;
|
||||
this.excalidrawData = new ExcalidrawData(plugin);
|
||||
this.hookServer = plugin.ea;
|
||||
this.canvasNodeFactory = new CanvasNodeFactory(this);
|
||||
}
|
||||
|
||||
setHookServer(ea:ExcalidrawAutomate) {
|
||||
@@ -1211,6 +1219,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
|
||||
const self = this;
|
||||
app.workspace.onLayoutReady(async () => {
|
||||
this.canvasNodeFactory.initialize();
|
||||
self.contentEl.addClass("excalidraw-view");
|
||||
//https://github.com/zsviczian/excalibrain/issues/28
|
||||
await self.addSlidingPanesListner(); //awaiting this because when using workspaces, onLayoutReady comes too early
|
||||
@@ -1622,6 +1631,9 @@ export default class ExcalidrawView extends TextFileView {
|
||||
|
||||
// clear the view content
|
||||
clear() {
|
||||
this.canvasNodeFactory.purgeNodes();
|
||||
this.iFrameRefs.clear();
|
||||
|
||||
delete this.exportDialog;
|
||||
const api = this.excalidrawAPI;
|
||||
if (!this.excalidrawRef || !api) {
|
||||
@@ -2296,6 +2308,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
const reactElement = React.createElement(() => {
|
||||
const excalidrawWrapperRef = React.useRef(null);
|
||||
const toolsPanelRef = React.useRef(null);
|
||||
const iframeMenuRef = React.useRef(null);
|
||||
|
||||
const [dimensions, setDimensions] = React.useState({
|
||||
width: undefined,
|
||||
@@ -2310,8 +2323,10 @@ export default class ExcalidrawView extends TextFileView {
|
||||
let blockOnMouseButtonDown = false;
|
||||
|
||||
this.toolsPanelRef = toolsPanelRef;
|
||||
this.iframeMenuRef = iframeMenuRef;
|
||||
this.obsidianMenu = new ObsidianMenu(this.plugin, toolsPanelRef, this);
|
||||
|
||||
this.iframeMenu = new IFrameMenu(this, iframeMenuRef);
|
||||
|
||||
//excalidrawRef readypromise based on
|
||||
//https://codesandbox.io/s/eexcalidraw-resolvable-promise-d0qg3?file=/src/App.js:167-760
|
||||
const resolvablePromise = () => {
|
||||
@@ -3222,7 +3237,8 @@ export default class ExcalidrawView extends TextFileView {
|
||||
await this.plugin.saveSettings();
|
||||
})();
|
||||
},
|
||||
renderTopRightUI: this.obsidianMenu.renderButton,
|
||||
renderTopRightUI: (isMobile: boolean, appState: AppState) => this.obsidianMenu.renderButton (isMobile, appState),
|
||||
renderIFrameMenu: (appState: AppState) => this.iframeMenu.renderButtons(appState),
|
||||
onPaste: (data: ClipboardData) => {
|
||||
//, event: ClipboardEvent | null
|
||||
/*if(data && data.text && hyperlinkIsYouTubeLink(data.text)) {
|
||||
@@ -3783,7 +3799,8 @@ export default class ExcalidrawView extends TextFileView {
|
||||
|
||||
}
|
||||
},
|
||||
iframeURLWhitelist: [true],
|
||||
validateIFrame: true,
|
||||
renderWebview: DEVICE.isDesktop,
|
||||
renderCustomIFrame: (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
radius: number,
|
||||
@@ -3793,10 +3810,6 @@ export default class ExcalidrawView extends TextFileView {
|
||||
if(!this.file || !element || !element.link || element.link.length === 0 || useDefaultExcalidrawFrame(element)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if(element.link.match(REG_LINKINDEX_HYPERLINK)) {
|
||||
return renderWebView(element.link, radius);
|
||||
}
|
||||
|
||||
const res = REGEX_LINK.getRes(element.link).next();
|
||||
if(!res || (!res.value && res.done)) {
|
||||
@@ -3807,7 +3820,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
|
||||
if(linkText.match(REG_LINKINDEX_HYPERLINK)) {
|
||||
if(DEVICE.isDesktop) {
|
||||
return renderWebView(linkText, radius);
|
||||
return renderWebView(linkText, radius, this, element.id);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
@@ -4331,6 +4344,16 @@ export default class ExcalidrawView extends TextFileView {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public updateIFrameRef(id: string, ref: HTMLIFrameElement | HTMLWebViewElement | null) {
|
||||
if (ref) {
|
||||
this.iFrameRefs.set(id, ref);
|
||||
}
|
||||
}
|
||||
|
||||
public getIFrameElementById(id: string): HTMLIFrameElement | HTMLWebViewElement | undefined {
|
||||
return this.iFrameRefs.get(id);
|
||||
}
|
||||
}
|
||||
|
||||
export function getTextMode(data: string): TextMode {
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
import { customAlphabet } from "nanoid";
|
||||
//This is only for backward compatibility because an early version of obsidian included an encoding to avoid fantom links from littering Obsidian graph view
|
||||
declare const PLUGIN_VERSION:string;
|
||||
|
||||
export const {
|
||||
sceneCoordsToViewportCoords,
|
||||
viewportCoordsToSceneCoords,
|
||||
determineFocusDistance,
|
||||
intersectElementWithLine,
|
||||
getCommonBoundingBox,
|
||||
getMaximumGroups,
|
||||
measureText,
|
||||
getDefaultLineHeight,
|
||||
wrapText,
|
||||
getFontString,
|
||||
getBoundTextMaxWidth,
|
||||
exportToSvg,
|
||||
exportToBlob,
|
||||
//@ts-ignore
|
||||
} = excalidrawLib;
|
||||
|
||||
export function JSON_parse(x: string): any {
|
||||
return JSON.parse(x.replaceAll("[", "["));
|
||||
}
|
||||
@@ -27,6 +45,17 @@ export const DEVICE: {
|
||||
isAndroid: document.body.hasClass("is-android")
|
||||
};
|
||||
|
||||
export const ROOTELEMENTSIZE = (() => {
|
||||
const tempElement = document.createElement('div');
|
||||
tempElement.style.fontSize = '1rem';
|
||||
tempElement.style.display = 'none'; // Hide the element
|
||||
document.body.appendChild(tempElement);
|
||||
const computedStyle = getComputedStyle(tempElement);
|
||||
const pixelSize = parseFloat(computedStyle.fontSize);
|
||||
document.body.removeChild(tempElement);
|
||||
return pixelSize;
|
||||
})();
|
||||
|
||||
export const nanoid = customAlphabet(
|
||||
"1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||
8,
|
||||
@@ -98,6 +127,45 @@ export const TEXT_DISPLAY_RAW_ICON_NAME = "presentation";
|
||||
export const FULLSCREEN_ICON_NAME = "fullscreen";
|
||||
export const EXIT_FULLSCREEN_ICON_NAME = "exit-fullscreen";
|
||||
export const SCRIPTENGINE_ICON_NAME = "ScriptEngine";
|
||||
|
||||
export const KEYBOARD_EVENT_TYPES = [
|
||||
"keydown",
|
||||
"keyup",
|
||||
"keypress"
|
||||
];
|
||||
|
||||
export const EXTENDED_EVENT_TYPES = [
|
||||
/* "pointerdown",
|
||||
"pointerup",
|
||||
"pointermove",
|
||||
"mousedown",
|
||||
"mouseup",
|
||||
"mousemove",
|
||||
"mouseover",
|
||||
"mouseout",
|
||||
"mouseenter",
|
||||
"mouseleave",
|
||||
"dblclick",
|
||||
"drag",
|
||||
"dragend",
|
||||
"dragenter",
|
||||
"dragexit",
|
||||
"dragleave",
|
||||
"dragover",
|
||||
"dragstart",
|
||||
"drop",*/
|
||||
"copy",
|
||||
"cut",
|
||||
"paste",
|
||||
/*"wheel",
|
||||
"touchstart",
|
||||
"touchend",
|
||||
"touchmove",*/
|
||||
];
|
||||
|
||||
export const TWITTER_REG = /^(?:http(?:s)?:\/\/)?(?:(?:w){3}.)?twitter.com/;
|
||||
|
||||
|
||||
export const COLOR_NAMES = new Map<string, string>();
|
||||
COLOR_NAMES.set("aliceblue", "#f0f8ff");
|
||||
COLOR_NAMES.set("antiquewhite", "#faebd7");
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { NonDeletedExcalidrawElement } from "@zsviczian/excalidraw/types/element/types";
|
||||
import ExcalidrawView from "./ExcalidrawView";
|
||||
import { Notice, Workspace, WorkspaceLeaf, WorkspaceSplit } from "obsidian";
|
||||
import { Notice, WorkspaceLeaf, WorkspaceSplit } from "obsidian";
|
||||
import * as React from "react";
|
||||
import { ConstructableWorkspaceSplit, getContainerForDocument, getParentOfClass, isObsidianThemeDark } from "./utils/ObsidianUtils";
|
||||
import { getLinkParts } from "./utils/Utils";
|
||||
import { DEVICE, REG_LINKINDEX_INVALIDCHARS } from "./Constants";
|
||||
import { ConstructableWorkspaceSplit, getContainerForDocument, isObsidianThemeDark } from "./utils/ObsidianUtils";
|
||||
import { DEVICE, EXTENDED_EVENT_TYPES, KEYBOARD_EVENT_TYPES, TWITTER_REG } from "./Constants";
|
||||
import { ExcalidrawImperativeAPI, UIAppState } from "@zsviczian/excalidraw/types/types";
|
||||
import { ObsidianCanvasNode } from "./utils/CanvasNodeFactory";
|
||||
import { processLinkText, patchMobileView } from "./utils/CustomIFrameUtils";
|
||||
|
||||
declare module "obsidian" {
|
||||
interface Workspace {
|
||||
@@ -17,68 +18,39 @@ declare module "obsidian" {
|
||||
}
|
||||
}
|
||||
|
||||
const KEYBOARD_EVENT_TYPES = [
|
||||
"keydown",
|
||||
"keyup",
|
||||
"keypress"
|
||||
];
|
||||
|
||||
const EXTENDED_EVENT_TYPES = [
|
||||
/* "pointerdown",
|
||||
"pointerup",
|
||||
"pointermove",
|
||||
"mousedown",
|
||||
"mouseup",
|
||||
"mousemove",
|
||||
"mouseover",
|
||||
"mouseout",
|
||||
"mouseenter",
|
||||
"mouseleave",
|
||||
"dblclick",
|
||||
"drag",
|
||||
"dragend",
|
||||
"dragenter",
|
||||
"dragexit",
|
||||
"dragleave",
|
||||
"dragover",
|
||||
"dragstart",
|
||||
"drop",*/
|
||||
"copy",
|
||||
"cut",
|
||||
"paste",
|
||||
/*"wheel",
|
||||
"touchstart",
|
||||
"touchend",
|
||||
"touchmove",*/
|
||||
];
|
||||
|
||||
const YOUTUBE_REG =
|
||||
/^(?:http(?:s)?:\/\/)?(?:(?:w){3}.)?youtu(?:be|.be)?(?:\.com)?\/(?:embed\/|watch\?v=|shorts\/)?([a-zA-Z0-9_-]+)(?:\?t=|&t=)?([a-zA-Z0-9_-]+)?[^\s]*$/;
|
||||
const VIMEO_REG =
|
||||
/^(?:http(?:s)?:\/\/)?(?:(?:w){3}.)?(?:player\.)?vimeo\.com\/(?:video\/)?([^?\s]+)(?:\?.*)?$/;
|
||||
const TWITTER_REG = /^(?:http(?:s)?:\/\/)?(?:(?:w){3}.)?twitter.com/;
|
||||
|
||||
export const useDefaultExcalidrawFrame = (element: NonDeletedExcalidrawElement) => {
|
||||
return element.link.match(YOUTUBE_REG) || element.link.match(VIMEO_REG);
|
||||
}
|
||||
|
||||
const leafMap = new Map<string, WorkspaceLeaf>();
|
||||
|
||||
export const renderWebView = (src: string, radius: number):JSX.Element =>{
|
||||
if(DEVICE.isIOS || DEVICE.isAndroid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------
|
||||
//Render webview for anything other than Vimeo and Youtube
|
||||
//Vimeo and Youtube are rendered by Excalidraw because of the window messaging
|
||||
//required to control the video
|
||||
//--------------------------------------------------------------------------------
|
||||
export const renderWebView = (src: string, radius: number, view: ExcalidrawView, id: string):JSX.Element =>{
|
||||
const twitterLink = src.match(TWITTER_REG);
|
||||
if (twitterLink) {
|
||||
src = `https://twitframe.com/show?url=${encodeURIComponent(src)}`;
|
||||
}
|
||||
|
||||
if(DEVICE.isDesktop) {
|
||||
return (
|
||||
<webview
|
||||
ref={(ref) => view.updateIFrameRef(id, ref)}
|
||||
className="excalidraw__iframe"
|
||||
title="Excalidraw Embedded Content"
|
||||
allowFullScreen={true}
|
||||
src={src}
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
borderRadius: `${radius}px`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<webview
|
||||
<iframe
|
||||
ref={(ref) => view.updateIFrameRef(id, ref)}
|
||||
className="excalidraw__iframe"
|
||||
title="Excalidraw Embedded Content"
|
||||
allowFullScreen={true}
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
src={src}
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
@@ -88,45 +60,21 @@ export const renderWebView = (src: string, radius: number):JSX.Element =>{
|
||||
);
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------
|
||||
//Render WorkspaceLeaf or CanvasNode
|
||||
//--------------------------------------------------------------------------------
|
||||
function RenderObsidianView(
|
||||
{ element, linkText, radius, view, containerRef, appState }:{
|
||||
{ element, linkText, radius, view, containerRef, appState, theme }:{
|
||||
element: NonDeletedExcalidrawElement;
|
||||
linkText: string;
|
||||
radius: number;
|
||||
view: ExcalidrawView;
|
||||
containerRef: React.RefObject<HTMLDivElement>;
|
||||
appState: UIAppState;
|
||||
theme: string;
|
||||
}): JSX.Element {
|
||||
|
||||
//This is definitely not the right solution, feels like sticking plaster
|
||||
//patch disappearing content on mobile
|
||||
const patchMobileView = () => {
|
||||
if(DEVICE.isDesktop) return;
|
||||
console.log("patching mobile view");
|
||||
const parent = getParentOfClass(view.containerEl,"mod-top");
|
||||
if(parent) {
|
||||
if(!parent.hasClass("mod-visible")) {
|
||||
parent.addClass("mod-visible");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let subpath:string = null;
|
||||
|
||||
if (linkText.search("#") > -1) {
|
||||
const linkParts = getLinkParts(linkText, view.file);
|
||||
subpath = `#${linkParts.isBlockRef ? "^" : ""}${linkParts.ref}`;
|
||||
linkText = linkParts.path;
|
||||
}
|
||||
|
||||
if (linkText.match(REG_LINKINDEX_INVALIDCHARS)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const file = app.metadataCache.getFirstLinkpathDest(
|
||||
linkText,
|
||||
view.file.path,
|
||||
);
|
||||
const { subpath, file } = processLinkText(linkText, view);
|
||||
|
||||
if (!file) {
|
||||
return null;
|
||||
@@ -134,95 +82,21 @@ function RenderObsidianView(
|
||||
const react = view.plugin.getPackage(view.ownerWindow).react;
|
||||
|
||||
//@ts-ignore
|
||||
const leafRef = react.useRef<WorkspaceLeaf | null>(null);
|
||||
const leafRef = react.useRef<{leaf: WorkspaceLeaf; node?: ObsidianCanvasNode} | null>(null);
|
||||
const isEditingRef = react.useRef(false);
|
||||
const isActiveRef = react.useRef(false);
|
||||
|
||||
|
||||
//--------------------------------------------------------------------------------
|
||||
//block propagation of events to the parent if the iframe element is active
|
||||
//--------------------------------------------------------------------------------
|
||||
const stopPropagation = react.useCallback((event:React.PointerEvent<HTMLElement>) => {
|
||||
if(isActiveRef.current) {
|
||||
event.stopPropagation(); // Stop the event from propagating up the DOM tree
|
||||
}
|
||||
}, [isActiveRef.current]);
|
||||
|
||||
react.useEffect(() => {
|
||||
EXTENDED_EVENT_TYPES.forEach((type) => containerRef.current.removeEventListener(type, stopPropagation));
|
||||
if(!containerRef?.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(isActiveRef.current) {
|
||||
EXTENDED_EVENT_TYPES.forEach((type) => containerRef.current.addEventListener(type, stopPropagation));
|
||||
}
|
||||
|
||||
return () => {
|
||||
if(!containerRef?.current) {
|
||||
return;
|
||||
}
|
||||
EXTENDED_EVENT_TYPES.forEach((type) => containerRef.current.removeEventListener(type, stopPropagation));
|
||||
}; //cleanup on unmount
|
||||
}, [isActiveRef.current, containerRef.current]);
|
||||
|
||||
react.useEffect(() => {
|
||||
if(!containerRef?.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
while(containerRef.current.hasChildNodes()) {
|
||||
containerRef.current.removeChild(containerRef.current.lastChild);
|
||||
}
|
||||
|
||||
const doc = view.ownerDocument;
|
||||
const rootSplit:WorkspaceSplit = new (WorkspaceSplit as ConstructableWorkspaceSplit)(app.workspace, "vertical");
|
||||
rootSplit.getRoot = () => app.workspace[doc === document ? 'rootSplit' : 'floatingSplit'];
|
||||
rootSplit.getContainer = () => getContainerForDocument(doc);
|
||||
containerRef.current.appendChild(rootSplit.containerEl);
|
||||
rootSplit.containerEl.style.width = '100%';
|
||||
rootSplit.containerEl.style.height = '100%';
|
||||
rootSplit.containerEl.style.borderRadius = `${radius}px`;
|
||||
leafRef.current = app.workspace.createLeafInParent(rootSplit, 0);
|
||||
const workspaceLeaf:HTMLDivElement = rootSplit.containerEl.querySelector("div.workspace-leaf");
|
||||
if(workspaceLeaf) workspaceLeaf.style.borderRadius = `${radius}px`;
|
||||
(async () => {
|
||||
await leafRef.current.openFile(file, subpath ? { eState: { subpath }, state: {mode:"preview"} } : undefined);
|
||||
if (leafRef.current.view?.getViewType() === "canvas") {
|
||||
leafRef.current.view.canvas?.setReadonly(true);
|
||||
}
|
||||
patchMobileView();
|
||||
})();
|
||||
return () => {}; //cleanup on unmount
|
||||
}, [linkText, subpath]);
|
||||
|
||||
const handleClick = react.useCallback((event: React.PointerEvent<HTMLElement>) => {
|
||||
if(isActiveRef.current) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
if (isActiveRef.current && !isEditingRef.current) {
|
||||
if (!leafRef.current?.view || leafRef.current.view.getViewType() !== 'markdown') {
|
||||
return;
|
||||
}
|
||||
|
||||
const api:ExcalidrawImperativeAPI = view.excalidrawAPI;
|
||||
const el = api.getSceneElements().filter(el=>el.id === element.id)[0];
|
||||
|
||||
if(!el || el.angle !== 0) {
|
||||
new Notice("Sorry, cannot edit rotated markdown documents");
|
||||
return;
|
||||
}
|
||||
//@ts-ignore
|
||||
const modes = leafRef.current.view.modes;
|
||||
if (!modes) {
|
||||
return;
|
||||
}
|
||||
leafRef.current.view.setMode(modes['source']);
|
||||
//@ts-ignore
|
||||
window.al = leafRef.current;
|
||||
app.workspace.setActiveLeaf(leafRef.current);
|
||||
isEditingRef.current = true;
|
||||
patchMobileView();
|
||||
}
|
||||
}, [leafRef.current, isActiveRef.current, element]);
|
||||
|
||||
//runs once after mounting of the component and when the component is unmounted
|
||||
react.useEffect(() => {
|
||||
if(!containerRef?.current) {
|
||||
return;
|
||||
@@ -241,32 +115,146 @@ function RenderObsidianView(
|
||||
}; //cleanup on unmount
|
||||
}, []);
|
||||
|
||||
//blocking or not the propagation of events to the parent if the iframe is active
|
||||
react.useEffect(() => {
|
||||
EXTENDED_EVENT_TYPES.forEach((type) => containerRef.current.removeEventListener(type, stopPropagation));
|
||||
if(!containerRef?.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(isActiveRef.current) {
|
||||
EXTENDED_EVENT_TYPES.forEach((type) => containerRef.current.addEventListener(type, stopPropagation));
|
||||
}
|
||||
|
||||
return () => {
|
||||
if(!containerRef?.current) {
|
||||
return;
|
||||
}
|
||||
EXTENDED_EVENT_TYPES.forEach((type) => containerRef.current.removeEventListener(type, stopPropagation));
|
||||
}; //cleanup on unmount
|
||||
}, [isActiveRef.current, containerRef.current]);
|
||||
|
||||
|
||||
//--------------------------------------------------------------------------------
|
||||
//mount the workspace leaf or the canvas node depending on subpath
|
||||
//--------------------------------------------------------------------------------
|
||||
react.useEffect(() => {
|
||||
if(!containerRef?.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!leafRef.current?.view || leafRef.current.view.getViewType() !== "markdown") {
|
||||
return;
|
||||
while(containerRef.current.hasChildNodes()) {
|
||||
containerRef.current.removeChild(containerRef.current.lastChild);
|
||||
}
|
||||
|
||||
//@ts-ignore
|
||||
const modes = leafRef.current.view.modes;
|
||||
if(!modes) {
|
||||
return;
|
||||
}
|
||||
|
||||
isActiveRef.current = (appState.activeIFrame?.element.id === element.id) && (appState.activeIFrame?.state === "active");
|
||||
|
||||
if(!isActiveRef.current) {
|
||||
//@ts-ignore
|
||||
leafRef.current.view.setMode(modes["preview"]);
|
||||
if(isEditingRef.current) {
|
||||
if(leafRef.current?.node) {
|
||||
view.canvasNodeFactory.stopEditing(leafRef.current.node);
|
||||
}
|
||||
isEditingRef.current = false;
|
||||
app.workspace.setActiveLeaf(view.leaf);
|
||||
}
|
||||
|
||||
const doc = view.ownerDocument;
|
||||
const rootSplit:WorkspaceSplit = new (WorkspaceSplit as ConstructableWorkspaceSplit)(app.workspace, "vertical");
|
||||
rootSplit.getRoot = () => app.workspace[doc === document ? 'rootSplit' : 'floatingSplit'];
|
||||
rootSplit.getContainer = () => getContainerForDocument(doc);
|
||||
rootSplit.containerEl.style.width = '100%';
|
||||
rootSplit.containerEl.style.height = '100%';
|
||||
rootSplit.containerEl.style.borderRadius = `${radius}px`;
|
||||
leafRef.current = {
|
||||
leaf: app.workspace.createLeafInParent(rootSplit, 0),
|
||||
node: null
|
||||
};
|
||||
|
||||
//if subpath is defined, create a canvas node else create a workspace leaf
|
||||
if(subpath && view.canvasNodeFactory.isInitialized()) {
|
||||
leafRef.current.node = view.canvasNodeFactory.createFileNote(file, subpath, containerRef.current, element.id);
|
||||
} else {
|
||||
containerRef.current.appendChild(rootSplit.containerEl);
|
||||
const workspaceLeaf:HTMLDivElement = rootSplit.containerEl.querySelector("div.workspace-leaf");
|
||||
if(workspaceLeaf) workspaceLeaf.style.borderRadius = `${radius}px`;
|
||||
(async () => {
|
||||
await leafRef.current.leaf.openFile(file, subpath ? { eState: { subpath }, state: {mode:"preview"} } : undefined);
|
||||
if (leafRef.current.leaf.view?.getViewType() === "canvas") {
|
||||
leafRef.current.leaf.view.canvas?.setReadonly(true);
|
||||
}
|
||||
patchMobileView(view);
|
||||
})();
|
||||
}
|
||||
return () => {}; //cleanup on unmount
|
||||
}, [linkText, subpath, view, containerRef, app, radius, isEditingRef, leafRef]);
|
||||
|
||||
|
||||
//--------------------------------------------------------------------------------
|
||||
//Switch to edit mode when markdown view is clicked
|
||||
//--------------------------------------------------------------------------------
|
||||
const handleClick = react.useCallback((event: React.PointerEvent<HTMLElement>) => {
|
||||
if(isActiveRef.current) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
if (isActiveRef.current && !isEditingRef.current && leafRef.current?.leaf) {
|
||||
if(leafRef.current.leaf.view?.getViewType() === "markdown") {
|
||||
const api:ExcalidrawImperativeAPI = view.excalidrawAPI;
|
||||
const el = api.getSceneElements().filter(el=>el.id === element.id)[0];
|
||||
|
||||
if(!el || el.angle !== 0) {
|
||||
new Notice("Sorry, cannot edit rotated markdown documents");
|
||||
return;
|
||||
}
|
||||
//@ts-ignore
|
||||
const modes = leafRef.current.leaf.view.modes;
|
||||
if (!modes) {
|
||||
return;
|
||||
}
|
||||
leafRef.current.leaf.view.setMode(modes['source']);
|
||||
//@ts-ignore
|
||||
window.al = leafRef.current.leaf;
|
||||
app.workspace.setActiveLeaf(leafRef.current.leaf);
|
||||
isEditingRef.current = true;
|
||||
patchMobileView(view);
|
||||
} else if (leafRef.current?.node) {
|
||||
//Handle canvas node
|
||||
view.canvasNodeFactory.startEditing(leafRef.current.node, theme);
|
||||
}
|
||||
}
|
||||
}, [leafRef.current?.leaf, element]);
|
||||
|
||||
//--------------------------------------------------------------------------------
|
||||
// Set isActiveRef and switch to preview mode when the iframe is not active
|
||||
//--------------------------------------------------------------------------------
|
||||
react.useEffect(() => {
|
||||
if(!containerRef?.current || !leafRef?.current) {
|
||||
return;
|
||||
}
|
||||
}, [appState.activeIFrame?.element, appState.activeIFrame?.state, element.id]);
|
||||
}
|
||||
|
||||
const previousIsActive = isActiveRef.current;
|
||||
isActiveRef.current = (appState.activeIFrame?.element.id === element.id) && (appState.activeIFrame?.state === "active");
|
||||
|
||||
if (previousIsActive === isActiveRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(leafRef.current.leaf?.view?.getViewType() === "markdown") {
|
||||
//Handle markdown leaf
|
||||
//@ts-ignore
|
||||
const modes = leafRef.current.leaf.view.modes;
|
||||
if(!modes) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!isActiveRef.current) {
|
||||
//@ts-ignore
|
||||
leafRef.current.leaf.view.setMode(modes["preview"]);
|
||||
isEditingRef.current = false;
|
||||
app.workspace.setActiveLeaf(view.leaf);
|
||||
return;
|
||||
}
|
||||
} else if (leafRef.current?.node) {
|
||||
//Handle canvas node
|
||||
view.canvasNodeFactory.stopEditing(leafRef.current.node);
|
||||
}
|
||||
}, [containerRef, leafRef, isActiveRef, appState, element, view, linkText, subpath, file, theme, isEditingRef, view.canvasNodeFactory]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -299,7 +287,8 @@ export const CustomIFrame: React.FC<{element: NonDeletedExcalidrawElement; radiu
|
||||
radius={radius}
|
||||
view={view}
|
||||
containerRef={containerRef}
|
||||
appState={appState}/>
|
||||
appState={appState}
|
||||
theme={theme}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -3,19 +3,14 @@ import ExcalidrawView from "../ExcalidrawView";
|
||||
import ExcalidrawPlugin from "../main";
|
||||
import { Modal, Setting, TextComponent } from "obsidian";
|
||||
import { FileSuggestionModal } from "./FolderSuggester";
|
||||
import { IMAGE_TYPES, REG_BLOCK_REF_CLEAN } from "src/Constants";
|
||||
import { IMAGE_TYPES, REG_BLOCK_REF_CLEAN, sceneCoordsToViewportCoords, viewportCoordsToSceneCoords } from "src/Constants";
|
||||
import { insertIFrameToView, insertImageToView } from "src/utils/ExcalidrawViewUtils";
|
||||
import { getEA } from "src";
|
||||
import { InsertPDFModal } from "./InsertPDFModal";
|
||||
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/types";
|
||||
import { MAX_IMAGE_SIZE } from "src/Constants";
|
||||
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
|
||||
|
||||
const {
|
||||
viewportCoordsToSceneCoords,
|
||||
sceneCoordsToViewportCoords
|
||||
//@ts-ignore
|
||||
} = excalidrawLib;
|
||||
import { ExcalidrawIFrameElement } from "@zsviczian/excalidraw/types/element/types";
|
||||
|
||||
export class UniversalInsertFileModal extends Modal {
|
||||
private center: { x: number, y: number } = { x: 0, y: 0 };
|
||||
|
||||
@@ -512,5 +512,13 @@ FILENAME_HEAD: "Filename",
|
||||
TOGGLE_FULLSCREEN: "Toggle fullscreen mode",
|
||||
TOGGLE_DISABLEBINDING: "Toggle to invert default binding behavior",
|
||||
OPEN_LINK_CLICK: "Navigate to selected element link",
|
||||
OPEN_LINK_PROPS: "Open markdown-embed properties or open link in new window"
|
||||
OPEN_LINK_PROPS: "Open markdown-embed properties or open link in new window",
|
||||
|
||||
//IFrameActionsMenu.tsx
|
||||
NARROW_TO_HEADING: "Narrow to heading...",
|
||||
NARROW_TO_BLOCK: "Narrow to block...",
|
||||
SHOW_ENTIRE_FILE: "Show entire file",
|
||||
ZOOM_TO_FIT: "Zoom to fit",
|
||||
RELOAD: "Reload original link",
|
||||
OPEN_IN_BROWSER: "Open current link in browser",
|
||||
};
|
||||
|
||||
@@ -2131,7 +2131,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
const imgFile = this.app.vault.getAbstractFileByPath(imageFullpath);
|
||||
if (!imgFile) {
|
||||
await this.app.vault.create(imageFullpath, "");
|
||||
await sleep(200);
|
||||
await sleep(200); //wait for metadata cache to update
|
||||
}
|
||||
|
||||
editor.replaceSelection(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ArrowBigLeft, Globe, Minimize2, RotateCcw, Scan } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { PenStyle } from "src/PenTypes";
|
||||
|
||||
@@ -25,6 +26,35 @@ export const ICONS = {
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
Reload: (<RotateCcw />),
|
||||
Globe: (<Globe />),
|
||||
ZoomToSelectedElement: (<Scan />),
|
||||
ZoomToSection: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="var(--icon-fill-color)"
|
||||
strokeWidth="1"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<text x="6" y="18" fontSize="22px">#</text>
|
||||
</svg>
|
||||
),
|
||||
ZoomToBlock: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="var(--icon-fill-color)"
|
||||
strokeWidth="1"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<text x="1" y="18" fontSize="22px">#^</text>
|
||||
</svg>
|
||||
),
|
||||
Discord: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
229
src/menu/IFrameActionsMenu.tsx
Normal file
229
src/menu/IFrameActionsMenu.tsx
Normal file
@@ -0,0 +1,229 @@
|
||||
import { TFile } from "obsidian";
|
||||
import * as React from "react";
|
||||
import ExcalidrawView from "../ExcalidrawView";
|
||||
import { ExcalidrawIFrameElement } from "@zsviczian/excalidraw/types/element/types";
|
||||
import { AppState, ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/types";
|
||||
import clsx from "clsx";
|
||||
import { ActionButton } from "./ActionButton";
|
||||
import { ICONS } from "./ActionIcons";
|
||||
import { t } from "src/lang/helpers";
|
||||
import { ScriptEngine } from "src/Scripts";
|
||||
import { REG_BLOCK_REF_CLEAN, ROOTELEMENTSIZE, nanoid, sceneCoordsToViewportCoords } from "src/Constants";
|
||||
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
|
||||
import { getEA } from "src";
|
||||
import { REGEX_LINK, REG_LINKINDEX_HYPERLINK } from "src/ExcalidrawData";
|
||||
import { errorlog } from "src/utils/Utils";
|
||||
import { processLinkText, useDefaultExcalidrawFrame } from "src/utils/CustomIFrameUtils";
|
||||
|
||||
export class IFrameMenu {
|
||||
|
||||
constructor(
|
||||
private view:ExcalidrawView,
|
||||
private containerRef: React.RefObject<HTMLDivElement>,
|
||||
) {
|
||||
}
|
||||
|
||||
private updateElement = (subpath: string, element: ExcalidrawIFrameElement, file: TFile) => {
|
||||
if(!element) return;
|
||||
const view = this.view;
|
||||
const path = app.metadataCache.fileToLinktext(
|
||||
file,
|
||||
view.file.path,
|
||||
file.extension === "md",
|
||||
)
|
||||
const ea:ExcalidrawAutomate = getEA(this.view);
|
||||
ea.copyViewElementsToEAforEditing([element]);
|
||||
(ea.getElement(element.id) as any).link = `[[${path}${subpath}]]`;
|
||||
view.setDirty(99);
|
||||
view.updateScene({appState: {activeIFrame: null}});
|
||||
ea.addElementsToView(false,true);
|
||||
}
|
||||
|
||||
renderButtons(appState: AppState) {
|
||||
const view = this.view;
|
||||
const api = view?.excalidrawAPI as ExcalidrawImperativeAPI;
|
||||
if(!api) return null;
|
||||
if(!appState.activeIFrame || appState.activeIFrame.state !== "active") return null;
|
||||
const element = appState.activeIFrame?.element as ExcalidrawIFrameElement;
|
||||
let link = element.link;
|
||||
|
||||
const isExcalidrawiFrame = useDefaultExcalidrawFrame(element);
|
||||
let isObsidianiFrame = element.link?.match(REG_LINKINDEX_HYPERLINK);
|
||||
|
||||
if(!isExcalidrawiFrame && !isObsidianiFrame) {
|
||||
const res = REGEX_LINK.getRes(element.link).next();
|
||||
if(!res || (!res.value && res.done)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
link = REGEX_LINK.getLink(res);
|
||||
|
||||
isObsidianiFrame = link.match(REG_LINKINDEX_HYPERLINK);
|
||||
|
||||
if(!isObsidianiFrame) {
|
||||
const { subpath, file } = processLinkText(link, view);
|
||||
if(!file || file.extension!=="md") return null;
|
||||
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`;
|
||||
const isDark = appState?.theme === "dark";
|
||||
return (
|
||||
<div
|
||||
ref={this.containerRef}
|
||||
className="iframe-menu"
|
||||
style={{
|
||||
top,
|
||||
left,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="Island"
|
||||
style={{
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<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) => `#${b.display.replaceAll(REG_BLOCK_REF_CLEAN, "").trim()}`)
|
||||
);
|
||||
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;
|
||||
|
||||
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);
|
||||
}}
|
||||
icon={ICONS.ZoomToSelectedElement}
|
||||
view={view}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
if(isObsidianiFrame || isExcalidrawiFrame) {
|
||||
const iframe = isExcalidrawiFrame
|
||||
? api.getIFrameElementById(element.id)
|
||||
: view.getIFrameElementById(element.id);
|
||||
if(!iframe || !iframe.contentWindow) return null;
|
||||
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`;
|
||||
const isDark = appState?.theme === "dark";
|
||||
return (
|
||||
<div
|
||||
ref={this.containerRef}
|
||||
className={clsx("excalidraw", {
|
||||
"theme--dark": isDark,
|
||||
})}
|
||||
style={{
|
||||
top,
|
||||
left,
|
||||
width: "fit-content",
|
||||
height: "100%",
|
||||
position: "fixed",
|
||||
display: "block",
|
||||
zIndex: 10000,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="Island"
|
||||
style={{
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{(iframe.src !== link) && !iframe.src.startsWith("https://www.youtube.com") && !iframe.src.startsWith("https://player.vimeo.com") && (
|
||||
<ActionButton
|
||||
key={"Reload"}
|
||||
title={t("RELOAD")}
|
||||
action={()=>{
|
||||
iframe.src = link;
|
||||
}}
|
||||
icon={ICONS.Reload}
|
||||
view={view}
|
||||
/>
|
||||
)}
|
||||
<ActionButton
|
||||
key={"Open"}
|
||||
title={t("OPEN_IN_BROWSER")}
|
||||
action={() => {
|
||||
view.openExternalLink(iframe.src);
|
||||
}}
|
||||
icon={ICONS.Globe}
|
||||
view={view}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"ZoomToElement"}
|
||||
title={t("ZOOM_TO_FIT")}
|
||||
action={() => {
|
||||
if(!element) return;
|
||||
api.zoomToFit([element], view.plugin.settings.zoomToFitMaxLevel, 0.1);
|
||||
}}
|
||||
icon={ICONS.ZoomToSelectedElement}
|
||||
view={view}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,7 @@ export class ObsidianMenu {
|
||||
constructor(
|
||||
private plugin: ExcalidrawPlugin,
|
||||
private toolsRef: React.MutableRefObject<any>,
|
||||
private view: ExcalidrawView
|
||||
private view: ExcalidrawView,
|
||||
) {
|
||||
this.clickTimestamp = Array.from({length: Object.keys(PENS).length}, () => 0);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ container.appendChild(node.contentEl)
|
||||
|
||||
import { TFile, WorkspaceLeaf, WorkspaceSplit } from "obsidian";
|
||||
import ExcalidrawView from "src/ExcalidrawView";
|
||||
import { getContainerForDocument, ConstructableWorkspaceSplit } from "./ObsidianUtils";
|
||||
import { getContainerForDocument, ConstructableWorkspaceSplit, isObsidianThemeDark } from "./ObsidianUtils";
|
||||
|
||||
declare module "obsidian" {
|
||||
interface Workspace {
|
||||
@@ -27,7 +27,7 @@ interface ObsidianCanvas {
|
||||
removeNode: Function;
|
||||
}
|
||||
|
||||
interface ObsidianCanvasNode {
|
||||
export interface ObsidianCanvasNode {
|
||||
startEditing: Function;
|
||||
child: any;
|
||||
}
|
||||
@@ -36,6 +36,8 @@ export class CanvasNodeFactory {
|
||||
leaf: WorkspaceLeaf;
|
||||
canvas: ObsidianCanvas;
|
||||
nodes = new Map<string, ObsidianCanvasNode>();
|
||||
initialized: boolean = false;
|
||||
public isInitialized = () => this.initialized;
|
||||
|
||||
constructor(
|
||||
private view: ExcalidrawView,
|
||||
@@ -54,31 +56,65 @@ export class CanvasNodeFactory {
|
||||
rootSplit.getRoot = () => app.workspace[doc === document ? 'rootSplit' : 'floatingSplit'];
|
||||
rootSplit.getContainer = () => getContainerForDocument(doc);
|
||||
this.leaf = app.workspace.createLeafInParent(rootSplit, 0);
|
||||
this.canvas = canvasPlugin.views.canvas(this.leaf);
|
||||
this.canvas = canvasPlugin.views.canvas(this.leaf).canvas;
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
public createFileNote(file: TFile, subpath: string, containerEl: HTMLDivElement, elementId: string) {
|
||||
public createFileNote(file: TFile, subpath: string, containerEl: HTMLDivElement, elementId: string): ObsidianCanvasNode {
|
||||
if(!this.initialized) return;
|
||||
if(this.nodes.has(elementId)) {
|
||||
this.canvas.removeNode(this.nodes.get(elementId));
|
||||
this.nodes.delete(elementId);
|
||||
}
|
||||
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)
|
||||
this.nodes.set(elementId, node);
|
||||
return node;
|
||||
}
|
||||
|
||||
public startEditing(elementId: string, theme: string) {
|
||||
const node = this.nodes.get(elementId);
|
||||
if(!node) return;
|
||||
public startEditing(node: ObsidianCanvasNode, theme: string) {
|
||||
if (!this.initialized || !node) return;
|
||||
node.startEditing();
|
||||
node.child.editor.containerEl.parentElement.parentElement.removeClass("theme-light");
|
||||
node.child.editor.containerEl.parentElement.parentElement.removeClass("theme-dark");
|
||||
node.child.editor.containerEl.parentElement.parentElement.addClass(theme);
|
||||
}
|
||||
|
||||
const obsidianTheme = isObsidianThemeDark() ? "theme-dark" : "theme-light";
|
||||
if (obsidianTheme === theme) return;
|
||||
|
||||
(async () => {
|
||||
let counter = 0;
|
||||
while (!node.child.editor?.containerEl?.parentElement?.parentElement && counter++ < 100) {
|
||||
await sleep(25);
|
||||
}
|
||||
if (!node.child.editor?.containerEl?.parentElement?.parentElement) return;
|
||||
node.child.editor.containerEl.parentElement.parentElement.classList.remove(obsidianTheme);
|
||||
node.child.editor.containerEl.parentElement.parentElement.classList.add(theme);
|
||||
|
||||
const observer = new MutationObserver((mutationsList) => {
|
||||
for (const mutation of mutationsList) {
|
||||
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
|
||||
const targetElement = mutation.target as HTMLElement;
|
||||
if (targetElement.classList.contains(obsidianTheme)) {
|
||||
targetElement.classList.remove(obsidianTheme);
|
||||
targetElement.classList.add(theme);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(node.child.editor.containerEl.parentElement.parentElement, { attributes: true });
|
||||
})();
|
||||
}
|
||||
|
||||
public stopEditing(elementId: string) {
|
||||
public stopEditing(node: ObsidianCanvasNode) {
|
||||
if(!this.initialized || !node) return;
|
||||
if(!node.child.editMode) return;
|
||||
node.child.showPreview();
|
||||
}
|
||||
|
||||
public purgeNodes() {
|
||||
if(!this.initialized) return;
|
||||
this.nodes.forEach(node => {
|
||||
this.canvas.removeNode(node);
|
||||
});
|
||||
|
||||
46
src/utils/CustomIFrameUtils.ts
Normal file
46
src/utils/CustomIFrameUtils.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { NonDeletedExcalidrawElement } from "@zsviczian/excalidraw/types/element/types";
|
||||
import { DEVICE, REG_LINKINDEX_INVALIDCHARS, TWITTER_REG } from "src/Constants";
|
||||
import { getParentOfClass } from "./ObsidianUtils";
|
||||
import { TFile, WorkspaceLeaf } from "obsidian";
|
||||
import { getLinkParts } from "./Utils";
|
||||
import ExcalidrawView from "src/ExcalidrawView";
|
||||
|
||||
export const useDefaultExcalidrawFrame = (element: NonDeletedExcalidrawElement) => {
|
||||
return !element.link.startsWith("[") && !element.link.match(TWITTER_REG);
|
||||
}
|
||||
|
||||
export const leafMap = new Map<string, WorkspaceLeaf>();
|
||||
|
||||
//This is definitely not the right solution, feels like sticking plaster
|
||||
//patch disappearing content on mobile
|
||||
export const patchMobileView = (view: ExcalidrawView) => {
|
||||
if(DEVICE.isDesktop) return;
|
||||
console.log("patching mobile view");
|
||||
const parent = getParentOfClass(view.containerEl,"mod-top");
|
||||
if(parent) {
|
||||
if(!parent.hasClass("mod-visible")) {
|
||||
parent.addClass("mod-visible");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const processLinkText = (linkText: string, view:ExcalidrawView): { subpath:string, file:TFile } => {
|
||||
let subpath:string = null;
|
||||
|
||||
if (linkText.search("#") > -1) {
|
||||
const linkParts = getLinkParts(linkText, view.file);
|
||||
subpath = `#${linkParts.isBlockRef ? "^" : ""}${linkParts.ref}`;
|
||||
linkText = linkParts.path;
|
||||
}
|
||||
|
||||
if (linkText.match(REG_LINKINDEX_INVALIDCHARS)) {
|
||||
return {subpath, file: null};
|
||||
}
|
||||
|
||||
const file = app.metadataCache.getFirstLinkpathDest(
|
||||
linkText,
|
||||
view.file.path,
|
||||
);
|
||||
|
||||
return { subpath, file };
|
||||
}
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
FRONTMATTER_KEY_EXPORT_SVGPADDING,
|
||||
FRONTMATTER_KEY_EXPORT_PNGSCALE,
|
||||
FRONTMATTER_KEY_EXPORT_PADDING,
|
||||
exportToSvg,
|
||||
exportToBlob,
|
||||
} from "../Constants";
|
||||
import ExcalidrawPlugin from "../main";
|
||||
import { ExcalidrawElement } from "@zsviczian/excalidraw/types/element/types";
|
||||
@@ -28,12 +30,6 @@ import { IMAGE_TYPES } from "../Constants";
|
||||
|
||||
declare const PLUGIN_VERSION:string;
|
||||
|
||||
const {
|
||||
exportToSvg,
|
||||
exportToBlob,
|
||||
//@ts-ignore
|
||||
} = excalidrawLib;
|
||||
|
||||
declare module "obsidian" {
|
||||
interface Workspace {
|
||||
getAdjacentLeafInDirection(
|
||||
@@ -180,29 +176,6 @@ export const rotatedDimensions = (
|
||||
];
|
||||
};
|
||||
|
||||
export const viewportCoordsToSceneCoords = (
|
||||
{ clientX, clientY }: { clientX: number; clientY: number },
|
||||
{
|
||||
zoom,
|
||||
offsetLeft,
|
||||
offsetTop,
|
||||
scrollX,
|
||||
scrollY,
|
||||
}: {
|
||||
zoom: Zoom;
|
||||
offsetLeft: number;
|
||||
offsetTop: number;
|
||||
scrollX: number;
|
||||
scrollY: number;
|
||||
},
|
||||
) => {
|
||||
const invScale = 1 / zoom.value;
|
||||
const x = (clientX - offsetLeft) * invScale - scrollX;
|
||||
const y = (clientY - offsetTop) * invScale - scrollY;
|
||||
|
||||
return { x, y };
|
||||
};
|
||||
|
||||
export const getDataURL = async (
|
||||
file: ArrayBuffer,
|
||||
mimeType: string,
|
||||
|
||||
@@ -356,4 +356,12 @@ div.excalidraw-draginfo {
|
||||
|
||||
.excalidraw .HelpDialog__key {
|
||||
background-color: var(--color-gray-80) !important;
|
||||
}
|
||||
|
||||
.excalidraw .iframe-menu {
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
position: absolute;
|
||||
display: block;
|
||||
z-index: var(--zIndex-layerUI);
|
||||
}
|
||||
15
yarn.lock
15
yarn.lock
@@ -2272,10 +2272,10 @@
|
||||
dependencies:
|
||||
"@zerollup/ts-helpers" "^1.7.18"
|
||||
|
||||
"@zsviczian/excalidraw@0.15.2-obsidian-4":
|
||||
"integrity" "sha512-piFX8c6PXPZ1N5DdWZFaxQNfkVvaofizy2cPKFChHx5PDjhtlD8FXzQ7zztluYjFvCF5RpJ2OfJWaNKQ1vn+7A=="
|
||||
"resolved" "https://registry.npmjs.org/@zsviczian/excalidraw/-/excalidraw-0.15.2-obsidian-4.tgz"
|
||||
"version" "0.15.2-obsidian-4"
|
||||
"@zsviczian/excalidraw@0.15.2-obsidian-6":
|
||||
"integrity" "sha512-+Wz1yHkKEh5+OUmAfXtT9NNVpMoLxODt2H3lhIKz+kdtdITg3D65Z/sCBIKArjJKwIIgd0KeO327N4DkEAovWw=="
|
||||
"resolved" "https://registry.npmjs.org/@zsviczian/excalidraw/-/excalidraw-0.15.2-obsidian-6.tgz"
|
||||
"version" "0.15.2-obsidian-6"
|
||||
|
||||
"abab@^2.0.3", "abab@^2.0.5":
|
||||
"integrity" "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q=="
|
||||
@@ -6172,6 +6172,11 @@
|
||||
dependencies:
|
||||
"yallist" "^4.0.0"
|
||||
|
||||
"lucide-react@0.259.0":
|
||||
"integrity" "sha512-dFBLc6jRDfcpD9NQ7NyFVa+YR3RHX6+bs+f/UiotvNPho+kd4WyeXWMCCchUf7i/pq3BAaHkbmtkbx/GxxHVUw=="
|
||||
"resolved" "https://registry.npmjs.org/lucide-react/-/lucide-react-0.259.0.tgz"
|
||||
"version" "0.259.0"
|
||||
|
||||
"lz-string@^1.4.4":
|
||||
"integrity" "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY="
|
||||
"resolved" "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz"
|
||||
@@ -7613,7 +7618,7 @@
|
||||
optionalDependencies:
|
||||
"fsevents" "^2.3.2"
|
||||
|
||||
"react@^17.0.2 || ^18.2.0", "react@^18.2.0", "react@>= 16":
|
||||
"react@^16.5.1 || ^17.0.0 || ^18.0.0", "react@^17.0.2 || ^18.2.0", "react@^18.2.0", "react@>= 16":
|
||||
"integrity" "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ=="
|
||||
"resolved" "https://registry.npmjs.org/react/-/react-18.2.0.tgz"
|
||||
"version" "18.2.0"
|
||||
|
||||
Reference in New Issue
Block a user