entry_types/scrolled/package/src/frontend/inlineEditing/EditableText/Selection.js
import React, {useEffect, useRef} from 'react';
import {Editor, Transforms, Range, Path, Node} from 'slate';
import {useSlate, ReactEditor} from 'slate-react';
import {useDrag} from 'react-dnd';
import styles from './index.module.css';
import {SelectionRect} from '../SelectionRect';
import {useContentElementEditorState} from '../../useContentElementEditorState';
import {useI18n} from '../../i18n';
import {postInsertContentElementMessage} from '../postMessage';
import {getUniformSelectedNode} from './getUniformSelectedNode';
import {toggleBlock, isBlockActive} from './blocks';
import TextIcon from '../images/text.svg';
import HeadingIcon from '../images/heading.svg';
import OlIcon from '../images/listOl.svg';
import UlIcon from '../images/listUl.svg';
import QuoteIcon from '../images/quote.svg';
export function Selection(props) {
const editor = useSlate();
const {t} = useI18n({locale: 'ui'});
const ref = useRef()
const outerRef = useRef()
const innerRef = useRef()
const boundsRef = useRef();
const lastRangeRef = useRef();
const {
setTransientState,
select,
isSelected: isContentElementSelected,
range
} = useContentElementEditorState();
useEffect(() => {
const {selection} = editor;
if (!ref.current) {
return
}
if (isContentElementSelected && range && lastRangeRef.current !== range) {
lastRangeRef.current = range;
if (range[1] === range[0] + 1) {
Transforms.select(editor,
Editor.point(editor, [range[0]], {edge: 'start'}));
}
else {
Transforms.select(editor, {
anchor: Editor.point(editor, [range[0]], {edge: 'start'}),
focus: Editor.point(editor, [range[1] - 1], {edge: 'end'}),
});
}
ReactEditor.focus(editor);
}
if (!selection) {
if (boundsRef.current) {
hideRect(ref.current);
boundsRef.current = null;
}
return;
}
if (!isContentElementSelected && boundsRef.current) {
hideRect(ref.current);
boundsRef.current = null;
window.getSelection().removeAllRanges();
return;
}
if (!isContentElementSelected && !boundsRef.current) {
select();
}
const [start, end] = computeBounds(editor);
setTransientState({
editableTextIsSingleBlock: editor.children.length <= 1,
exampleNode: getUniformSelectedNode(editor, 'type'),
typographyVariant: getUniformSelectedNode(editor, 'variant')?.variant,
color: getUniformSelectedNode(editor, 'color')?.color
});
boundsRef.current = {start, end};
updateRect(editor, start, end, outerRef.current, ref.current, innerRef.current);
});
const [, drag] = useDrag({
item: {type: 'contentElement', id: props.contentElementId},
begin: () => ({
type: 'contentElement',
id: props.contentElementId,
range: [
boundsRef.current.start,
boundsRef.current.end + 1
]
})
});
return (
<div ref={outerRef}>
<div ref={ref} className={styles.selection}>
<SelectionRect selected={true}
drag={drag}
scrollPoint={isContentElementSelected}
insertButtonTitles={t('pageflow_scrolled.inline_editing.insert_content_element')}
onInsertButtonClick={at => {
if ((at === 'before' &&boundsRef.current.start === 0) ||
(at === 'after' && !Node.has(editor, [boundsRef.current.end + 1]))) {
postInsertContentElementMessage({
id: props.contentElementId,
at
});
}
else {
postInsertContentElementMessage({
id: props.contentElementId,
at: 'split',
splitPoint: at === 'before' ?
boundsRef.current.start :
boundsRef.current.end + 1
});
}
}}
toolbarButtons={toolbarButtons(t).map(button => ({
...button,
active: isBlockActive(editor, button.name)
}))}
onToolbarButtonClick={name => toggleBlock(editor, name)}>
<div ref={innerRef} />
</SelectionRect>
</div>
</div>
);
}
function computeBounds(editor) {
const startPoint = Range.start(editor.selection);
const endPoint = Range.end(editor.selection);
const startPath = startPoint.path.slice(0, 1);
let endPath = endPoint.path.slice(0, 1);
if (!Path.equals(startPath, endPath) && endPoint.offset === 0) {
endPath = Path.previous(endPath);
}
return [startPath[0], endPath[0]];
}
function hideRect(el) {
el.removeAttribute('style');
}
function updateRect(editor, startIndex, endIndex, outer, el, inner) {
const [startDOMNode, endDOMNode] = getDOMNodes(editor, startIndex, endIndex);
if (startDOMNode && endDOMNode) {
const startRect = startDOMNode.getBoundingClientRect()
const endRect = endDOMNode.getBoundingClientRect()
const outerRect = outer.getBoundingClientRect()
el.style.display = 'block';
el.style.top = `${startRect.top - outerRect.top}px`
inner.style.height = `${endRect.bottom - startRect.top}px`
}
}
function getDOMNodes(editor, startIndex, endIndex) {
const startNode = Node.get(editor, [startIndex]);
const endNode = Node.get(editor, [endIndex]);
try {
const startDOMNode = ReactEditor.toDOMNode(editor, startNode);
const endDOMNode = ReactEditor.toDOMNode(editor, endNode);
return [startDOMNode, endDOMNode];
}
catch(e) {
return [];
}
}
function toolbarButtons(t) {
return [
{
name: 'paragraph',
text: t('pageflow_scrolled.inline_editing.formats.paragraph'),
icon: TextIcon
},
{
name: 'heading',
text: t('pageflow_scrolled.inline_editing.formats.heading'),
icon: HeadingIcon
},
{
name: 'numbered-list',
text: t('pageflow_scrolled.inline_editing.formats.ordered_list'),
icon: OlIcon
},
{
name: 'bulleted-list',
text: t('pageflow_scrolled.inline_editing.formats.bulleted_list'),
icon: UlIcon
},
{
name: 'block-quote',
text: t('pageflow_scrolled.inline_editing.formats.block_quote'),
icon: QuoteIcon
}
];
}