import React, { useContext } from "react"; import { flushSync } from "react-dom"; import { RoughCanvas } from "roughjs/bin/canvas"; import rough from "roughjs/bin/rough"; import clsx from "clsx"; import { nanoid } from "nanoid"; import { actionAddToLibrary, actionBringForward, actionBringToFront, actionCopy, actionCopyAsPng, actionCopyAsSvg, copyText, actionCopyStyles, actionCut, actionDeleteSelected, actionDuplicateSelection, actionFinalize, actionFlipHorizontal, actionFlipVertical, actionGroup, actionPasteStyles, actionSelectAll, actionSendBackward, actionSendToBack, actionToggleGridMode, actionToggleStats, actionToggleZenMode, actionUnbindText, actionBindText, actionUngroup, actionLink, actionToggleElementLock, actionToggleLinearEditor, actionToggleObjectsSnapMode, } from "../actions"; import { createRedoAction, createUndoAction } from "../actions/actionHistory"; import { ActionManager } from "../actions/manager"; import { actions } from "../actions/register"; import { Action, ActionResult } from "../actions/types"; import { trackEvent } from "../analytics"; import { getDefaultAppState, isEraserActive, isHandToolActive, } from "../appState"; import { PastedMixedContent, copyTextToSystemClipboard, parseClipboard, } from "../clipboard"; import { APP_NAME, CURSOR_TYPE, DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT, DEFAULT_VERTICAL_ALIGN, DRAGGING_THRESHOLD, ELEMENT_SHIFT_TRANSLATE_AMOUNT, ELEMENT_TRANSLATE_AMOUNT, ENV, EVENT, FRAME_STYLE, EXPORT_IMAGE_TYPES, GRID_SIZE, IMAGE_MIME_TYPES, IMAGE_RENDER_TIMEOUT, isBrave, LINE_CONFIRM_THRESHOLD, MAX_ALLOWED_FILE_BYTES, MIME_TYPES, MQ_MAX_HEIGHT_LANDSCAPE, MQ_MAX_WIDTH_LANDSCAPE, MQ_MAX_WIDTH_PORTRAIT, MQ_RIGHT_SIDEBAR_MIN_WIDTH, POINTER_BUTTON, ROUNDNESS, SCROLL_TIMEOUT, TAP_TWICE_TIMEOUT, TEXT_TO_CENTER_SNAP_THRESHOLD, THEME, THEME_FILTER, TOUCH_CTX_MENU_TIMEOUT, VERTICAL_ALIGN, YOUTUBE_STATES, ZOOM_STEP, POINTER_EVENTS, TOOL_TYPE, EDITOR_LS_KEYS, isIOS, } from "../constants"; import { ExportedElements, exportCanvas, loadFromBlob } from "../data"; import Library, { distributeLibraryItemsOnSquareGrid } from "../data/library"; import { restore, restoreElements } from "../data/restore"; import { dragNewElement, dragSelectedElements, duplicateElement, getCommonBounds, getCursorForResizingElement, getDragOffsetXY, getElementWithTransformHandleType, getNormalizedDimensions, getResizeArrowDirection, getResizeOffsetXY, getLockedLinearCursorAlignSize, getTransformHandleTypeFromCoords, hitTest, isHittingElementBoundingBoxWithoutHittingElement, isInvisiblySmallElement, isNonDeletedElement, isTextElement, newElement, newLinearElement, newTextElement, newImageElement, textWysiwyg, transformElements, updateTextElement, redrawTextBoundingBox, } from "../element"; import { bindOrUnbindLinearElement, bindOrUnbindSelectedElements, fixBindingsAfterDeletion, fixBindingsAfterDuplication, getEligibleElementsForBinding, getHoveredElementForBinding, isBindingEnabled, isLinearElementSimpleAndAlreadyBound, maybeBindLinearElement, shouldEnableBindingForPointerEvent, unbindLinearElements, updateBoundElements, } from "../element/binding"; import { LinearElementEditor } from "../element/linearElementEditor"; import { mutateElement, newElementWith } from "../element/mutateElement"; import { deepCopyElement, duplicateElements, newFrameElement, newFreeDrawElement, newEmbeddableElement, newMagicFrameElement, newIframeElement, } from "../element/newElement"; import { hasBoundTextElement, isArrowElement, isBindingElement, isBindingElementType, isBoundToContainer, isFrameLikeElement, isImageElement, isEmbeddableElement, isInitializedImageElement, isLinearElement, isLinearElementType, isUsingAdaptiveRadius, isFrameElement, isIframeElement, isIframeLikeElement, isMagicFrameElement, } from "../element/typeChecks"; import { ExcalidrawBindableElement, ExcalidrawElement, ExcalidrawFreeDrawElement, ExcalidrawGenericElement, ExcalidrawLinearElement, ExcalidrawTextElement, NonDeleted, InitializedExcalidrawImageElement, ExcalidrawImageElement, FileId, NonDeletedExcalidrawElement, ExcalidrawTextContainer, ExcalidrawFrameLikeElement, ExcalidrawMagicFrameElement, ExcalidrawIframeLikeElement, IframeData, ExcalidrawIframeElement, ExcalidrawEmbeddableElement, } from "../element/types"; import { getCenter, getDistance } from "../gesture"; import { editGroupForSelectedElement, getElementsInGroup, getSelectedGroupIdForElement, getSelectedGroupIds, isElementInGroup, isSelectedViaGroup, selectGroupsForSelectedElements, } from "../groups"; import History from "../history"; import { defaultLang, getLanguage, languages, setLanguage, t } from "../i18n"; import { CODES, shouldResizeFromCenter, shouldMaintainAspectRatio, shouldRotateWithDiscreteAngle, isArrowKey, KEYS, } from "../keys"; import { isElementInViewport } from "../element/sizeHelpers"; import { distance2d, getCornerRadius, getGridPoint, isPathALoop, } from "../math"; import { calculateScrollCenter, getElementsAtPosition, getElementsWithinSelection, getNormalizedZoom, getSelectedElements, hasBackground, isOverScrollBars, isSomeElementSelected, } from "../scene"; import Scene from "../scene/Scene"; import { RenderInteractiveSceneCallback, ScrollBars } from "../scene/types"; import { getStateForZoom } from "../scene/zoom"; import { findShapeByKey } from "../shapes"; import { AppClassProperties, AppProps, AppState, BinaryFileData, DataURL, ExcalidrawImperativeAPI, BinaryFiles, Gesture, GestureEvent, LibraryItems, PointerDownState, SceneData, Device, FrameNameBoundsCache, SidebarName, SidebarTabName, KeyboardModifiersObject, CollaboratorPointer, ToolType, OnUserFollowedPayload, UnsubscribeCallback, EmbedsValidationStatus, ElementsPendingErasure, } from "../types"; import { debounce, distance, getFontString, getNearestScrollableContainer, isInputLike, isToolIcon, isWritableElement, sceneCoordsToViewportCoords, tupleToCoors, viewportCoordsToSceneCoords, wrapEvent, updateObject, updateActiveTool, getShortcutKey, isTransparent, easeToValuesRAF, muteFSAbortError, isTestEnv, easeOut, updateStable, addEventListener, normalizeEOL, } from "../utils"; import { createSrcDoc, embeddableURLValidator, maybeParseEmbedSrc, getEmbedLink, } from "../element/embeddable"; import { ContextMenu, ContextMenuItems, CONTEXT_MENU_SEPARATOR, } from "./ContextMenu"; import LayerUI from "./LayerUI"; import { Toast } from "./Toast"; import { actionToggleViewMode } from "../actions/actionToggleViewMode"; import { dataURLToFile, generateIdFromFile, getDataURL, getFileFromEvent, ImageURLToFile, isImageFileHandle, isSupportedImageFile, loadSceneOrLibraryFromBlob, normalizeFile, parseLibraryJSON, resizeImageFile, SVGStringToFile, } from "../data/blob"; import { getInitializedImageElements, loadHTMLImageElement, normalizeSVG, updateImageCache as _updateImageCache, } from "../element/image"; import throttle from "lodash.throttle"; import { fileOpen, FileSystemHandle } from "../data/filesystem"; import { bindTextToShapeAfterDuplication, getApproxMinLineHeight, getApproxMinLineWidth, getBoundTextElement, getContainerCenter, getContainerElement, getDefaultLineHeight, getLineHeightInPx, getTextBindableContainerAtPosition, isMeasureTextSupported, isValidTextContainer, } from "../element/textElement"; import { isHittingElementNotConsideringBoundingBox } from "../element/collision"; import { showHyperlinkTooltip, hideHyperlinkToolip, Hyperlink, isPointHittingLink, isPointHittingLinkIcon, } from "../element/Hyperlink"; import { isLocalLink, normalizeLink, toValidURL } from "../data/url"; import { shouldShowBoundingBox } from "../element/transformHandles"; import { actionUnlockAllElements } from "../actions/actionElementLock"; import { Fonts } from "../scene/Fonts"; import { getFrameChildren, isCursorInFrame, bindElementsToFramesAfterDuplication, addElementsToFrame, replaceAllElementsInFrame, removeElementsFromFrame, getElementsInResizingFrame, getElementsInNewFrame, getContainingFrame, elementOverlapsWithFrame, updateFrameMembershipOfSelectedElements, isElementInFrame, getFrameLikeTitle, getElementsOverlappingFrame, filterElementsEligibleAsFrameChildren, } from "../frame"; import { excludeElementsInFramesFromSelection, makeNextSelectedElementIds, } from "../scene/selection"; import { actionPaste } from "../actions/actionClipboard"; import { actionRemoveAllElementsFromFrame, actionSelectAllElementsInFrame, } from "../actions/actionFrame"; import { actionToggleHandTool, zoomToFit } from "../actions/actionCanvas"; import { jotaiStore } from "../jotai"; import { activeConfirmDialogAtom } from "./ActiveConfirmDialog"; import { ImageSceneDataError } from "../errors"; import { getSnapLinesAtPointer, snapDraggedElements, isActiveToolNonLinearSnappable, snapNewElement, snapResizingElements, isSnappingEnabled, getVisibleGaps, getReferenceSnapPoints, SnapCache, } from "../snapping"; import { actionWrapTextInContainer } from "../actions/actionBoundText"; import BraveMeasureTextError from "./BraveMeasureTextError"; import { activeEyeDropperAtom } from "./EyeDropper"; import { ExcalidrawElementSkeleton, convertToExcalidrawElements, } from "../data/transform"; import { ValueOf } from "../utility-types"; import { isSidebarDockedAtom } from "./Sidebar/Sidebar"; import { StaticCanvas, InteractiveCanvas } from "./canvases"; import { Renderer } from "../scene/Renderer"; import { ShapeCache } from "../scene/ShapeCache"; import { SVGLayer } from "./SVGLayer"; import { setEraserCursor, setCursor, resetCursor, setCursorForShape, } from "../cursor"; import { Emitter } from "../emitter"; import { ElementCanvasButtons } from "../element/ElementCanvasButtons"; import { MagicCacheData, diagramToHTML } from "../data/magic"; import { exportToBlob } from "../../utils/export"; import { COLOR_PALETTE } from "../colors"; import { ElementCanvasButton } from "./MagicButton"; import { MagicIcon, copyIcon, fullscreenIcon } from "./icons"; import { EditorLocalStorage } from "../data/EditorLocalStorage"; import FollowMode from "./FollowMode/FollowMode"; import { AnimationFrameHandler } from "../animation-frame-handler"; import { AnimatedTrail } from "../animated-trail"; import { LaserTrails } from "../laser-trails"; import { withBatchedUpdates, withBatchedUpdatesThrottled } from "../reactUtils"; import { getRenderOpacity } from "../renderer/renderElement"; const AppContext = React.createContext(null!); const AppPropsContext = React.createContext(null!); const deviceContextInitialValue = { viewport: { isMobile: false, isLandscape: false, }, editor: { isMobile: false, canFitSidebar: false, }, isTouchScreen: false, }; const DeviceContext = React.createContext(deviceContextInitialValue); DeviceContext.displayName = "DeviceContext"; export const ExcalidrawContainerContext = React.createContext<{ container: HTMLDivElement | null; id: string | null; }>({ container: null, id: null }); ExcalidrawContainerContext.displayName = "ExcalidrawContainerContext"; const ExcalidrawElementsContext = React.createContext< readonly NonDeletedExcalidrawElement[] >([]); ExcalidrawElementsContext.displayName = "ExcalidrawElementsContext"; const ExcalidrawAppStateContext = React.createContext({ ...getDefaultAppState(), width: 0, height: 0, offsetLeft: 0, offsetTop: 0, }); ExcalidrawAppStateContext.displayName = "ExcalidrawAppStateContext"; const ExcalidrawSetAppStateContext = React.createContext< React.Component["setState"] >(() => { console.warn("Uninitialized ExcalidrawSetAppStateContext context!"); }); ExcalidrawSetAppStateContext.displayName = "ExcalidrawSetAppStateContext"; const ExcalidrawActionManagerContext = React.createContext( null!, ); ExcalidrawActionManagerContext.displayName = "ExcalidrawActionManagerContext"; export const useApp = () => useContext(AppContext); export const useAppProps = () => useContext(AppPropsContext); export const useDevice = () => useContext(DeviceContext); export const useExcalidrawContainer = () => useContext(ExcalidrawContainerContext); export const useExcalidrawElements = () => useContext(ExcalidrawElementsContext); export const useExcalidrawAppState = () => useContext(ExcalidrawAppStateContext); export const useExcalidrawSetAppState = () => useContext(ExcalidrawSetAppStateContext); export const useExcalidrawActionManager = () => useContext(ExcalidrawActionManagerContext); const supportsResizeObserver = typeof window !== "undefined" && "ResizeObserver" in window; let didTapTwice: boolean = false; let tappedTwiceTimer = 0; let isHoldingSpace: boolean = false; let isPanning: boolean = false; let isDraggingScrollBar: boolean = false; let currentScrollBars: ScrollBars = { horizontal: null, vertical: null }; let touchTimeout = 0; let invalidateContextMenu = false; /** * Map of youtube embed video states */ const YOUTUBE_VIDEO_STATES = new Map< ExcalidrawElement["id"], ValueOf >(); let IS_PLAIN_PASTE = false; let IS_PLAIN_PASTE_TIMER = 0; let PLAIN_PASTE_TOAST_SHOWN = false; let lastPointerUp: (() => void) | null = null; const gesture: Gesture = { pointers: new Map(), lastCenter: null, initialDistance: null, initialScale: null, }; class App extends React.Component { canvas: AppClassProperties["canvas"]; interactiveCanvas: AppClassProperties["interactiveCanvas"] = null; rc: RoughCanvas; unmounted: boolean = false; actionManager: ActionManager; device: Device = deviceContextInitialValue; private excalidrawContainerRef = React.createRef(); public scene: Scene; public renderer: Renderer; private fonts: Fonts; private resizeObserver: ResizeObserver | undefined; private nearestScrollableContainer: HTMLElement | Document | undefined; public library: AppClassProperties["library"]; public libraryItemsFromStorage: LibraryItems | undefined; public id: string; private history: History; private excalidrawContainerValue: { container: HTMLDivElement | null; id: string; }; public files: BinaryFiles = {}; public imageCache: AppClassProperties["imageCache"] = new Map(); private iFrameRefs = new Map(); /** * Indicates whether the embeddable's url has been validated for rendering. * If value not set, indicates that the validation is pending. * Initially or on url change the flag is not reset so that we can guarantee * the validation came from a trusted source (the editor). **/ private embedsValidationStatus: EmbedsValidationStatus = new Map(); /** embeds that have been inserted to DOM (as a perf optim, we don't want to * insert to DOM before user initially scrolls to them) */ private initializedEmbeds = new Set(); private elementsPendingErasure: ElementsPendingErasure = new Set(); hitLinkElement?: NonDeletedExcalidrawElement; lastPointerDownEvent: React.PointerEvent | null = null; lastPointerUpEvent: React.PointerEvent | PointerEvent | null = null; lastPointerMoveEvent: PointerEvent | null = null; lastViewportPosition = { x: 0, y: 0 }; animationFrameHandler = new AnimationFrameHandler(); laserTrails = new LaserTrails(this.animationFrameHandler, this); eraserTrail = new AnimatedTrail(this.animationFrameHandler, this, { streamline: 0.2, size: 5, keepHead: true, sizeMapping: (c) => { const DECAY_TIME = 200; const DECAY_LENGTH = 10; const t = Math.max(0, 1 - (performance.now() - c.pressure) / DECAY_TIME); const l = (DECAY_LENGTH - Math.min(DECAY_LENGTH, c.totalLength - c.currentIndex)) / DECAY_LENGTH; return Math.min(easeOut(l), easeOut(t)); }, fill: () => this.state.theme === THEME.LIGHT ? "rgba(0, 0, 0, 0.2)" : "rgba(255, 255, 255, 0.2)", }); onChangeEmitter = new Emitter< [ elements: readonly ExcalidrawElement[], appState: AppState, files: BinaryFiles, ] >(); onPointerDownEmitter = new Emitter< [ activeTool: AppState["activeTool"], pointerDownState: PointerDownState, event: React.PointerEvent, ] >(); onPointerUpEmitter = new Emitter< [ activeTool: AppState["activeTool"], pointerDownState: PointerDownState, event: PointerEvent, ] >(); onUserFollowEmitter = new Emitter<[payload: OnUserFollowedPayload]>(); onScrollChangeEmitter = new Emitter< [scrollX: number, scrollY: number, zoom: AppState["zoom"]] >(); missingPointerEventCleanupEmitter = new Emitter< [event: PointerEvent | null] >(); onRemoveEventListenersEmitter = new Emitter<[]>(); constructor(props: AppProps) { super(props); const defaultAppState = getDefaultAppState(); const { excalidrawAPI, viewModeEnabled = false, zenModeEnabled = false, gridModeEnabled = false, objectsSnapModeEnabled = false, theme = defaultAppState.theme, name = defaultAppState.name, } = props; this.state = { ...defaultAppState, theme, isLoading: true, ...this.getCanvasOffsets(), viewModeEnabled, zenModeEnabled, objectsSnapModeEnabled, gridSize: gridModeEnabled ? GRID_SIZE : null, name, width: window.innerWidth, height: window.innerHeight, }; this.id = nanoid(); this.library = new Library(this); this.actionManager = new ActionManager( this.syncActionResult, () => this.state, () => this.scene.getElementsIncludingDeleted(), this, ); this.scene = new Scene(); this.canvas = document.createElement("canvas"); this.rc = rough.canvas(this.canvas); this.renderer = new Renderer(this.scene); if (excalidrawAPI) { const api: ExcalidrawImperativeAPI = { updateScene: this.updateScene, updateLibrary: this.library.updateLibrary, addFiles: this.addFiles, resetScene: this.resetScene, getSceneElementsIncludingDeleted: this.getSceneElementsIncludingDeleted, history: { clear: this.resetHistory, }, scrollToContent: this.scrollToContent, getSceneElements: this.getSceneElements, getAppState: () => this.state, getFiles: () => this.files, registerAction: (action: Action) => { this.actionManager.registerAction(action); }, refresh: this.refresh, setToast: this.setToast, id: this.id, setActiveTool: this.setActiveTool, setCursor: this.setCursor, resetCursor: this.resetCursor, updateFrameRendering: this.updateFrameRendering, toggleSidebar: this.toggleSidebar, onChange: (cb) => this.onChangeEmitter.on(cb), onPointerDown: (cb) => this.onPointerDownEmitter.on(cb), onPointerUp: (cb) => this.onPointerUpEmitter.on(cb), onScrollChange: (cb) => this.onScrollChangeEmitter.on(cb), onUserFollow: (cb) => this.onUserFollowEmitter.on(cb), } as const; if (typeof excalidrawAPI === "function") { excalidrawAPI(api); } else { console.error("excalidrawAPI should be a function!"); } } this.excalidrawContainerValue = { container: this.excalidrawContainerRef.current, id: this.id, }; this.fonts = new Fonts({ scene: this.scene, onSceneUpdated: this.onSceneUpdated, }); this.history = new History(); this.actionManager.registerAll(actions); this.actionManager.registerAction(createUndoAction(this.history)); this.actionManager.registerAction(createRedoAction(this.history)); } private onWindowMessage(event: MessageEvent) { if ( event.origin !== "https://player.vimeo.com" && event.origin !== "https://www.youtube.com" ) { return; } let data = null; try { data = JSON.parse(event.data); } catch (e) {} if (!data) { return; } switch (event.origin) { case "https://player.vimeo.com": //Allowing for multiple instances of Excalidraw running in the window if (data.method === "paused") { let source: Window | null = null; const iframes = document.body.querySelectorAll( "iframe.excalidraw__embeddable", ); if (!iframes) { break; } for (const iframe of iframes as NodeListOf) { if (iframe.contentWindow === event.source) { source = iframe.contentWindow; } } source?.postMessage( JSON.stringify({ method: data.value ? "play" : "pause", value: true, }), "*", ); } break; case "https://www.youtube.com": if ( data.event === "infoDelivery" && data.info && data.id && typeof data.info.playerState === "number" ) { const id = data.id; const playerState = data.info.playerState as number; if ( (Object.values(YOUTUBE_STATES) as number[]).includes(playerState) ) { YOUTUBE_VIDEO_STATES.set( id, playerState as ValueOf, ); } } break; } } private cacheEmbeddableRef( element: ExcalidrawIframeLikeElement, ref: HTMLIFrameElement | null, ) { if (ref) { this.iFrameRefs.set(element.id, ref); } } private getHTMLIFrameElement( element: ExcalidrawIframeLikeElement, ): HTMLIFrameElement | undefined { return this.iFrameRefs.get(element.id); } private handleEmbeddableCenterClick(element: ExcalidrawIframeLikeElement) { if ( this.state.activeEmbeddable?.element === element && this.state.activeEmbeddable?.state === "active" ) { return; } // The delay serves two purposes // 1. To prevent first click propagating to iframe on mobile, // else the click will immediately start and stop the video // 2. If the user double clicks the frame center to activate it // without the delay youtube will immediately open the video // in fullscreen mode setTimeout(() => { this.setState({ activeEmbeddable: { element, state: "active" }, selectedElementIds: { [element.id]: true }, draggingElement: null, selectionElement: null, }); }, 100); if (isIframeElement(element)) { return; } const iframe = this.getHTMLIFrameElement(element); if (!iframe?.contentWindow) { return; } if (iframe.src.includes("youtube")) { const state = YOUTUBE_VIDEO_STATES.get(element.id); if (!state) { YOUTUBE_VIDEO_STATES.set(element.id, YOUTUBE_STATES.UNSTARTED); iframe.contentWindow.postMessage( JSON.stringify({ event: "listening", id: element.id, }), "*", ); } switch (state) { case YOUTUBE_STATES.PLAYING: case YOUTUBE_STATES.BUFFERING: iframe.contentWindow?.postMessage( JSON.stringify({ event: "command", func: "pauseVideo", args: "", }), "*", ); break; default: iframe.contentWindow?.postMessage( JSON.stringify({ event: "command", func: "playVideo", args: "", }), "*", ); } } if (iframe.src.includes("player.vimeo.com")) { iframe.contentWindow.postMessage( JSON.stringify({ method: "paused", //video play/pause in onWindowMessage handler }), "*", ); } } private isIframeLikeElementCenter( el: ExcalidrawIframeLikeElement | null, event: React.PointerEvent | PointerEvent, sceneX: number, sceneY: number, ) { return ( el && !event.altKey && !event.shiftKey && !event.metaKey && !event.ctrlKey && (this.state.activeEmbeddable?.element !== el || this.state.activeEmbeddable?.state === "hover" || !this.state.activeEmbeddable) && sceneX >= el.x + el.width / 3 && sceneX <= el.x + (2 * el.width) / 3 && sceneY >= el.y + el.height / 3 && sceneY <= el.y + (2 * el.height) / 3 ); } private updateEmbedValidationStatus = ( element: ExcalidrawEmbeddableElement, status: boolean, ) => { this.embedsValidationStatus.set(element.id, status); ShapeCache.delete(element); }; private updateEmbeddables = () => { const iframeLikes = new Set(); let updated = false; this.scene.getNonDeletedElements().filter((element) => { if (isEmbeddableElement(element)) { iframeLikes.add(element.id); if (!this.embedsValidationStatus.has(element.id)) { updated = true; const validated = embeddableURLValidator( element.link, this.props.validateEmbeddable, ); this.updateEmbedValidationStatus(element, validated); } } else if (isIframeElement(element)) { iframeLikes.add(element.id); } return false; }); if (updated) { this.scene.informMutation(); } // GC this.iFrameRefs.forEach((ref, id) => { if (!iframeLikes.has(id)) { this.iFrameRefs.delete(id); } }); }; private renderEmbeddables() { const scale = this.state.zoom.value; const normalizedWidth = this.state.width; const normalizedHeight = this.state.height; const embeddableElements = this.scene .getNonDeletedElements() .filter( (el): el is NonDeleted => (isEmbeddableElement(el) && this.embedsValidationStatus.get(el.id) === true) || isIframeElement(el), ); return ( <> {embeddableElements.map((el) => { const { x, y } = sceneCoordsToViewportCoords( { sceneX: el.x, sceneY: el.y }, this.state, ); const isVisible = isElementInViewport( el, normalizedWidth, normalizedHeight, this.state, ); const hasBeenInitialized = this.initializedEmbeds.has(el.id); if (isVisible && !hasBeenInitialized) { this.initializedEmbeds.add(el.id); } const shouldRender = isVisible || hasBeenInitialized; if (!shouldRender) { return null; } let src: IframeData | null; if (isIframeElement(el)) { src = null; const data: MagicCacheData = (el.customData?.generationData ?? this.magicGenerations.get(el.id)) || { status: "error", message: "No generation data", code: "ERR_NO_GENERATION_DATA", }; if (data.status === "done") { const html = data.html; src = { intrinsicSize: { w: el.width, h: el.height }, type: "document", srcdoc: () => { return html; }, } as const; } else if (data.status === "pending") { src = { intrinsicSize: { w: el.width, h: el.height }, type: "document", srcdoc: () => { return createSrcDoc(`
Generating...
`); }, } as const; } else { let message: string; if (data.code === "ERR_GENERATION_INTERRUPTED") { message = "Generation was interrupted..."; } else { message = data.message || "Generation failed"; } src = { intrinsicSize: { w: el.width, h: el.height }, type: "document", srcdoc: () => { return createSrcDoc(`

Error!

${message}

`); }, } as const; } } else { src = getEmbedLink(toValidURL(el.link || "")); } const isActive = this.state.activeEmbeddable?.element === el && this.state.activeEmbeddable?.state === "active"; const isHovered = this.state.activeEmbeddable?.element === el && this.state.activeEmbeddable?.state === "hover"; return (
{ if (!this.excalidrawContainerRef.current) { return; } const container = this.excalidrawContainerRef.current; const sh = container.scrollHeight; const ch = container.clientHeight; if (sh !== ch) { container.style.height = `${sh}px`; setTimeout(() => { container.style.height = `100%`; }); } }}*/ className="excalidraw__embeddable-container__inner" style={{ width: isVisible ? `${el.width}px` : 0, height: isVisible ? `${el.height}px` : 0, transform: isVisible ? `rotate(${el.angle}rad)` : "none", pointerEvents: isActive ? POINTER_EVENTS.enabled : POINTER_EVENTS.disabled, }} > {isHovered && (
{t("buttons.embeddableInteractionButton")}
)}
{(isEmbeddableElement(el) ? this.props.renderEmbeddable?.(el, this.state) : null) ?? (