snapshot of https://github.com/excalidraw/excalidraw.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Tests / test (push) Failing after 15s
Some checks failed
Tests / test (push) Failing after 15s
This commit is contained in:
172
packages/excalidraw/hooks/useAppStateValue.ts
Normal file
172
packages/excalidraw/hooks/useAppStateValue.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import { useExcalidrawAPI } from "../components/App";
|
||||
|
||||
import { getDefaultAppState } from "../appState";
|
||||
|
||||
import type { AppState } from "../types";
|
||||
|
||||
type AppStateSelector =
|
||||
| keyof AppState
|
||||
| (keyof AppState)[]
|
||||
| ((appState: AppState) => unknown);
|
||||
|
||||
const getSelectedValue = (appState: AppState, selector: AppStateSelector) => {
|
||||
if (typeof selector === "function") {
|
||||
return selector(appState);
|
||||
}
|
||||
if (Array.isArray(selector)) {
|
||||
return appState;
|
||||
}
|
||||
return appState[selector];
|
||||
};
|
||||
|
||||
const getLatestValue = (
|
||||
api: ReturnType<typeof useExcalidrawAPI>,
|
||||
selector: AppStateSelector,
|
||||
_internal: boolean,
|
||||
) => {
|
||||
if (api?.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
let appState = api?.getAppState();
|
||||
if (!appState) {
|
||||
if (!_internal) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
console.warn(
|
||||
"useAppStateValue: excalidrawAPI not defined yet for internal component while it should always be defined. Are you sure you're rendering inside of <Excalidraw/> component tree?",
|
||||
);
|
||||
// fall back in case there's a bug so we don't break the app
|
||||
// (internal components using this internal useAppStateValue expect
|
||||
// non-undefined values on init)
|
||||
appState = Object.assign(
|
||||
{ width: 0, height: 0, offsetLeft: 0, offsetTop: 0 },
|
||||
getDefaultAppState(),
|
||||
);
|
||||
}
|
||||
|
||||
return getSelectedValue(appState, selector);
|
||||
};
|
||||
|
||||
/**
|
||||
* Subscribes to specific appState changes. The component re-renders
|
||||
* only when the specified prop(s) change — not on every appState update.
|
||||
*
|
||||
* Works both inside and outside the <Excalidraw> tree, as long as
|
||||
* ExcalidrawAPIContext.Provider is an ancestor (automatically provided
|
||||
* inside <Excalidraw>, or manually by the host app).
|
||||
*
|
||||
* Returns the narrowed value depending on prop form:
|
||||
* - `keyof AppState` → `AppState[K]`
|
||||
* - `(keyof AppState)[]` → whole `AppState`
|
||||
* - selector function → selector's return type `T`
|
||||
*
|
||||
* If excalidrawAPI is not ready yet (host apps), hook is rerendered with latest
|
||||
* value once available.
|
||||
*/
|
||||
export function useAppStateValue<K extends keyof AppState>(
|
||||
prop: K,
|
||||
_internal?: boolean,
|
||||
): AppState[K];
|
||||
export function useAppStateValue(
|
||||
props: (keyof AppState)[],
|
||||
_internal?: boolean,
|
||||
): AppState;
|
||||
export function useAppStateValue<T>(
|
||||
selector: (appState: AppState) => T,
|
||||
_internal?: boolean,
|
||||
): T;
|
||||
export function useAppStateValue(
|
||||
selector: AppStateSelector,
|
||||
_internal: boolean = true,
|
||||
): unknown {
|
||||
const api = useExcalidrawAPI();
|
||||
const [, rerender] = useState(0);
|
||||
|
||||
const stateRef = useRef<{
|
||||
selector: AppStateSelector;
|
||||
isInitialized: boolean;
|
||||
latestValue: unknown;
|
||||
} | null>(null);
|
||||
if (!stateRef.current) {
|
||||
stateRef.current = {
|
||||
selector,
|
||||
isInitialized: !!api,
|
||||
latestValue: getLatestValue(api, selector, _internal),
|
||||
};
|
||||
}
|
||||
stateRef.current.selector = selector;
|
||||
if (!stateRef.current.isInitialized && api && !api.isDestroyed) {
|
||||
stateRef.current.isInitialized = true;
|
||||
stateRef.current.latestValue = getLatestValue(api, selector, _internal);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const currentStateRef = stateRef.current;
|
||||
if (!api || api.isDestroyed || !currentStateRef) {
|
||||
return;
|
||||
}
|
||||
|
||||
return api.onStateChange(currentStateRef.selector, (newValue: any) => {
|
||||
currentStateRef.latestValue = newValue;
|
||||
rerender((value) => value + 1);
|
||||
});
|
||||
}, [api]);
|
||||
|
||||
return stateRef.current.latestValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to specific appState changes without causing component rerenders.
|
||||
*
|
||||
* The callback is called on every matching change, but also on initial render
|
||||
* so you can initialize your state.
|
||||
*/
|
||||
export function useOnAppStateChange<K extends keyof AppState>(
|
||||
prop: K,
|
||||
callback: (value: AppState[K], appState: AppState) => void,
|
||||
): undefined;
|
||||
export function useOnAppStateChange(
|
||||
props: (keyof AppState)[],
|
||||
callback: (props: AppState, appState: AppState) => void,
|
||||
): undefined;
|
||||
export function useOnAppStateChange<T>(
|
||||
selector: (appState: AppState) => T,
|
||||
callback: (value: T, appState: AppState) => void,
|
||||
): undefined;
|
||||
export function useOnAppStateChange(
|
||||
selector: AppStateSelector,
|
||||
callback: (value: any, appState: AppState) => void,
|
||||
): undefined {
|
||||
const api = useExcalidrawAPI();
|
||||
|
||||
const stateRef = useRef({
|
||||
selector,
|
||||
callback,
|
||||
});
|
||||
stateRef.current.selector = selector;
|
||||
stateRef.current.callback = callback;
|
||||
|
||||
useEffect(() => {
|
||||
if (!api || api.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
stateRef.current.callback(
|
||||
getLatestValue(api, stateRef.current.selector, true),
|
||||
api.getAppState(),
|
||||
);
|
||||
|
||||
return api.onStateChange(
|
||||
stateRef.current.selector,
|
||||
(newValue: any, state: AppState) => {
|
||||
stateRef.current.callback(newValue, state);
|
||||
},
|
||||
);
|
||||
}, [api]);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
7
packages/excalidraw/hooks/useCallbackRefState.ts
Normal file
7
packages/excalidraw/hooks/useCallbackRefState.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
export const useCallbackRefState = <T>() => {
|
||||
const [refValue, setRefValue] = useState<T | null>(null);
|
||||
const refCallback = useCallback((value: T | null) => setRefValue(value), []);
|
||||
return [refValue, refCallback] as const;
|
||||
};
|
||||
27
packages/excalidraw/hooks/useCopiedIndicator.ts
Normal file
27
packages/excalidraw/hooks/useCopiedIndicator.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
|
||||
const TIMEOUT = 2000;
|
||||
|
||||
export const useCopyStatus = () => {
|
||||
const [copyStatus, setCopyStatus] = useState<"success" | null>(null);
|
||||
const timeoutRef = useRef<number>(0);
|
||||
|
||||
const onCopy = () => {
|
||||
clearTimeout(timeoutRef.current);
|
||||
setCopyStatus("success");
|
||||
|
||||
timeoutRef.current = window.setTimeout(() => {
|
||||
setCopyStatus(null);
|
||||
}, TIMEOUT);
|
||||
};
|
||||
|
||||
const resetCopyStatus = useCallback(() => {
|
||||
setCopyStatus(null);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
copyStatus,
|
||||
resetCopyStatus,
|
||||
onCopy,
|
||||
};
|
||||
};
|
||||
53
packages/excalidraw/hooks/useCreatePortalContainer.ts
Normal file
53
packages/excalidraw/hooks/useCreatePortalContainer.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { useState, useLayoutEffect } from "react";
|
||||
|
||||
import { THEME } from "@excalidraw/common";
|
||||
|
||||
import { useEditorInterface, useExcalidrawContainer } from "../components/App";
|
||||
import { useUIAppState } from "../context/ui-appState";
|
||||
|
||||
export const useCreatePortalContainer = (opts?: {
|
||||
className?: string;
|
||||
parentSelector?: string;
|
||||
}) => {
|
||||
const [div, setDiv] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
const editorInterface = useEditorInterface();
|
||||
const { theme } = useUIAppState();
|
||||
|
||||
const { container: excalidrawContainer } = useExcalidrawContainer();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (div) {
|
||||
div.className = "";
|
||||
div.classList.add("excalidraw", ...(opts?.className?.split(/\s+/) || []));
|
||||
div.classList.toggle(
|
||||
"excalidraw--mobile",
|
||||
editorInterface.formFactor === "phone",
|
||||
);
|
||||
div.classList.toggle("theme--dark", theme === THEME.DARK);
|
||||
}
|
||||
}, [div, theme, editorInterface.formFactor, opts?.className]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const ownerDocument = excalidrawContainer?.ownerDocument;
|
||||
const container = opts?.parentSelector
|
||||
? excalidrawContainer?.querySelector(opts.parentSelector)
|
||||
: ownerDocument?.body;
|
||||
|
||||
if (!container || !ownerDocument) {
|
||||
return;
|
||||
}
|
||||
|
||||
const div = ownerDocument.createElement("div");
|
||||
|
||||
container.appendChild(div);
|
||||
|
||||
setDiv(div);
|
||||
|
||||
return () => {
|
||||
container.removeChild(div);
|
||||
};
|
||||
}, [excalidrawContainer, opts?.parentSelector]);
|
||||
|
||||
return div;
|
||||
};
|
||||
22
packages/excalidraw/hooks/useEmitter.ts
Normal file
22
packages/excalidraw/hooks/useEmitter.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import type { Emitter } from "@excalidraw/common";
|
||||
|
||||
export const useEmitter = <TEvent extends unknown>(
|
||||
emitter: Emitter<[TEvent]>,
|
||||
initialState: TEvent,
|
||||
) => {
|
||||
const [event, setEvent] = useState<TEvent>(initialState);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = emitter.on((event) => {
|
||||
setEvent(event);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [emitter]);
|
||||
|
||||
return event;
|
||||
};
|
||||
101
packages/excalidraw/hooks/useLibraryItemSvg.ts
Normal file
101
packages/excalidraw/hooks/useLibraryItemSvg.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { exportToSvg } from "@excalidraw/utils/export";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { COLOR_PALETTE } from "@excalidraw/common";
|
||||
|
||||
import { atom, useAtom } from "../editor-jotai";
|
||||
|
||||
import type { LibraryItem } from "../types";
|
||||
|
||||
export type SvgCache = Map<LibraryItem["id"], SVGSVGElement>;
|
||||
|
||||
export const libraryItemSvgsCache = atom<SvgCache>(new Map());
|
||||
|
||||
const exportLibraryItemToSvg = async (elements: LibraryItem["elements"]) => {
|
||||
// TODO should pass theme (appState.exportWithDark) - we're still using
|
||||
// CSS filter here
|
||||
return await exportToSvg({
|
||||
elements,
|
||||
appState: {
|
||||
exportBackground: false,
|
||||
viewBackgroundColor: COLOR_PALETTE.white,
|
||||
},
|
||||
files: null,
|
||||
renderEmbeddables: false,
|
||||
skipInliningFonts: true,
|
||||
});
|
||||
};
|
||||
|
||||
export const useLibraryItemSvg = (
|
||||
id: LibraryItem["id"] | null,
|
||||
elements: LibraryItem["elements"] | undefined,
|
||||
svgCache: SvgCache,
|
||||
ref: React.RefObject<HTMLDivElement | null>,
|
||||
): SVGSVGElement | undefined => {
|
||||
const [svg, setSvg] = useState<SVGSVGElement>();
|
||||
|
||||
useEffect(() => {
|
||||
if (elements) {
|
||||
if (id) {
|
||||
// Try to load cached svg
|
||||
const cachedSvg = svgCache.get(id);
|
||||
|
||||
if (cachedSvg) {
|
||||
setSvg(cachedSvg);
|
||||
} else {
|
||||
// When there is no svg in cache export it and save to cache
|
||||
(async () => {
|
||||
const exportedSvg = await exportLibraryItemToSvg(elements);
|
||||
// TODO: should likely be removed for custom fonts
|
||||
exportedSvg.querySelector(".style-fonts")?.remove();
|
||||
|
||||
if (exportedSvg) {
|
||||
svgCache.set(id, exportedSvg);
|
||||
setSvg(exportedSvg);
|
||||
}
|
||||
})();
|
||||
}
|
||||
} else {
|
||||
// When we have no id (usualy selected items from canvas) just export the svg
|
||||
(async () => {
|
||||
const exportedSvg = await exportLibraryItemToSvg(elements);
|
||||
setSvg(exportedSvg);
|
||||
})();
|
||||
}
|
||||
}
|
||||
}, [id, elements, svgCache, setSvg]);
|
||||
|
||||
useEffect(() => {
|
||||
const node = ref.current;
|
||||
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (svg) {
|
||||
node.innerHTML = svg.outerHTML;
|
||||
}
|
||||
|
||||
return () => {
|
||||
node.innerHTML = "";
|
||||
};
|
||||
}, [svg, ref]);
|
||||
|
||||
return svg;
|
||||
};
|
||||
|
||||
export const useLibraryCache = () => {
|
||||
const [svgCache] = useAtom(libraryItemSvgsCache);
|
||||
|
||||
const clearLibraryCache = () => svgCache.clear();
|
||||
|
||||
const deleteItemsFromLibraryCache = (items: LibraryItem["id"][]) => {
|
||||
items.forEach((item) => svgCache.delete(item));
|
||||
};
|
||||
|
||||
return {
|
||||
clearLibraryCache,
|
||||
deleteItemsFromLibraryCache,
|
||||
svgCache,
|
||||
};
|
||||
};
|
||||
93
packages/excalidraw/hooks/useOutsideClick.ts
Normal file
93
packages/excalidraw/hooks/useOutsideClick.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { EVENT } from "@excalidraw/common";
|
||||
|
||||
export function useOutsideClick<T extends HTMLElement>(
|
||||
ref: React.RefObject<T | null>,
|
||||
/** if performance is of concern, memoize the callback */
|
||||
callback: (event: Event & { target: T }) => void,
|
||||
/**
|
||||
* Optional callback which is called on every click.
|
||||
*
|
||||
* Should return `true` if click should be considered as inside the container,
|
||||
* and `false` if it falls outside and should call the `callback`.
|
||||
*
|
||||
* Returning `true` overrides the default behavior and `callback` won't be
|
||||
* called.
|
||||
*
|
||||
* Returning `undefined` will fallback to the default behavior.
|
||||
*/
|
||||
isInside?: (
|
||||
event: Event & { target: T },
|
||||
/** the element of the passed ref */
|
||||
container: T,
|
||||
) => boolean | undefined,
|
||||
) {
|
||||
useEffect(() => {
|
||||
const refOwnerDocument = ref.current?.ownerDocument;
|
||||
if (!refOwnerDocument) {
|
||||
return;
|
||||
}
|
||||
const ownerDocument: Document = refOwnerDocument;
|
||||
|
||||
function onOutsideClick(event: Event) {
|
||||
const _event = event as Event & { target: T };
|
||||
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isInsideOverride = isInside?.(_event, ref.current);
|
||||
|
||||
if (isInsideOverride === true) {
|
||||
return;
|
||||
} else if (isInsideOverride === false) {
|
||||
return callback(_event);
|
||||
}
|
||||
|
||||
// clicked element is in the descenendant of the target container
|
||||
if (
|
||||
ref.current.contains(_event.target) ||
|
||||
// target is detached from DOM (happens when the element is removed
|
||||
// on a pointerup event fired *before* this handler's pointerup is
|
||||
// dispatched)
|
||||
!ownerDocument.documentElement.contains(_event.target)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isClickOnRadixPortal =
|
||||
_event.target.closest("[data-radix-portal]") ||
|
||||
// when radix popup is in "modal" mode, it disables pointer events on
|
||||
// the `body` element, so the target element is going to be the `html`
|
||||
// (note: this won't work if we selectively re-enable pointer events on
|
||||
// specific elements as we do with navbar or excalidraw UI elements)
|
||||
(_event.target === ownerDocument.documentElement &&
|
||||
ownerDocument.body.style.pointerEvents === "none");
|
||||
|
||||
// if clicking on radix portal, assume it's a popup that
|
||||
// should be considered as part of the UI. Obviously this is a terrible
|
||||
// hack you can end up click on radix popups that outside the tree,
|
||||
// but it works for most cases and the downside is minimal for now
|
||||
if (isClickOnRadixPortal) {
|
||||
return;
|
||||
}
|
||||
|
||||
// clicking on a container that ignores outside clicks
|
||||
if (_event.target.closest("[data-prevent-outside-click]")) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(_event);
|
||||
}
|
||||
|
||||
// note: don't use `click` because it often reports incorrect `event.target`
|
||||
ownerDocument.addEventListener(EVENT.POINTER_DOWN, onOutsideClick);
|
||||
ownerDocument.addEventListener(EVENT.TOUCH_START, onOutsideClick);
|
||||
|
||||
return () => {
|
||||
ownerDocument.removeEventListener(EVENT.POINTER_DOWN, onOutsideClick);
|
||||
ownerDocument.removeEventListener(EVENT.TOUCH_START, onOutsideClick);
|
||||
};
|
||||
}, [ref, callback, isInside]);
|
||||
}
|
||||
33
packages/excalidraw/hooks/useScrollPosition.ts
Normal file
33
packages/excalidraw/hooks/useScrollPosition.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import throttle from "lodash.throttle";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { atom, useAtom } from "../editor-jotai";
|
||||
|
||||
const scrollPositionAtom = atom<number>(0);
|
||||
|
||||
export const useScrollPosition = <T extends HTMLElement>(
|
||||
elementRef: React.RefObject<T | null>,
|
||||
) => {
|
||||
const [scrollPosition, setScrollPosition] = useAtom(scrollPositionAtom);
|
||||
|
||||
useEffect(() => {
|
||||
const { current: element } = elementRef;
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleScroll = throttle(() => {
|
||||
const { scrollTop } = element;
|
||||
setScrollPosition(scrollTop);
|
||||
}, 200);
|
||||
|
||||
element.addEventListener("scroll", handleScroll);
|
||||
|
||||
return () => {
|
||||
handleScroll.cancel();
|
||||
element.removeEventListener("scroll", handleScroll);
|
||||
};
|
||||
}, [elementRef, setScrollPosition]);
|
||||
|
||||
return scrollPosition;
|
||||
};
|
||||
7
packages/excalidraw/hooks/useStable.ts
Normal file
7
packages/excalidraw/hooks/useStable.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { useRef } from "react";
|
||||
|
||||
export const useStable = <T extends Record<string, any>>(value: T) => {
|
||||
const ref = useRef<T>(value);
|
||||
Object.assign(ref.current, value);
|
||||
return ref.current;
|
||||
};
|
||||
18
packages/excalidraw/hooks/useStableCallback.ts
Normal file
18
packages/excalidraw/hooks/useStableCallback.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { useRef } from "react";
|
||||
|
||||
/**
|
||||
* Returns a stable function of the same type.
|
||||
*/
|
||||
export const useStableCallback = <T extends (...args: any[]) => any>(
|
||||
userFn: T,
|
||||
) => {
|
||||
const stableRef = useRef<{ userFn: T; stableFn?: T }>({ userFn });
|
||||
stableRef.current.userFn = userFn;
|
||||
|
||||
if (!stableRef.current.stableFn) {
|
||||
stableRef.current.stableFn = ((...args: any[]) =>
|
||||
stableRef.current.userFn(...args)) as T;
|
||||
}
|
||||
|
||||
return stableRef.current.stableFn as T;
|
||||
};
|
||||
124
packages/excalidraw/hooks/useTextEditorFocus.ts
Normal file
124
packages/excalidraw/hooks/useTextEditorFocus.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { useState, useCallback } from "react";
|
||||
|
||||
// Utility type for caret position
|
||||
export type CaretPosition = {
|
||||
start: number;
|
||||
end: number;
|
||||
};
|
||||
|
||||
// Utility function to get text editor element
|
||||
const getTextEditor = (ownerDocument: Document): HTMLTextAreaElement | null => {
|
||||
return ownerDocument.querySelector(
|
||||
".excalidraw-wysiwyg",
|
||||
) as HTMLTextAreaElement;
|
||||
};
|
||||
|
||||
// Utility functions for caret position management
|
||||
export const saveCaretPosition = (
|
||||
ownerDocument: Document = document,
|
||||
): CaretPosition | null => {
|
||||
const textEditor = getTextEditor(ownerDocument);
|
||||
if (textEditor) {
|
||||
return {
|
||||
start: textEditor.selectionStart,
|
||||
end: textEditor.selectionEnd,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const restoreCaretPosition = (
|
||||
position: CaretPosition | null,
|
||||
ownerDocument: Document = document,
|
||||
): void => {
|
||||
const ownerWindow = ownerDocument.defaultView ?? window;
|
||||
ownerWindow.setTimeout(() => {
|
||||
const textEditor = getTextEditor(ownerDocument);
|
||||
if (textEditor) {
|
||||
textEditor.focus();
|
||||
if (position) {
|
||||
textEditor.selectionStart = position.start;
|
||||
textEditor.selectionEnd = position.end;
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
};
|
||||
|
||||
export const withCaretPositionPreservation = (
|
||||
callback: () => void,
|
||||
isCompactMode: boolean,
|
||||
isEditingText: boolean,
|
||||
onPreventClose?: () => void,
|
||||
ownerDocument: Document = document,
|
||||
): void => {
|
||||
// Prevent popover from closing in compact mode
|
||||
if (isCompactMode && onPreventClose) {
|
||||
onPreventClose();
|
||||
}
|
||||
|
||||
// Save caret position if editing text
|
||||
const savedPosition =
|
||||
isCompactMode && isEditingText ? saveCaretPosition(ownerDocument) : null;
|
||||
|
||||
// Execute the callback
|
||||
callback();
|
||||
|
||||
// Restore caret position if needed
|
||||
if (isCompactMode && isEditingText) {
|
||||
restoreCaretPosition(savedPosition, ownerDocument);
|
||||
}
|
||||
};
|
||||
|
||||
// Hook for managing text editor caret position with state
|
||||
export const useTextEditorFocus = (ownerDocument: Document = document) => {
|
||||
const [savedCaretPosition, setSavedCaretPosition] =
|
||||
useState<CaretPosition | null>(null);
|
||||
|
||||
const saveCaretPositionToState = useCallback(() => {
|
||||
const position = saveCaretPosition(ownerDocument);
|
||||
setSavedCaretPosition(position);
|
||||
}, [ownerDocument]);
|
||||
|
||||
const restoreCaretPositionFromState = useCallback(() => {
|
||||
const ownerWindow = ownerDocument.defaultView ?? window;
|
||||
ownerWindow.setTimeout(() => {
|
||||
const textEditor = getTextEditor(ownerDocument);
|
||||
if (textEditor) {
|
||||
textEditor.focus();
|
||||
if (savedCaretPosition) {
|
||||
textEditor.selectionStart = savedCaretPosition.start;
|
||||
textEditor.selectionEnd = savedCaretPosition.end;
|
||||
setSavedCaretPosition(null);
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
}, [ownerDocument, savedCaretPosition]);
|
||||
|
||||
const clearSavedPosition = useCallback(() => {
|
||||
setSavedCaretPosition(null);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
saveCaretPosition: saveCaretPositionToState,
|
||||
restoreCaretPosition: restoreCaretPositionFromState,
|
||||
clearSavedPosition,
|
||||
hasSavedPosition: !!savedCaretPosition,
|
||||
};
|
||||
};
|
||||
|
||||
// Utility function to temporarily disable text editor blur
|
||||
export const temporarilyDisableTextEditorBlur = (
|
||||
ownerDocument: Document = document,
|
||||
duration: number = 100,
|
||||
): void => {
|
||||
const textEditor = getTextEditor(ownerDocument);
|
||||
if (textEditor) {
|
||||
const originalOnBlur = textEditor.onblur;
|
||||
textEditor.onblur = null;
|
||||
|
||||
const ownerWindow = ownerDocument.defaultView ?? window;
|
||||
ownerWindow.setTimeout(() => {
|
||||
textEditor.onblur = originalOnBlur;
|
||||
}, duration);
|
||||
}
|
||||
};
|
||||
9
packages/excalidraw/hooks/useTransition.ts
Normal file
9
packages/excalidraw/hooks/useTransition.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import React, { useCallback } from "react";
|
||||
|
||||
/** noop polyfill for v17. Subset of API available */
|
||||
function useTransitionPolyfill() {
|
||||
const startTransition = useCallback((callback: () => void) => callback(), []);
|
||||
return [false, startTransition] as const;
|
||||
}
|
||||
|
||||
export const useTransition = React.useTransition || useTransitionPolyfill;
|
||||
Reference in New Issue
Block a user