0
Fork 0
mirror of https://github.com/fastmail/Squire.git synced 2024-12-22 15:23:29 -05:00
Squire/source/range/Block.ts
Neil Jenkins fe0dfdf6c4 Squire 2.0
This is a massive refactor to port Squire to TypeScript, fix a bunch of small
bugs and modernise our tooling. The development was done on an internal
repository, so apologies to anyone following externally for the commit dump;
updates from here should come as real commits again.

Co-authored-by: Joe Woods <woods@fastmailteam.com>
2023-01-23 13:18:29 +11:00

170 lines
5.3 KiB
TypeScript

import { isInline, isBlock } from '../node/Category';
import { getPreviousBlock, getNextBlock } from '../node/Block';
import { getNodeBeforeOffset, getNodeAfterOffset } from '../node/Node';
import { notWS } from '../Constants';
import { isNodeContainedInRange } from './Boundaries';
import { TreeIterator, SHOW_ELEMENT_OR_TEXT } from '../node/TreeIterator';
// ---
// Returns the first block at least partially contained by the range,
// or null if no block is contained by the range.
const getStartBlockOfRange = (
range: Range,
root: Element | DocumentFragment,
): HTMLElement | null => {
const container = range.startContainer;
let block: HTMLElement | null;
// If inline, get the containing block.
if (isInline(container)) {
block = getPreviousBlock(container, root);
} else if (
container !== root &&
container instanceof HTMLElement &&
isBlock(container)
) {
block = container;
} else {
const node = getNodeBeforeOffset(container, range.startOffset);
block = getNextBlock(node, root);
}
// Check the block actually intersects the range
return block && isNodeContainedInRange(range, block, true) ? block : null;
};
// Returns the last block at least partially contained by the range,
// or null if no block is contained by the range.
const getEndBlockOfRange = (
range: Range,
root: Element | DocumentFragment,
): HTMLElement | null => {
const container = range.endContainer;
let block: HTMLElement | null;
// If inline, get the containing block.
if (isInline(container)) {
block = getPreviousBlock(container, root);
} else if (
container !== root &&
container instanceof HTMLElement &&
isBlock(container)
) {
block = container;
} else {
let node = getNodeAfterOffset(container, range.endOffset);
if (!node || !root.contains(node)) {
node = root;
let child: Node | null;
while ((child = node.lastChild)) {
node = child;
}
}
block = getPreviousBlock(node, root);
}
// Check the block actually intersects the range
return block && isNodeContainedInRange(range, block, true) ? block : null;
};
const isContent = (node: Element | Text): boolean => {
return node instanceof Text
? notWS.test(node.data)
: node.nodeName === 'IMG';
};
const rangeDoesStartAtBlockBoundary = (
range: Range,
root: Element,
): boolean => {
const startContainer = range.startContainer;
const startOffset = range.startOffset;
let nodeAfterCursor: Node | null;
// If in the middle or end of a text node, we're not at the boundary.
if (startContainer instanceof Text) {
if (startOffset) {
return false;
}
nodeAfterCursor = startContainer;
} else {
nodeAfterCursor = getNodeAfterOffset(startContainer, startOffset);
if (nodeAfterCursor && !root.contains(nodeAfterCursor)) {
nodeAfterCursor = null;
}
// The cursor was right at the end of the document
if (!nodeAfterCursor) {
nodeAfterCursor = getNodeBeforeOffset(startContainer, startOffset);
if (nodeAfterCursor instanceof Text && nodeAfterCursor.length) {
return false;
}
}
}
// Otherwise, look for any previous content in the same block.
const block = getStartBlockOfRange(range, root);
if (!block) {
return false;
}
const contentWalker = new TreeIterator<Element | Text>(
block,
SHOW_ELEMENT_OR_TEXT,
isContent,
);
contentWalker.currentNode = nodeAfterCursor;
return !contentWalker.previousNode();
};
const rangeDoesEndAtBlockBoundary = (range: Range, root: Element): boolean => {
const endContainer = range.endContainer;
const endOffset = range.endOffset;
let currentNode: Node;
// If in a text node with content, and not at the end, we're not
// at the boundary
if (endContainer instanceof Text) {
const length = endContainer.data.length;
if (length && endOffset < length) {
return false;
}
currentNode = endContainer;
} else {
currentNode = getNodeBeforeOffset(endContainer, endOffset);
}
// Otherwise, look for any further content in the same block.
const block = getEndBlockOfRange(range, root);
if (!block) {
return false;
}
const contentWalker = new TreeIterator<Element | Text>(
block,
SHOW_ELEMENT_OR_TEXT,
isContent,
);
contentWalker.currentNode = currentNode;
return !contentWalker.nextNode();
};
const expandRangeToBlockBoundaries = (range: Range, root: Element): void => {
const start = getStartBlockOfRange(range, root);
const end = getEndBlockOfRange(range, root);
let parent: Node;
if (start && end) {
parent = start.parentNode!;
range.setStart(parent, Array.from(parent.childNodes).indexOf(start));
parent = end.parentNode!;
range.setEnd(parent, Array.from(parent.childNodes).indexOf(end) + 1);
}
};
// ---
export {
getStartBlockOfRange,
getEndBlockOfRange,
rangeDoesStartAtBlockBoundary,
rangeDoesEndAtBlockBoundary,
expandRangeToBlockBoundaries,
};