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 2d37608aac Ignore ZWS when determining if at block boundary
These are just added to allow Chrome to focus an empty text node, but
they should not be considered "real" content.
2023-09-27 14:51:26 +10:00

176 lines
5.5 KiB
TypeScript

import { isInline, isBlock } from '../node/Category';
import { getPreviousBlock, getNextBlock } from '../node/Block';
import { getNodeBeforeOffset, getNodeAfterOffset } from '../node/Node';
import { ZWS, 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) {
const text = startContainer.data;
for (let i = startOffset; i > 0; i -= 1) {
if (text.charAt(i - 1) !== ZWS) {
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. Ignore ZWS.
if (endContainer instanceof Text) {
const text = endContainer.data;
const length = text.length;
for (let i = endOffset; i < length; i += 1) {
if (text.charAt(i) !== ZWS) {
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,
};