import { RawCommands, FocusPosition } from '../types' import isTextSelection from '../helpers/isTextSelection' import isiOS from '../utilities/isiOS' import resolveFocusPosition from '../helpers/resolveFocusPosition' declare module '@tiptap/core' { interface Commands { focus: { /** * Focus the editor at the given position. */ focus: ( position?: FocusPosition, options?: { scrollIntoView?: boolean, }, ) => ReturnType, } } } export const focus: RawCommands['focus'] = (position = null, options) => ({ editor, view, tr, dispatch, }) => { options = { scrollIntoView: true, ...options, } const delayedFocus = () => { // focus within `requestAnimationFrame` breaks focus on iOS // so we have to call this if (isiOS()) { (view.dom as HTMLElement).focus() } // For React we have to focus asynchronously. Otherwise wild things happen. // see: https://github.com/ueberdosis/tiptap/issues/1520 requestAnimationFrame(() => { if (!editor.isDestroyed) { view.focus() if (options?.scrollIntoView) { editor.commands.scrollIntoView() } } }) } if ((view.hasFocus() && position === null) || position === false) { return true } // we don’t try to resolve a NodeSelection or CellSelection if (dispatch && position === null && !isTextSelection(editor.state.selection)) { delayedFocus() return true } const selection = resolveFocusPosition(editor.state.doc, position) || editor.state.selection const isSameSelection = editor.state.selection.eq(selection) if (dispatch) { if (!isSameSelection) { tr.setSelection(selection) } // `tr.setSelection` resets the stored marks // so we’ll restore them if the selection is the same as before if (isSameSelection && tr.storedMarks) { tr.setStoredMarks(tr.storedMarks) } delayedFocus() } return true }