Skip to content

Commit

Permalink
Caret positioning & scrolling on enter (#247)
Browse files Browse the repository at this point in the history
  • Loading branch information
BartoszGrajdek authored Mar 27, 2024
1 parent 9970698 commit 012ec8b
Show file tree
Hide file tree
Showing 2 changed files with 46 additions and 2 deletions.
2 changes: 2 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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': [
Expand Down
46 changes: 44 additions & 2 deletions src/web/cursorUtils.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -13,13 +15,34 @@ 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);

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;
Expand All @@ -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;
}
Expand Down Expand Up @@ -116,4 +158,4 @@ function scrollCursorIntoView(target: HTMLInputElement) {
}
}

export {getCurrentCursorPosition, moveCursorToEnd, setCursorPosition, removeSelection, scrollCursorIntoView};
export {getCurrentCursorPosition, moveCursorToEnd, setCursorPosition, setPrevText, removeSelection, scrollCursorIntoView};

0 comments on commit 012ec8b

Please sign in to comment.