codevise/pageflow

View on GitHub
entry_types/scrolled/package/src/frontend/layouts/TwoColumn.js

Summary

Maintainability
A
3 hrs
Test Coverage
import React from 'react';
import classNames from 'classnames';

import {api} from '../api';
import {ContentElements} from '../ContentElements';
import useMediaQuery from '../useMediaQuery';
import {useTheme} from '../../entryState';
import {widths, widthName} from './widths';

import styles from './TwoColumn.module.css';

export function TwoColumn(props) {
  const shouldInline = useShouldInlineSticky();

  return (
    <div className={classNames(styles.root, styles[props.align])}>
      <div className={classNames(styles.group)} key={props.align}>
        <div className={classNames(styles.box, styles.inline)}
             ref={props.contentAreaRef}
             {...TwoColumn.contentAreaProbeProps} />
      </div>
      {renderItems(props, shouldInline)}
      {renderPlaceholder(props.placeholder)}
    </div>
  );
}

TwoColumn.defaultProps = {
  align: 'left'
}

function useShouldInlineSticky() {
  const theme = useTheme();
  const root = theme.options.properties?.root || {};

  const shouldInline = {
    [widths.md]: useMediaQuery(`(max-width: ${root.twoColumnStickyBreakpoint || '950px'})`),
    [widths.lg]: useMediaQuery(`(max-width: ${root.twoColumnStickyLgBreakpoint || '1200px'})`),
    [widths.xl]: useMediaQuery(`(max-width: ${root.twoColumnStickyXlBreakpoint || '1400px'})`)
  };

  return function(width) {
    return width <= widths.md ? shouldInline[widths.md] : shouldInline[width];
  }
}

// Used in tests to render markers around groups
TwoColumn.GroupComponent = 'div';

// Used to set data-testids on probe element
TwoColumn.contentAreaProbeProps = {};

function renderItems(props, shouldInline) {
  return groupItemsByPosition(props.items, shouldInline).map((group, index) =>
    <TwoColumn.GroupComponent key={index}
                              className={classNames(styles.group,
                                                    styles[`group-${widthName(group.width)}`])}>
      {group.boxes.map((box, index) => renderItemGroup(props, box, index))}
    </TwoColumn.GroupComponent>
  );
}

function renderItemGroup(props, box, key) {
  if (box.items.length) {

    return (
      <div key={key} className={classNames(styles.box,
                                           styles[box.position],
                                           styles[`width-${widthName(box.width)}`],
                                           {[styles.customMargin]: box.customMargin})}>
        {props.children(
          <RestrictWidth width={box.width}>
            <ContentElements sectionProps={props.sectionProps}
                             customMargin={box.customMargin}
                             items={box.items} />
          </RestrictWidth>,
          {
            position: box.position,
            width: box.width,
            customMargin: box.customMargin,
            openStart: box.openStart,
            openEnd: box.openEnd
          }
        )}
      </div>
    );
  }
}

function RestrictWidth({width, children}) {
  if (width >= 0) {
    return children;
  }
  else {
    return (
      <div className={styles[`restrict-${widthName(width)}`]}>
        {children}
      </div>
    );
  }
}

function groupItemsByPosition(items, shouldInline) {
  const groups = [];

  let lastInlineBox = null;
  let currentGroup, currentBox;

  items.reduce((previousPosition, item, index) => {
    const {customMargin: elementSupportsCustomMargin} =
      api.contentElementTypes.getOptions(item.type) || {};
    let width = item.width || 0;
    const position = item.position === 'sticky' && !shouldInline(width) ? 'sticky' : 'inline';
    const customMargin = !!elementSupportsCustomMargin && width < widths.full;

    if (item.position === 'sticky' && position === 'inline' && width > widths.md) {
      width -= 1;
    }

    if (!currentGroup || previousPosition !== position ||
        (position === 'sticky' && currentBox.customMargin !== customMargin) ||
        currentBox.width !== width) {
      currentBox = null;

      if (!(previousPosition === 'sticky' && position === 'inline' && width <= widths.md)) {
        currentGroup = {
          width,
          boxes: []
        };

        groups.push(currentGroup);
      }
    }

    if (!currentBox || currentBox.customMargin !== customMargin) {
      currentBox = {
        customMargin,
        position,
        width,
        items: []
      };

      if (lastInlineBox && position === 'inline' && width <= widths.md && !customMargin) {
        lastInlineBox.openEnd = true;
        currentBox.openStart = true;
      }

      if (position === 'inline' && width <= widths.md && !customMargin) {
        lastInlineBox = currentBox;
      }
      else if ((position === 'inline' && width > widths.md) ||
               (customMargin && position !== 'sticky')) {
        lastInlineBox = null;
      }

      currentGroup.boxes.push(currentBox)
    }

    currentBox.items.push(item);
    return position;
  }, null);

  return groups;
}

function renderPlaceholder(placeholder) {
  if (!placeholder) {
    return null;
  }

  return (
    <div className={classNames(styles.group)}>
      <div className={classNames(styles.box, styles.inline)}>
        {placeholder}
      </div>
    </div>
  )
}