codevise/pageflow

View on GitHub
entry_types/scrolled/package/src/frontend/inlineEditing/EditableText/Selection.js

Summary

Maintainability
B
6 hrs
Test Coverage
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
    }
  ];
}