mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
customIframes v0.1
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "1.8.15-beta",
|
||||
"minAppVersion": "0.16.0",
|
||||
"version": "1.9.4-beta",
|
||||
"minAppVersion": "1.1.6",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
"authorUrl": "https://zsolt.blog",
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
MarkdownView,
|
||||
request,
|
||||
requireApiVersion,
|
||||
WorkspaceSplit,
|
||||
} from "obsidian";
|
||||
//import * as React from "react";
|
||||
//import * as ReactDOM from "react-dom";
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
BinaryFileData,
|
||||
ExcalidrawImperativeAPI,
|
||||
LibraryItems,
|
||||
UIAppState,
|
||||
} from "@zsviczian/excalidraw/types/types";
|
||||
import {
|
||||
VIEW_TYPE_EXCALIDRAW,
|
||||
@@ -111,9 +113,20 @@ import { emulateCTRLClickForLinks, externalDragModifierType, internalDragModifie
|
||||
import { setDynamicStyle } from "./utils/DynamicStyling";
|
||||
import { MenuLinks } from "./menu/MenuLinks";
|
||||
import { InsertPDFModal } from "./dialogs/InsertPDFModal";
|
||||
import { CustomIFrame, renderWebView, useDefaultExcalidrawFrame } from "./customIFrame";
|
||||
|
||||
declare const PLUGIN_VERSION:string;
|
||||
|
||||
declare module "obsidian" {
|
||||
interface Workspace {
|
||||
floatingSplit: any;
|
||||
}
|
||||
|
||||
interface WorkspaceSplit {
|
||||
containerEl: HTMLDivElement;
|
||||
}
|
||||
}
|
||||
|
||||
type SelectedElementWithLink = { id: string; text: string };
|
||||
type SelectedImage = { id: string; fileId: FileId };
|
||||
|
||||
@@ -3610,7 +3623,35 @@ export default class ExcalidrawView extends TextFileView {
|
||||
|
||||
}
|
||||
},
|
||||
},//,React.createElement(Footer,{},React.createElement(customTextEditor.render)),
|
||||
iframeURLWhitelist: [/.*/],
|
||||
renderCustomIFrame: (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
radius: number,
|
||||
appState: UIAppState,
|
||||
) => {
|
||||
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)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let linkText = REGEX_LINK.getLink(res);
|
||||
|
||||
if(linkText.match(REG_LINKINDEX_HYPERLINK)) {
|
||||
return renderWebView(linkText, radius);
|
||||
}
|
||||
|
||||
return React.createElement(CustomIFrame, {element,radius,view:this, appState, linkText});
|
||||
}
|
||||
|
||||
},//,React.createElement(Footer,{},React.createElement(customTextEditor.render)),
|
||||
React.createElement (
|
||||
MainMenu,
|
||||
{},
|
||||
|
||||
225
src/customIFrame.tsx
Normal file
225
src/customIFrame.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
import { NonDeletedExcalidrawElement } from "@zsviczian/excalidraw/types/element/types";
|
||||
import ExcalidrawView from "./ExcalidrawView";
|
||||
import { Notice, Workspace, WorkspaceLeaf, WorkspaceSplit } from "obsidian";
|
||||
import * as React from "react";
|
||||
import { isObsidianThemeDark } from "./utils/ObsidianUtils";
|
||||
import { REGEX_LINK, REG_LINKINDEX_HYPERLINK } from "./ExcalidrawData";
|
||||
import { getLinkParts } from "./utils/Utils";
|
||||
import { DEVICE, REG_LINKINDEX_INVALIDCHARS } from "./Constants";
|
||||
import { UIAppState } from "@zsviczian/excalidraw/types/types";
|
||||
|
||||
declare module "obsidian" {
|
||||
interface Workspace {
|
||||
floatingSplit: any;
|
||||
}
|
||||
|
||||
interface WorkspaceSplit {
|
||||
containerEl: HTMLDivElement;
|
||||
}
|
||||
}
|
||||
|
||||
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/;
|
||||
|
||||
type ConstructableWorkspaceSplit = new (ws: Workspace, dir: "horizontal"|"vertical") => WorkspaceSplit;
|
||||
|
||||
const getContainerForDocument = (doc:Document) => {
|
||||
if (doc !== document && app.workspace.floatingSplit) {
|
||||
for (const container of app.workspace.floatingSplit.children) {
|
||||
if (container.doc === doc) return container;
|
||||
}
|
||||
}
|
||||
return app.workspace.rootSplit;
|
||||
};
|
||||
|
||||
export const useDefaultExcalidrawFrame = (element: NonDeletedExcalidrawElement) => {
|
||||
return element.link.match(YOUTUBE_REG) || element.link.match(VIMEO_REG) || element.link.match(TWITTER_REG);
|
||||
}
|
||||
|
||||
const leafMap = new Map<string, WorkspaceLeaf>();
|
||||
|
||||
export const renderWebView = (src: string, radius: number):JSX.Element =>{
|
||||
if(DEVICE.isIOS || DEVICE.isAndroid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<webview
|
||||
className="excalidraw__iframe"
|
||||
title="Excalidraw Embedded Content"
|
||||
allowFullScreen={true}
|
||||
src={src}
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
borderRadius: `${radius}px`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function RenderObsidianView(
|
||||
{ element, linkText, radius, view, containerRef, appState }:{
|
||||
element: NonDeletedExcalidrawElement;
|
||||
linkText: string;
|
||||
radius: number;
|
||||
view: ExcalidrawView;
|
||||
containerRef: React.RefObject<HTMLDivElement>;
|
||||
appState: UIAppState;
|
||||
}): JSX.Element {
|
||||
|
||||
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,
|
||||
);
|
||||
|
||||
if (!file) {
|
||||
return null;
|
||||
}
|
||||
const react = view.plugin.getPackage(view.ownerWindow).react;
|
||||
|
||||
//@ts-ignore
|
||||
const leafRef = react.useRef<WorkspaceLeaf | null>(null);
|
||||
const isEditingRef = react.useRef(false);
|
||||
const isActiveRef = react.useRef(false);
|
||||
|
||||
|
||||
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);
|
||||
//leafMap.set(element.id, leaf);
|
||||
const workspaceLeaf:HTMLDivElement = rootSplit.containerEl.querySelector("div.workspace-leaf");
|
||||
if(workspaceLeaf) workspaceLeaf.style.borderRadius = `${radius}px`;
|
||||
leafRef.current.openFile(file, subpath ? { eState: { subpath }, state: {mode:"preview"} } : undefined);
|
||||
|
||||
return () => {}; //cleanup on unmount
|
||||
}, [linkText, subpath]);
|
||||
|
||||
const handleClick = react.useCallback(() => {
|
||||
if (isActiveRef.current && !isEditingRef.current) {
|
||||
if (!leafRef.current?.view || leafRef.current.view.getViewType() !== 'markdown') {
|
||||
return;
|
||||
}
|
||||
if(element.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']);
|
||||
app.workspace.setActiveLeaf(leafRef.current);
|
||||
isEditingRef.current = true;
|
||||
}
|
||||
}, [leafRef.current, element]);
|
||||
|
||||
react.useEffect(() => {
|
||||
if(!containerRef?.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stopPropagation = (event:KeyboardEvent) => {
|
||||
event.stopPropagation(); // Stop the event from propagating up the DOM tree
|
||||
}
|
||||
|
||||
containerRef.current.addEventListener("keydown", stopPropagation);
|
||||
containerRef.current.addEventListener("keyup", stopPropagation);
|
||||
containerRef.current.addEventListener("keypress", stopPropagation);
|
||||
containerRef.current.addEventListener("click", handleClick);
|
||||
|
||||
return () => {
|
||||
if(!containerRef?.current) {
|
||||
return;
|
||||
}
|
||||
containerRef.current.removeEventListener("keydown", stopPropagation);
|
||||
containerRef.current.removeEventListener("keyup", stopPropagation);
|
||||
containerRef.current.removeEventListener("keypress", stopPropagation);
|
||||
containerRef.current.removeEventListener("click", handleClick);
|
||||
}; //cleanup on unmount
|
||||
}, []);
|
||||
|
||||
react.useEffect(() => {
|
||||
if(!containerRef?.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!leafRef.current?.view || leafRef.current.view.getViewType() !== "markdown") {
|
||||
return;
|
||||
}
|
||||
|
||||
//@ts-ignore
|
||||
const modes = leafRef.current.view.modes;
|
||||
if(!modes) {
|
||||
return;
|
||||
}
|
||||
|
||||
isActiveRef.current = appState.activeIFrameElement === element;
|
||||
|
||||
if(!isActiveRef.current) {
|
||||
//@ts-ignore
|
||||
leafRef.current.view.setMode(modes["preview"]);
|
||||
isEditingRef.current = false;
|
||||
app.workspace.setActiveLeaf(view.leaf);
|
||||
return;
|
||||
}
|
||||
}, [appState.activeIFrameElement, element]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const CustomIFrame: React.FC<{element: NonDeletedExcalidrawElement; radius: number; view: ExcalidrawView; appState: UIAppState; linkText: string}> = ({ element, radius, view, appState, linkText }) => {
|
||||
const react = view.plugin.getPackage(view.ownerWindow).react;
|
||||
const containerRef: React.RefObject<HTMLDivElement> = react.useRef(null);
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
style = {{
|
||||
width: `100%`,
|
||||
height: `100%`,
|
||||
borderRadius: `${radius}px`,
|
||||
color: `var(--text-normal)`,
|
||||
}}
|
||||
className={isObsidianThemeDark() ? "theme-dark" : "theme-light"}
|
||||
>
|
||||
<RenderObsidianView
|
||||
element={element}
|
||||
linkText={linkText}
|
||||
radius={radius}
|
||||
view={view}
|
||||
containerRef={containerRef}
|
||||
appState={appState}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user