mirror of
https://github.com/fastmail/Squire.git
synced 2024-12-22 07:13:08 -05:00
cbde45311a
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
140 lines
4.6 KiB
TypeScript
140 lines
4.6 KiB
TypeScript
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 };
|