0
Fork 0
mirror of https://github.com/fastmail/Squire.git synced 2024-12-22 07:13:08 -05:00
Squire/source/keyboard/Backspace.ts
Neil Jenkins cf30a69b89 Fix backspace can delete two characters
If we're handling the backspace completely ourselves, must prevent
default to stop the browser also having a go.
2023-02-22 11:27:16 +11:00

111 lines
3.8 KiB
TypeScript

import type { Squire } from '../Editor';
import { getPreviousBlock } from '../node/Block';
import {
fixContainer,
mergeContainers,
mergeWithBlock,
} from '../node/MergeSplit';
import { getNearest } from '../node/Node';
import {
getStartBlockOfRange,
rangeDoesStartAtBlockBoundary,
} from '../range/Block';
import { moveRangeBoundariesDownTree } from '../range/Boundaries';
import { deleteContentsOfRange } from '../range/InsertDelete';
import { afterDelete, detachUneditableNode } from './KeyHelpers';
// ---
const Backspace = (self: Squire, event: KeyboardEvent, range: Range): void => {
const root: Element = self._root;
self._removeZWS();
// Record undo checkpoint.
self.saveUndoState(range);
if (!range.collapsed) {
// If not collapsed, delete contents
event.preventDefault();
deleteContentsOfRange(range, root);
afterDelete(self, range);
} else if (rangeDoesStartAtBlockBoundary(range, root)) {
// If at beginning of block, merge with previous
event.preventDefault();
const startBlock = getStartBlockOfRange(range, root);
if (!startBlock) {
return;
}
let current = startBlock;
// In case inline data has somehow got between blocks.
fixContainer(current.parentNode!, root);
// Now get previous block
const previous = getPreviousBlock(current, root);
// Must not be at the very beginning of the text area.
if (previous) {
// If not editable, just delete whole block.
if (!(previous as HTMLElement).isContentEditable) {
detachUneditableNode(previous, root);
return;
}
// Otherwise merge.
mergeWithBlock(previous, current, range, root);
// If deleted line between containers, merge newly adjacent
// containers.
current = previous.parentNode as HTMLElement;
while (current !== root && !current.nextSibling) {
current = current.parentNode as HTMLElement;
}
if (
current !== root &&
(current = current.nextSibling as HTMLElement)
) {
mergeContainers(current, root);
}
self.setSelection(range);
// If at very beginning of text area, allow backspace
// to break lists/blockquote.
} else if (current) {
if (
getNearest(current, root, 'UL') ||
getNearest(current, root, 'OL')
) {
// Break list
self.decreaseListLevel(range);
return;
} else if (getNearest(current, root, 'BLOCKQUOTE')) {
// Break blockquote
self.removeQuote(range);
return;
}
self.setSelection(range);
self._updatePath(range, true);
}
} else {
// If deleting text inside a link that looks like a URL, delink.
// This is to allow you to easily correct auto-linked text.
moveRangeBoundariesDownTree(range);
const text = range.startContainer;
const offset = range.startOffset;
const a = text.parentNode;
if (
text instanceof Text &&
a instanceof HTMLAnchorElement &&
offset &&
a.href.includes(text.data)
) {
text.deleteData(offset - 1, 1);
self.setSelection(range);
self.removeLink();
event.preventDefault();
} else {
// Otherwise, leave to browser but check afterwards whether it has
// left behind an empty inline tag.
self.setSelection(range);
setTimeout(() => {
afterDelete(self);
}, 0);
}
}
};
// ---
export { Backspace };