mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-12-15 07:33:36 +08:00
171 lines
6.0 KiB
TypeScript
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();
|
||
|
},
|
||
|
);
|
||
|
}
|