All files / publisher/tour helpers.ts

42.3% Statements 11/26
42.85% Branches 6/14
50% Functions 5/10
44% Lines 11/25

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116          2x 5x                     5x   5x               5x         2x                                               2x                                                                 2x       2x 5x       5x                                  
import { MASK_OFFSET } from "./constants";
 
import type { Step } from "../listing/types";
 
// check if element is part of the DOM and is visible
export const isVisibleInDocument = (el: HTMLElement): boolean =>
  document.contains(el) && !(el.offsetWidth === 0 && el.offsetHeight === 0);
 
// find DOM elements for each step, ignore steps with no elements
// set default position to "bottom-left"
export function prepareSteps(steps: Step[]): Array<{
  id: string;
  position: string;
  elements: HTMLElement[];
  title: string;
  content: string;
}> {
  return steps
    .map((step) => {
      return {
        ...step,
        elements: [].slice.apply(
          document.querySelectorAll(`[data-tour="${step.id}"]`),
        ),
        position: step.position || "bottom-left",
      };
    })
    .filter((step) => step.elements.length > 0);
}
 
// get rectangle of given DOM element
// relative to the page, taking scroll into account
const getRectFromEl = (
  el: HTMLElement,
): {
  top: number;
  left: number;
  width: number;
  height: number;
} => {
  const clientRect = el.getBoundingClientRect();
  const ret = {
    top:
      clientRect.top +
      (window.pageYOffset || document.documentElement.scrollTop),
    left:
      clientRect.left +
      (window.pageXOffset || document.documentElement.scrollLeft),
    width: clientRect.width,
    height: clientRect.height,
  };
 
  return ret;
};
 
// get mask based on rectangle
const getMaskFromRect = (rect: {
  top: number;
  left: number;
  width: number;
  height: number;
}): {
  top: number;
  bottom: number;
  left: number;
  right: number;
} => {
  let top = rect.top - MASK_OFFSET;
  if (top < 0) {
    top = 0;
  }
 
  let left = rect.left - MASK_OFFSET;
  if (left < 0) {
    left = 0;
  }
 
  const bottom = rect.top + rect.height + MASK_OFFSET;
  const right = rect.left + rect.width + MASK_OFFSET;
 
  return {
    top,
    bottom,
    left,
    right,
  };
};
 
// calculate mask for given element
const getMaskFromEl = (el: HTMLElement) => getMaskFromRect(getRectFromEl(el));
 
// get mask that is an union of all elements' masks
// calculates the rectangle that contains each individual element rectangles
export const getMaskFromElements = (elements: Array<HTMLElement>) => {
  const masks = elements
    .filter(isVisibleInDocument)
    .map((el) => getMaskFromEl(el));
 
  return masks.reduce(
    (unionMask, elMask) => {
      return {
        top: Math.min(unionMask.top, elMask.top),
        left: Math.min(unionMask.left, elMask.left),
        bottom: Math.max(unionMask.bottom, elMask.bottom),
        right: Math.max(unionMask.right, elMask.right),
      };
    },
    {
      top: Infinity,
      left: Infinity,
      right: 0,
      bottom: 0,
    },
  );
};