Files
furniture/node_modules/react-konva/es/ReactKonvaCore.js
T
2026-10-17 12:09:36 -04:00

209 lines
8.5 KiB
JavaScript

/**
* 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 };