0
Fork 0
mirror of https://github.com/fastmail/Squire.git synced 2024-12-22 07:13:08 -05:00
Squire/source/keyboard/KeyHelpers.ts
Neil Jenkins cbde45311a Fix crash linkifying text in middle of node
We were looking at the selection properties after we had mutated the DOM, and
trying to manipulate them based on some numbers cached from before mutating
the DOM, which could result in trying to set a negative index for the selection
offset. Instead, calculate all our values before we do any mutations.

Fixes #430
2023-04-20 10:57:05 +10:00

140 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { ZWS } from '../Constants';
import { getPreviousBlock } from '../node/Block';
import { isInline, isBlock } from '../node/Category';
import { fixCursor } from '../node/MergeSplit';
import { createElement, detach, getNearest } from '../node/Node';
import { moveRangeBoundariesDownTree } from '../range/Boundaries';
import type { Squire } from '../Editor';
// ---
// If you delete the content inside a span with a font styling, Webkit will
// replace it with a <font> tag (!). If you delete all the text inside a
// link in Opera, it won't delete the link. Let's make things consistent. If
// you delete all text inside an inline tag, remove the inline tag.
const afterDelete = (self: Squire, range?: Range): void => {
try {
if (!range) {
range = self.getSelection();
}
let node = range!.startContainer;
// Climb the tree from the focus point while we are inside an empty
// inline element
if (node instanceof Text) {
node = node.parentNode!;
}
let parent = node;
while (
isInline(parent) &&
(!parent.textContent || parent.textContent === ZWS)
) {
node = parent;
parent = node.parentNode!;
}
// If focused in empty inline element
if (node !== parent) {
// Move focus to just before empty inline(s)
range!.setStart(
parent,
Array.from(parent.childNodes as NodeListOf<Node>).indexOf(node),
);
range!.collapse(true);
// Remove empty inline(s)
parent.removeChild(node);
// Fix cursor in block
if (!isBlock(parent)) {
parent = getPreviousBlock(parent, self._root) || self._root;
}
fixCursor(parent);
// Move cursor into text node
moveRangeBoundariesDownTree(range!);
}
// If you delete the last character in the sole <div> in Chrome,
// it removes the div and replaces it with just a <br> inside the
// root. Detach the <br>; the _ensureBottomLine call will insert a new
// block.
if (
node === self._root &&
(node = node.firstChild!) &&
node.nodeName === 'BR'
) {
detach(node);
}
self._ensureBottomLine();
self.setSelection(range);
self._updatePath(range, true);
} catch (error) {
self._config.didError(error);
}
};
const detachUneditableNode = (node: Node, root: Element): void => {
let parent: Node | null;
while ((parent = node.parentNode)) {
if (parent === root || (parent as HTMLElement).isContentEditable) {
break;
}
node = parent;
}
detach(node);
};
// ---
const linkifyText = (self: Squire, textNode: Text, offset: number): void => {
if (getNearest(textNode, self._root, 'A')) {
return;
}
const data = textNode.data || '';
const searchFrom =
Math.max(
data.lastIndexOf(' ', offset - 1),
data.lastIndexOf(' ', offset - 1),
) + 1;
const searchText = data.slice(searchFrom, offset);
const match = self.linkRegExp.exec(searchText);
if (match) {
// Record an undo point
const selection = self.getSelection();
self._docWasChanged();
self._recordUndoState(selection);
self._getRangeAndRemoveBookmark(selection);
const index = searchFrom + match.index;
const endIndex = index + match[0].length;
const needsSelectionUpdate = selection.startContainer === textNode;
const newSelectionOffset = selection.startOffset - endIndex;
if (index) {
textNode = textNode.splitText(index);
}
const defaultAttributes = self._config.tagAttributes.a;
const link = createElement(
'A',
Object.assign(
{
href: match[1]
? /^(?:ht|f)tps?:/i.test(match[1])
? match[1]
: 'http://' + match[1]
: 'mailto:' + match[0],
},
defaultAttributes,
),
);
link.textContent = data.slice(index, endIndex);
textNode.parentNode!.insertBefore(link, textNode);
textNode.data = data.slice(endIndex);
if (needsSelectionUpdate) {
selection.setStart(textNode, newSelectionOffset);
selection.setEnd(textNode, newSelectionOffset);
}
self.setSelection(selection);
}
};
// ---
export { afterDelete, detachUneditableNode, linkifyText };