/** * Based on ReactArt.js * Copyright (c) 2017-present Lavrenov Anton. * All rights reserved. * * MIT */ 'use strict'; import React from 'react'; import { useFormStatus } from 'react-dom'; const [reactMajor, reactMinor] = React.version.split('.').map(Number); if (reactMajor !== 19 || reactMinor < 3) { throw new Error('react-konva requires React 19.3 or later within React 19. ' + 'Install matching versions of react and react-dom. ' + 'For React 19.2, use react-konva 19.2. For React 18, use react-konva 18.'); } import Konva from 'konva/lib/Core.js'; import ReactFiberReconciler from 'react-reconciler'; import { ConcurrentRoot } from 'react-reconciler/constants.js'; import * as HostConfig from './ReactKonvaHostConfig.js'; import { getEventBatch, addPendingUnmount, prepareUnmounts } from './EventBatch.js'; import { applyNodeProps, toggleStrictMode, EVENTS_NAMESPACE, } from './makeUpdates.js'; import { useContextBridge, useFiber, traverseFiber, FiberProvider } from 'its-fine'; function SuspendCanvas({ promise }) { throw promise; } const StageWrap = (props) => { const container = React.useRef(null); const stage = React.useRef(null); const fiberRef = React.useRef(null); const children = React.useRef(null); const suspension = React.useRef(null); // DOM and Konva reconcilers have separate useId counters. const identifierPrefix = React.useId(); const formStatus = useFormStatus(); const previousProps = React.useRef({}); // Keep the render-time snapshot when React replays layout effects. const oldProps = previousProps.current; const Bridge = useContextBridge(); const fiber = useFiber(); const isStrictMode = !!traverseFiber(fiber, true, (node) => node.type === React.StrictMode); const effectsDisconnected = React.useRef(false); const isUnmounting = React.useRef(false); const cancelUnmount = React.useRef(null); // Insertion effects stay mounted during hiding and StrictMode replay. // Activity has already disconnected layout and passive effects when hidden. // Its remaining insertion cleanups must still run inside a DOM commit. React.useInsertionEffect(() => { isUnmounting.current = false; return () => { isUnmounting.current = true; if (effectsDisconnected.current && stage.current) destroyStage(); }; }, []); const destroyStage = () => { cancelUnmount.current?.(); cancelUnmount.current = null; prepareUnmounts(); // Discard queued child work before destroying the Stage, including updates // from external stores whose objects are no longer alive. KonvaRenderer.flushSyncFromReconciler(() => { KonvaRenderer.updateContainer(null, fiberRef.current, null); }); stage.current?.destroy(); stage.current?.off(EVENTS_NAMESPACE); stage.current = null; suspension.current?.resolve(); suspension.current = null; }; const setCanvasVisibility = (mode) => { if (mode === 'suspended') { if (!suspension.current) { let resolve; const promise = new Promise((done) => { resolve = done; }); suspension.current = { promise, resolve }; } } else { suspension.current?.resolve(); suspension.current = null; } KonvaRenderer.updateContainer(React.createElement(React.Activity, { mode: mode === 'hidden' ? 'hidden' : 'visible', children: React.createElement(React.Suspense, { fallback: null }, children.current, suspension.current && React.createElement(SuspendCanvas, { promise: suspension.current.promise, })), }), fiberRef.current, null); prepareUnmounts(); // Parent layout effects must see the committed canvas nodes and refs. KonvaRenderer.flushSyncWork(); }; React.useLayoutEffect(() => { cancelUnmount.current?.(); cancelUnmount.current = null; // Reconnecting layout effects reuses the existing Stage. if (!stage.current) { stage.current = new Konva.Stage({ width: props.width, height: props.height, container: container.current, }); fiberRef.current = KonvaRenderer.createContainer(stage.current, ConcurrentRoot, null, isStrictMode, null, identifierPrefix, console.error, console.error, console.error, null, null); } return () => { if (!isUnmounting.current) { // If this hidden Stage is deleted, discard queued child work before // another root or the scheduler flushes it. const unmount = () => { if (!isUnmounting.current) return; cancelUnmount.current?.(); cancelUnmount.current = null; KonvaRenderer.updateContainer(null, fiberRef.current, null); }; cancelUnmount.current = addPendingUnmount(unmount); // Suspense disconnects layout effects but keeps passive effects. setCanvasVisibility('suspended'); } else { destroyStage(); } }; }, []); React.useEffect(() => { effectsDisconnected.current = false; return () => { effectsDisconnected.current = true; // Activity and StrictMode also disconnect the DOM passive effects. if (!stage.current) return; if (isUnmounting.current) destroyStage(); else setCanvasVisibility('hidden'); }; }, []); React.useLayoutEffect(() => { // Older Konva versions use the normal asynchronous React scheduler. if (typeof stage.current.eventBatchFunc === 'function') { stage.current.eventBatchFunc(getEventBatch(props.eventBatchFunc)); } }, [props.eventBatchFunc]); React.useImperativeHandle(props.forwardedRef, () => stage.current, []); React.useLayoutEffect(() => { previousProps.current = props; applyNodeProps(stage.current, props, oldProps); // updateContainer schedules sync-lane work; with async scheduleMicrotask // the work is queued, so flushSyncWork() drains it inline here so that // a parent useLayoutEffect can read Konva nodes added by subscribing // children. (Lighter than flushSyncFromReconciler — no callback wrapper.) children.current = React.createElement(HostConfig.HostTransitionContext.Provider, { value: formStatus }, React.createElement(Bridge, {}, props.children)); setCanvasVisibility('visible'); }); return React.createElement('div', { ref: container, id: props.id, accessKey: props.accessKey, className: props.className, role: props.role, style: props.style, tabIndex: props.tabIndex, title: props.title, }); }; export const Layer = 'Layer'; export const FastLayer = 'FastLayer'; export const Group = 'Group'; export const Label = 'Label'; export const Rect = 'Rect'; export const Circle = 'Circle'; export const Ellipse = 'Ellipse'; export const Wedge = 'Wedge'; export const Line = 'Line'; export const Sprite = 'Sprite'; export const Image = 'Image'; export const Text = 'Text'; export const TextPath = 'TextPath'; export const Star = 'Star'; export const Ring = 'Ring'; export const Arc = 'Arc'; export const Tag = 'Tag'; export const Path = 'Path'; export const RegularPolygon = 'RegularPolygon'; export const Arrow = 'Arrow'; export const Shape = 'Shape'; export const Transformer = 'Transformer'; export const version = '19.3.2'; const rendererConfig = { ...HostConfig, // React's public context type omits fields used by the reconciler. HostTransitionContext: HostConfig.HostTransitionContext, rendererVersion: version, rendererPackageName: 'react-konva', extraDevToolsConfig: null, }; export const KonvaRenderer = ReactFiberReconciler(rendererConfig); KonvaRenderer.injectIntoDevTools(); export const Stage = React.forwardRef((props, ref) => { return React.createElement(FiberProvider, {}, React.createElement(StageWrap, { ...props, forwardedRef: ref })); }); export const useStrictMode = toggleStrictMode; // export useContextBridge from its-fine for reuse in react-konva-utils // so react-konva-utils don't use its own version of its-fine (it is possible on pnpm) export { useContextBridge };