BookStack/resources/js/wysiwyg/lexical/utils/markSelection.ts
Dan Brown 22d078b47f
Lexical: Imported core lexical libs
Imported at 0.17.1, Modified to work in-app.
Added & configured test dependancies.
Tests need to be altered to avoid using non-included deps including
react dependancies.
2024-09-18 13:43:39 +01:00

171 lines
6.0 KiB
TypeScript

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import {
$getSelection,
$isRangeSelection,
type EditorState,
ElementNode,
type LexicalEditor,
TextNode,
} from 'lexical';
import invariant from 'lexical/shared/invariant';
import mergeRegister from './mergeRegister';
import positionNodeOnRange from './positionNodeOnRange';
import px from './px';
export default function markSelection(
editor: LexicalEditor,
onReposition?: (node: Array<HTMLElement>) => void,
): () => void {
let previousAnchorNode: null | TextNode | ElementNode = null;
let previousAnchorOffset: null | number = null;
let previousFocusNode: null | TextNode | ElementNode = null;
let previousFocusOffset: null | number = null;
let removeRangeListener: () => void = () => {};
function compute(editorState: EditorState) {
editorState.read(() => {
const selection = $getSelection();
if (!$isRangeSelection(selection)) {
// TODO
previousAnchorNode = null;
previousAnchorOffset = null;
previousFocusNode = null;
previousFocusOffset = null;
removeRangeListener();
removeRangeListener = () => {};
return;
}
const {anchor, focus} = selection;
const currentAnchorNode = anchor.getNode();
const currentAnchorNodeKey = currentAnchorNode.getKey();
const currentAnchorOffset = anchor.offset;
const currentFocusNode = focus.getNode();
const currentFocusNodeKey = currentFocusNode.getKey();
const currentFocusOffset = focus.offset;
const currentAnchorNodeDOM = editor.getElementByKey(currentAnchorNodeKey);
const currentFocusNodeDOM = editor.getElementByKey(currentFocusNodeKey);
const differentAnchorDOM =
previousAnchorNode === null ||
currentAnchorNodeDOM === null ||
currentAnchorOffset !== previousAnchorOffset ||
currentAnchorNodeKey !== previousAnchorNode.getKey() ||
(currentAnchorNode !== previousAnchorNode &&
(!(previousAnchorNode instanceof TextNode) ||
currentAnchorNode.updateDOM(
previousAnchorNode,
currentAnchorNodeDOM,
editor._config,
)));
const differentFocusDOM =
previousFocusNode === null ||
currentFocusNodeDOM === null ||
currentFocusOffset !== previousFocusOffset ||
currentFocusNodeKey !== previousFocusNode.getKey() ||
(currentFocusNode !== previousFocusNode &&
(!(previousFocusNode instanceof TextNode) ||
currentFocusNode.updateDOM(
previousFocusNode,
currentFocusNodeDOM,
editor._config,
)));
if (differentAnchorDOM || differentFocusDOM) {
const anchorHTMLElement = editor.getElementByKey(
anchor.getNode().getKey(),
);
const focusHTMLElement = editor.getElementByKey(
focus.getNode().getKey(),
);
// TODO handle selection beyond the common TextNode
if (
anchorHTMLElement !== null &&
focusHTMLElement !== null &&
anchorHTMLElement.tagName === 'SPAN' &&
focusHTMLElement.tagName === 'SPAN'
) {
const range = document.createRange();
let firstHTMLElement;
let firstOffset;
let lastHTMLElement;
let lastOffset;
if (focus.isBefore(anchor)) {
firstHTMLElement = focusHTMLElement;
firstOffset = focus.offset;
lastHTMLElement = anchorHTMLElement;
lastOffset = anchor.offset;
} else {
firstHTMLElement = anchorHTMLElement;
firstOffset = anchor.offset;
lastHTMLElement = focusHTMLElement;
lastOffset = focus.offset;
}
const firstTextNode = firstHTMLElement.firstChild;
invariant(
firstTextNode !== null,
'Expected text node to be first child of span',
);
const lastTextNode = lastHTMLElement.firstChild;
invariant(
lastTextNode !== null,
'Expected text node to be first child of span',
);
range.setStart(firstTextNode, firstOffset);
range.setEnd(lastTextNode, lastOffset);
removeRangeListener();
removeRangeListener = positionNodeOnRange(
editor,
range,
(domNodes) => {
for (const domNode of domNodes) {
const domNodeStyle = domNode.style;
if (domNodeStyle.background !== 'Highlight') {
domNodeStyle.background = 'Highlight';
}
if (domNodeStyle.color !== 'HighlightText') {
domNodeStyle.color = 'HighlightText';
}
if (domNodeStyle.zIndex !== '-1') {
domNodeStyle.zIndex = '-1';
}
if (domNodeStyle.pointerEvents !== 'none') {
domNodeStyle.pointerEvents = 'none';
}
if (domNodeStyle.marginTop !== px(-1.5)) {
domNodeStyle.marginTop = px(-1.5);
}
if (domNodeStyle.paddingTop !== px(4)) {
domNodeStyle.paddingTop = px(4);
}
if (domNodeStyle.paddingBottom !== px(0)) {
domNodeStyle.paddingBottom = px(0);
}
}
if (onReposition !== undefined) {
onReposition(domNodes);
}
},
);
}
}
previousAnchorNode = currentAnchorNode;
previousAnchorOffset = currentAnchorOffset;
previousFocusNode = currentFocusNode;
previousFocusOffset = currentFocusOffset;
});
}
compute(editor.getEditorState());
return mergeRegister(
editor.registerUpdateListener(({editorState}) => compute(editorState)),
removeRangeListener,
() => {
removeRangeListener();
},
);
}