UNPKG

react-joyride

Version:

Create guided tours for your apps

278 lines (221 loc) 6.36 kB
import scroll from 'scroll'; import scrollParent from 'scrollparent'; export function canUseDOM() { return !!(typeof window !== 'undefined' && window.document?.createElement); } /** * Find the bounding client rect */ export function getClientRect(element: HTMLElement | null) { if (!element) { return null; } return element.getBoundingClientRect(); } /** * Helper function to get the browser-normalized "document height" */ export function getDocumentHeight(median = false): number { const { body, documentElement } = document; if (!body || !documentElement) { return 0; } if (median) { const heights = [ body.scrollHeight, body.offsetHeight, documentElement.clientHeight, documentElement.scrollHeight, documentElement.offsetHeight, ].sort((a, b) => a - b); const middle = Math.floor(heights.length / 2); if (heights.length % 2 === 0) { return (heights[middle - 1] + heights[middle]) / 2; } return heights[middle]; } return Math.max( body.scrollHeight, body.offsetHeight, documentElement.clientHeight, documentElement.scrollHeight, documentElement.offsetHeight, ); } /** * Find and return the target DOM element based on a step's 'target'. */ export function getElement(element: string | HTMLElement): HTMLElement | null { if (typeof element === 'string') { try { return document.querySelector(element); } catch (error: any) { if (process.env.NODE_ENV !== 'production') { // eslint-disable-next-line no-console console.error(error); } return null; } } return element; } /** * Get computed style property */ export function getStyleComputedProperty(el: HTMLElement): CSSStyleDeclaration | null { if (!el || el.nodeType !== 1) { return null; } return getComputedStyle(el); } /** * Get scroll parent with fix */ export function getScrollParent( element: HTMLElement | null, skipFix: boolean, forListener?: boolean, ) { if (!element) { return scrollDocument(); } const parent = scrollParent(element) as HTMLElement; if (parent) { if (parent.isSameNode(scrollDocument())) { if (forListener) { return document; } return scrollDocument(); } const hasScrolling = parent.scrollHeight > parent.offsetHeight; if (!hasScrolling && !skipFix) { parent.style.overflow = 'initial'; return scrollDocument(); } } return parent; } /** * Check if the element has custom scroll parent */ export function hasCustomScrollParent(element: HTMLElement | null, skipFix: boolean): boolean { if (!element) { return false; } const parent = getScrollParent(element, skipFix); return parent ? !parent.isSameNode(scrollDocument()) : false; } /** * Check if the element has custom offset parent */ export function hasCustomOffsetParent(element: HTMLElement): boolean { return element.offsetParent !== document.body; } /** * Check if an element has fixed/sticky position */ export function hasPosition(el: HTMLElement | Node | null, type: string = 'fixed'): boolean { if (!el || !(el instanceof HTMLElement)) { return false; } const { nodeName } = el; const styles = getStyleComputedProperty(el); if (nodeName === 'BODY' || nodeName === 'HTML') { return false; } if (styles && styles.position === type) { return true; } if (!el.parentNode) { return false; } return hasPosition(el.parentNode, type); } /** * Check if the element is visible */ export function isElementVisible(element: HTMLElement): element is HTMLElement { if (!element) { return false; } let parentElement: HTMLElement | null = element; while (parentElement) { if (parentElement === document.body) { break; } if (parentElement instanceof HTMLElement) { const { display, visibility } = getComputedStyle(parentElement); if (display === 'none' || visibility === 'hidden') { return false; } } parentElement = parentElement.parentElement ?? null; } return true; } /** * Find and return the target DOM element based on a step's 'target'. */ export function getElementPosition( element: HTMLElement | null, offset: number, skipFix: boolean, ): number { const elementRect = getClientRect(element); const parent = getScrollParent(element, skipFix); const hasScrollParent = hasCustomScrollParent(element, skipFix); const isFixedTarget = hasPosition(element); let parentTop = 0; let top = elementRect?.top ?? 0; if (hasScrollParent && isFixedTarget) { const offsetTop = element?.offsetTop ?? 0; const parentScrollTop = (parent as HTMLElement)?.scrollTop ?? 0; top = offsetTop - parentScrollTop; } else if (parent instanceof HTMLElement) { parentTop = parent.scrollTop; if (!hasScrollParent && !hasPosition(element)) { top += parentTop; } if (!parent.isSameNode(scrollDocument())) { top += scrollDocument().scrollTop; } } return Math.floor(top - offset); } /** * Get the scrollTop position */ export function getScrollTo(element: HTMLElement | null, offset: number, skipFix: boolean): number { if (!element) { return 0; } const { offsetTop = 0, scrollTop = 0 } = scrollParent(element) ?? {}; let top = element.getBoundingClientRect().top + scrollTop; if (!!offsetTop && (hasCustomScrollParent(element, skipFix) || hasCustomOffsetParent(element))) { top -= offsetTop; } const output = Math.floor(top - offset); return output < 0 ? 0 : output; } export function scrollDocument(): Element | HTMLElement { return document.scrollingElement ?? document.documentElement; } /** * Scroll to position */ export function scrollTo( value: number, options: { duration?: number; element: Element | HTMLElement }, ): Promise<void> { const { duration, element } = options; return new Promise((resolve, reject) => { const { scrollTop } = element; const limit = value > scrollTop ? value - scrollTop : scrollTop - value; scroll.top(element as HTMLElement, value, { duration: limit < 100 ? 50 : duration }, error => { if (error && error.message !== 'Element already at target scroll position') { return reject(error); } return resolve(); }); }); }