diff --git a/.eslintrc.js b/.eslintrc.js index ef20f0bc..c15cf36b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -45,6 +45,8 @@ module.exports = { 'import/no-unresolved': 'error', 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], 'no-use-before-define': 'off', + 'es/no-nullish-coalescing-operators': 'off', + 'es/no-optional-chaining': 'off', '@typescript-eslint/no-use-before-define': 'off', // TODO consider enabling this (currently it reports styles defined at the bottom of the file) '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], '@typescript-eslint/consistent-type-imports': [ diff --git a/src/web/cursorUtils.ts b/src/web/cursorUtils.ts index b93c9c18..4cc103e4 100644 --- a/src/web/cursorUtils.ts +++ b/src/web/cursorUtils.ts @@ -1,5 +1,7 @@ import * as BrowserUtils from './browserUtils'; +let prevTextLength: number | undefined; + function findTextNodes(textNodes: Text[], node: ChildNode) { if (node.nodeType === Node.TEXT_NODE) { textNodes.push(node as Text); @@ -13,6 +15,18 @@ function findTextNodes(textNodes: Text[], node: ChildNode) { } } +function setPrevText(target: HTMLElement) { + let text = []; + const textNodes: Text[] = []; + findTextNodes(textNodes, target); + text = textNodes + .map((e) => e.nodeValue ?? '') + ?.join('') + ?.split(''); + + prevTextLength = text.length; +} + function setCursorPosition(target: HTMLElement, start: number, end: number | null = null) { const range = document.createRange(); range.selectNodeContents(target); @@ -20,6 +34,15 @@ function setCursorPosition(target: HTMLElement, start: number, end: number | nul const textNodes: Text[] = []; findTextNodes(textNodes, target); + // These are utilities for handling the boundary cases (especially onEnter) + // prevChar & nextChar are characters before & after the target cursor position + const textCharacters = textNodes + .map((e) => e.nodeValue ?? '') + ?.join('') + ?.split(''); + const prevChar = textCharacters?.[start - 1] ?? ''; + const nextChar = textCharacters?.[start] ?? ''; + let charCount = 0; let startNode: Text | null = null; let endNode: Text | null = null; @@ -31,7 +54,26 @@ function setCursorPosition(target: HTMLElement, start: number, end: number | nul if (!startNode && start >= charCount && (start <= nextCharCount || (start === nextCharCount && i < n - 1))) { startNode = textNode; - range.setStart(textNode, start - charCount); + + // There are 4(/5) cases to consider here: + // 1. Caret in front of a character, when pressing enter + // 2. Caret at the end of a line (not last one) + // 3a. Caret at the end of whole input, when pressing enter - On firefox + // 3b. Caret at the end of whole input, when pressing enter - On other browsers + // 4. All other placements + if (prevChar === '\n' && prevTextLength !== undefined && prevTextLength < textCharacters.length) { + if (nextChar !== '\n') { + range.setStart(textNodes[i + 1] as Node, 0); + } else if (i !== textNodes.length - 1) { + range.setStart(textNodes[i] as Node, 1); + } else if (BrowserUtils.isFirefox) { + range.setStart(textNode, start - charCount); + } else { + range.setStart(textNodes[i] as Node, 2); + } + } else { + range.setStart(textNode, start - charCount); + } if (!end) { break; } @@ -116,4 +158,4 @@ function scrollCursorIntoView(target: HTMLInputElement) { } } -export {getCurrentCursorPosition, moveCursorToEnd, setCursorPosition, removeSelection, scrollCursorIntoView}; +export {getCurrentCursorPosition, moveCursorToEnd, setCursorPosition, setPrevText, removeSelection, scrollCursorIntoView};