Files
upstream-excalidraw/packages/excalidraw/hooks/useTextEditorFocus.ts
2026-09-16 05:31:04 +05:00

125 lines
3.5 KiB
TypeScript

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);
}
};