remirror/remirror

View on GitHub
packages/remirror__extension-positioner/src/positioner-utils.ts

Summary

Maintainability
A
0 mins
Test Coverage
import {
  EditorState,
  EditorStateProps,
  getStyle,
  hasTransactionChanged,
  isElementDomNode,
  Transaction,
  TransactionProps,
  within,
} from '@remirror/core';

export { isEmptyBlockNode } from '@remirror/core';

interface HasChangedProps extends EditorStateProps, Partial<TransactionProps> {
  previousState: EditorState | undefined;
}

/**
 * Checks if the given transaction force updates positioners.
 *
 * @param tr - the Transaction to check
 * @param key - filter for a specific key. Defaults to all.
 */
export function isPositionerUpdateTransaction(
  tr: Transaction,
  key = POSITIONER_UPDATE_ALL,
): boolean {
  const { key: trKey } = tr?.getMeta(POSITIONER_UPDATE_KEY) ?? {};
  return trKey === key;
}

/**
 * Checks the transaction for changes or compares the state with the previous
 * state.
 *
 * Return `true` when a change is detected in the document or the selection.
 */
export function hasStateChanged(props: HasChangedProps): boolean {
  const { tr, state, previousState } = props;

  if (!previousState) {
    return true;
  }

  if (tr && isPositionerUpdateTransaction(tr)) {
    return true;
  }

  if (tr) {
    return hasTransactionChanged(tr);
  }

  return !state.doc.eq(previousState.doc) || !state.selection.eq(previousState.selection);
}

interface IsPositionVisibleOptions {
  /**
   * When `true` account for padding and the scroll bar width for the provided
   * element.
   *
   * @defaultValue false
   */
  accountForPadding?: boolean;
}

/**
 * Checks that the rect is visible within the provided element.
 *
 * This is specific for the remirror editor.
 */
export function isPositionVisible(
  rect: DOMRect,
  element: Element,
  options: IsPositionVisibleOptions = {},
): boolean {
  const elementRect = element.getBoundingClientRect();
  const { accountForPadding = false } = options;

  // Amount to increment these values by.
  let leftDelta = 0;
  let rightDelta = 0;
  let topDelta = 0;
  let bottomDelta = 0;

  if (isElementDomNode(element) && accountForPadding) {
    const paddingLeft = Number.parseFloat(getStyle(element, 'padding-left').replace('px', ''));
    const paddingRight = Number.parseFloat(getStyle(element, 'padding-right').replace('px', ''));
    const paddingTop = Number.parseFloat(getStyle(element, 'padding-top').replace('px', ''));
    const paddingBottom = Number.parseFloat(getStyle(element, 'padding-bottom').replace('px', ''));
    const borderLeft = Number.parseFloat(getStyle(element, 'border-left').replace('px', ''));
    const borderRight = Number.parseFloat(getStyle(element, 'border-right').replace('px', ''));
    const borderTop = Number.parseFloat(getStyle(element, 'border-top').replace('px', ''));
    const borderBottom = Number.parseFloat(getStyle(element, 'border-bottom').replace('px', ''));
    const verticalScrollBarWidth = element.offsetWidth - element.clientWidth;
    const horizontalScrollBarHeight = element.offsetHeight - element.clientHeight;
    leftDelta += paddingLeft + borderLeft + (element.dir === 'rtl' ? verticalScrollBarWidth : 0);
    rightDelta += paddingRight + borderRight + (element.dir === 'rtl' ? 0 : verticalScrollBarWidth);
    topDelta += paddingTop + borderTop;
    bottomDelta += paddingBottom + borderBottom + horizontalScrollBarHeight;
  }

  const containerRect = new DOMRect(
    elementRect.left + leftDelta,
    elementRect.top + topDelta,
    elementRect.width - rightDelta,
    elementRect.height - bottomDelta,
  );

  for (const [top, left] of [
    [rect.top, rect.left],
    [rect.top, rect.right],
    [rect.bottom, rect.left],
    [rect.bottom, rect.right],
  ] as const) {
    if (
      within(top, containerRect.top, containerRect.bottom) &&
      within(left, containerRect.left, containerRect.right)
    ) {
      return true;
    }
  }

  return false;
}

export const POSITIONER_WIDGET_KEY = 'remirror-positioner-widget';

export const POSITIONER_UPDATE_KEY = 'positionerUpdate';

export const POSITIONER_UPDATE_ALL = '__all_positioners__';