codevise/pageflow

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

Summary

Maintainability
A
25 mins
Test Coverage
import {useRef, useCallback} from 'react';
import {usePostMessageListener} from '../usePostMessageListener';

// Scroll points are used to preserve scroll position when toggling
// the editor phone preview. Each ContentElementDecorator renders a
// `data-scrollpoint` attribute with a unique value on its wrapper
// div. Before toggling the phone preview mode, the `EntryPreviewView`
// sends a `SAVE_SCROLL_POINT` message. `getCurrentScrollPoint` looks
// through all DOM elements with `data-scrollpoint` attributes and
// stores the unique id of the element with the smallest non-negative
// y coordinate in the viewport (i.e. the first content element inside
// the viewport). `ScrollPointMessageHandler` responds with a
// `SAVED_SCROLL_POINT` message which makes `EntryPreviewView` toggle
// the preview mode. Once the preview has been resized,
// `EntryPreviewView` sends a `RESTORE_SCROLL_POINT`
// message. `restoreScrollPoint` looks up the new position of the
// element with the saved scroll point and scrolls it into view.
//
// When a content element is selected, we want to keep that element
// in the viewport instead. The `SelectionRect` therefore renders a
// `data-scrollpoint=selection` attribute. If an element with such an
// attribute is present, `getCurrentScrollPoint` prefers it over all
// other scroll points. Since text block elements render a selection
// rect around the current paragraph, scroll position is also
// preserved correctly inside long text blocks.

export function ScrollPointMessageHandler() {
  const scrollPoint = useRef();

  const receiveMessage = useCallback(data => {
    if (data.type === 'SAVE_SCROLL_POINT') {
      scrollPoint.current = getCurrentScrollPoint();
      window.parent.postMessage({type: 'SAVED_SCROLL_POINT'}, window.location.origin);
    }
    else if (data.type === 'RESTORE_SCROLL_POINT') {
      if (scrollPoint.current) {
        restoreScrollPoint(scrollPoint.current);
      }
    }
  }, []);

  usePostMessageListener(receiveMessage);

  return null;
}

function getCurrentScrollPoint() {
  let scrollPointElement =
    getSelectionScrollPointElement() ||
    getScrollPointElementWithMinimumTopPositionInViewport();

  return scrollPointElement?.getAttribute('data-scrollpoint');
}

function getSelectionScrollPointElement() {
  return document.querySelector('[data-scrollpoint=selection]');
}

function getScrollPointElementWithMinimumTopPositionInViewport() {
  let minTop = Infinity;
  let scrollPointElement;

  const scrollPoints = document.querySelectorAll('[data-scrollpoint]');

  for (let i = 0; i < scrollPoints.length; i++) {
    const rect = scrollPoints[i].getBoundingClientRect();

    if (rect.top > 0 && rect.top < minTop) {
      minTop = rect.top;
      scrollPointElement = scrollPoints[i];
    }
  }

  return scrollPointElement
}

function restoreScrollPoint(name) {
  let element = document.querySelector(`[data-scrollpoint="${name}"]`);

  if (element) {
    window.scrollTo({
      top: element.getBoundingClientRect().top + window.scrollY - 100,
      behavior: 'smooth'
    });
  }
}