UNPKG

@ant-design/x

Version:

Craft AI-driven interfaces effortlessly

436 lines (420 loc) 12.6 kB
import { useCallback } from 'react'; import warning from "../../_util/warning"; const useCursor = options => { const getSelection = useCallback(() => { if (typeof window === 'undefined') { return null; } return window.getSelection(); }, []); const findOuterContainer = (node, editableDom) => { if (!node || !editableDom) { return editableDom; } let currentNode = node; let lastSpan = null; // 如果当前节点是文本节点,从父节点开始 if (currentNode.nodeType === Node.TEXT_NODE) { currentNode = currentNode.parentElement; } // 向上遍历DOM树,找到最外层的span元素 while (currentNode && currentNode !== editableDom) { if (currentNode instanceof HTMLElement && currentNode.tagName === 'SPAN') { lastSpan = currentNode; } currentNode = currentNode.parentElement; } return lastSpan || editableDom; }; const getRange = useCallback(() => { const selection = getSelection(); if (!selection) { return { range: null, selection }; } try { const range = selection.getRangeAt(0) || document.createRange(); return { range, selection }; } catch (_error) { // 当没有选中范围时创建新的 Range const range = document.createRange(); return { range, selection }; } }, [getSelection]); const setRange = useCallback((range, selection) => { if (!range || !selection) { return; } try { selection.removeAllRanges(); selection.addRange(range); } catch (error) { warning(false, 'Sender', `Failed to set range: ${error}`); } }, []); const removeAllRanges = useCallback(() => { const selection = getSelection(); if (!selection) { return; } try { selection.removeAllRanges(); } catch (error) { warning(false, 'Sender', `Failed to remove all ranges: ${error}`); } }, [getSelection]); const focus = useCallback((targetNode, preventScroll = false) => { if (!targetNode || typeof targetNode.focus !== 'function') { return; } try { targetNode.focus({ preventScroll }); } catch (error) { warning(false, 'Sender', `Failed to focus element: ${error}`); } }, []); const setEndCursor = useCallback((targetNode, preventScroll) => { if (!targetNode) { return; } focus(targetNode, preventScroll); const { range, selection } = getRange(); if (range && selection) { try { range.selectNodeContents(targetNode); range.collapse(false); setRange(range, selection); } catch (error) { warning(false, 'Sender', `Failed to set end cursor: ${error}`); } } }, [focus, getRange, setRange]); const setStartCursor = useCallback((targetNode, preventScroll) => { if (!targetNode) { return; } focus(targetNode, preventScroll); const { range, selection } = getRange(); if (range && selection) { try { range.selectNodeContents(targetNode); range.collapse(true); setRange(range, selection); } catch (error) { warning(false, 'Sender', `Failed to set start cursor: ${error}`); } } }, [focus, getRange, setRange]); const setAllSelectCursor = useCallback((targetNode, skillDom, preventScroll) => { if (!targetNode) { return; } focus(targetNode, preventScroll); const { range, selection } = getRange(); if (range && selection) { try { range.selectNodeContents(targetNode); if (skillDom) { range.setStart(targetNode, 1); } setRange(range, selection); } catch (error) { warning(false, 'Sender', `Failed to select all content: ${error}`); } } }, [focus, getRange, setRange]); const setCursorPosition = useCallback((targetNode, editableNode, position, preventScroll) => { if (!targetNode || typeof position !== 'number' || position < 0 || !editableNode) { return { range: null, selection: null }; } focus(editableNode, preventScroll); const { range, selection } = getRange(); if (range && selection) { try { const maxPosition = Math.min(position, targetNode.childNodes.length); range.setStart(targetNode, maxPosition); range.setEnd(targetNode, maxPosition); range.collapse(false); setRange(range, selection); } catch (error) { warning(false, 'Sender', `Failed to set cursor position:: ${error}`); } } return { range: range, selection: selection }; }, [focus, getRange, setRange]); const setSlotFocus = useCallback((editableRef, key, preventScroll = false) => { if (!options || !editableRef?.current) return; const { getSlotDom, slotConfigMap } = options; const getFocusableElement = slotKey => { const slotDom = getSlotDom(slotKey); if (!slotDom) return null; const slotConfig = slotConfigMap.get(slotKey); if (!slotConfig) return null; // 处理 input 类型的 slot if (slotConfig.type === 'input') { return slotDom.querySelector('input'); } // 处理 content 类型的 slot(排除占位符节点) const nodeType = slotDom?.getAttribute?.('data-node-type') || ''; if (slotConfig.type === 'content' && nodeType !== 'nbsp') { return slotDom; } return null; }; const findFocusableSlot = targetKey => { const editor = editableRef.current; if (!editor) return null; // 如果指定了 key,直接查找对应的 slot if (targetKey) { return getFocusableElement(targetKey); } // 否则查找第一个可聚焦的 slot for (const node of Array.from(editor.childNodes)) { const slotKey = node?.getAttribute?.('data-slot-key'); if (slotKey) { const focusableElement = getFocusableElement(slotKey); if (focusableElement) { return focusableElement; } } } return null; }; const targetElement = findFocusableSlot(key); if (!targetElement) return; if (targetElement.nodeName === 'INPUT') { targetElement.focus({ preventScroll }); } else { setCursorPosition(targetElement, editableRef.current, 0, preventScroll); } }, [options, setCursorPosition]); const setAfterNodeFocus = useCallback((targetNode, editableNode, range, selection, preventScroll) => { if (!range || !selection) return; focus(editableNode, preventScroll); range?.setStartAfter(targetNode); range.collapse(false); selection.removeAllRanges(); selection.addRange(range); }, [focus]); const getTextBeforeCursor = useCallback(targetNode => { // 快速路径:空节点直接返回 if (!targetNode) { return { value: '', startContainer: null, startOffset: 0 }; } const selection = getSelection(); if (!selection || selection.rangeCount === 0) { return { value: '', startContainer: null, startOffset: 0 }; } try { const range = selection.getRangeAt(0); // 验证光标位置是否在目标节点内 if (!targetNode.contains(range.startContainer)) { return { value: '', startContainer: null, startOffset: 0 }; } const clone = range.cloneRange(); clone.selectNodeContents(targetNode); clone.setEnd(range.startContainer, range.startOffset); // 清理并返回结果 const value = clone.toString().replace(/\u200B/g, ''); // 移除零宽空格 return { value, startContainer: range.startContainer, startOffset: range.startOffset }; } catch (error) { warning(false, 'Sender', `Failed to get text before cursor: ${error}`); return { value: '', startContainer: null, startOffset: 0 }; } }, [getSelection]); /** * 获取插入位置信息 * @param position - 插入位置类型:'cursor' | 'end' | 'start' * @returns 包含插入类型和对应 range 的对象 */ const getInsertPosition = useCallback((position, editableRef, lastSelectionRef) => { if (position === 'start' || position === 'end') { return { type: position, selection: getSelection() }; } let range = null; let selection = null; if (lastSelectionRef?.current) { range = lastSelectionRef.current; selection = getSelection(); } else { const rangeResult = getRange(); range = rangeResult.range; selection = rangeResult.selection; } if (!range || !selection) { return { type: 'end', selection }; } const editableDom = editableRef?.current; if (!editableDom) { return { type: 'end', selection }; } // 检查是否在可编辑区域内,如果不在则设置对应的光标位置 const isEndInEditableBox = editableDom.contains(range.endContainer); const isStartInEditableBox = editableDom.contains(range.startContainer); if (!isEndInEditableBox) { setEndCursor(editableDom, true); return { type: 'end', selection }; } if (!isStartInEditableBox) { setStartCursor(editableDom, true); return { type: 'start', selection }; } // 获取容器信息 const endContainer = findOuterContainer(range.endContainer, editableDom); const startContainer = findOuterContainer(range.startContainer, editableDom); // 检查是否是 slot 类型 if (endContainer === startContainer && startContainer !== editableDom && options?.getNodeInfo) { const { slotKey, slotConfig, skillKey } = options.getNodeInfo(endContainer) || {}; if (slotKey) { return { type: 'slot', slotKey: slotConfig?.key, slotType: slotConfig?.type, range, selection }; } if (skillKey) { return { type: 'start', selection }; } } // 在可编辑区域内但不是 slot,返回 box return { type: 'box', range, selection }; }, [options, getRange, getSelection, setEndCursor, setStartCursor, findOuterContainer]); /** * 获取末尾插入范围 */ const getEndRange = useCallback(editableDom => { const lastNode = editableDom.childNodes[editableDom.childNodes.length - 1]; const targetIndex = lastNode?.nodeType === Node.TEXT_NODE && lastNode.textContent === '\n' ? editableDom.childNodes.length - 1 : editableDom.childNodes.length; const result = setCursorPosition(editableDom, editableDom, targetIndex); return result.range || document.createRange(); }, [setCursorPosition]); /** * 获取开头插入范围 */ const getStartRange = useCallback(editableDom => { const startIndex = options?.getEditorValue?.().skill ? 1 : 0; const result = setCursorPosition(editableDom, editableDom, startIndex); return result.range || document.createRange(); }, [setCursorPosition, options]); const getCleanedText = useCallback(ori => { return ori.replace(/\u200B/g, '') // 移除零宽空格 .replace(/\n/g, '').replace(/^\n+|\n+$/g, ''); // 移除开头和结尾的换行 }, []); const copySelectionString = useCallback(async () => { try { const selection = getSelection(); if (!selection) { return false; } const selectingString = selection.toString(); if (!selectingString) { return false; } const cleanedText = getCleanedText(selectingString); await navigator.clipboard.writeText(cleanedText); return true; } catch (error) { warning(false, 'Sender', `Failed to copy selection: ${error}`); return false; } }, [getSelection, getCleanedText]); return { setEndCursor, setStartCursor, setAllSelectCursor, setCursorPosition, setAfterNodeFocus, setSlotFocus, getTextBeforeCursor, getSelection, removeAllRanges, getRange, getInsertPosition, getEndRange, getStartRange, copySelectionString, getCleanedText }; }; export default useCursor;