|
import { bringInView } from "./utils"; |
|
import { STAGE_PADDING } from "./stage"; |
|
|
|
export type Side = "top" | "right" | "bottom" | "left"; |
|
export type Alignment = "start" | "center" | "end"; |
|
|
|
const POPOVER_OFFSET = 10; |
|
|
|
export type Popover = { |
|
title?: string; |
|
description: string; |
|
side?: Side; |
|
align?: Alignment; |
|
}; |
|
|
|
type PopoverDOM = { |
|
wrapper: HTMLElement; |
|
arrow: HTMLElement; |
|
title: HTMLElement; |
|
description: HTMLElement; |
|
footer: HTMLElement; |
|
previousButton: HTMLElement; |
|
nextButton: HTMLElement; |
|
closeButton: HTMLElement; |
|
footerButtons: HTMLElement; |
|
}; |
|
|
|
let popover: PopoverDOM | undefined; |
|
|
|
export function renderPopover(element: Element) { |
|
if (!popover) { |
|
popover = createPopover(); |
|
document.body.appendChild(popover.wrapper); |
|
} |
|
|
|
|
|
const popoverWrapper = popover.wrapper; |
|
popoverWrapper.style.display = "block"; |
|
popoverWrapper.style.left = ""; |
|
popoverWrapper.style.top = ""; |
|
popoverWrapper.style.bottom = ""; |
|
popoverWrapper.style.right = ""; |
|
|
|
|
|
const popoverArrow = popover.arrow; |
|
popoverArrow.className = "driver-popover-arrow"; |
|
|
|
repositionPopover(element); |
|
bringInView(popoverWrapper); |
|
} |
|
|
|
function getPopoverDimensions() { |
|
if (!popover?.wrapper) { |
|
return; |
|
} |
|
|
|
const boundingClientRect = popover.wrapper.getBoundingClientRect(); |
|
|
|
return { |
|
width: boundingClientRect.width + STAGE_PADDING + POPOVER_OFFSET, |
|
height: boundingClientRect.height + STAGE_PADDING + POPOVER_OFFSET, |
|
|
|
realWidth: boundingClientRect.width, |
|
realHeight: boundingClientRect.height, |
|
}; |
|
} |
|
|
|
|
|
function calculateLeftForTopBottom( |
|
alignment: Alignment, |
|
config: { |
|
elementDimensions: DOMRect; |
|
popoverDimensions?: ReturnType<typeof getPopoverDimensions>; |
|
popoverPadding: number; |
|
popoverArrowDimensions: { width: number; height: number }; |
|
} |
|
): number { |
|
const { elementDimensions, popoverDimensions, popoverPadding, popoverArrowDimensions } = config; |
|
|
|
if (alignment === "start") { |
|
return Math.max( |
|
Math.min( |
|
elementDimensions.left - popoverPadding, |
|
window.innerWidth - popoverDimensions!.realWidth - popoverArrowDimensions.width |
|
), |
|
popoverArrowDimensions.width |
|
); |
|
} |
|
|
|
if (alignment === "end") { |
|
return Math.max( |
|
Math.min( |
|
elementDimensions.left - popoverDimensions?.realWidth + elementDimensions.width + popoverPadding, |
|
window.innerWidth - popoverDimensions?.realWidth - popoverArrowDimensions.width |
|
), |
|
popoverArrowDimensions.width |
|
); |
|
} |
|
|
|
if (alignment === "center") { |
|
return Math.max( |
|
Math.min( |
|
elementDimensions.left + elementDimensions.width / 2 - popoverDimensions?.realWidth / 2, |
|
window.innerWidth - popoverDimensions?.realWidth - popoverArrowDimensions.width |
|
), |
|
popoverArrowDimensions.width |
|
); |
|
} |
|
|
|
return 0; |
|
} |
|
|
|
export function repositionPopover(element: Element) { |
|
if (!popover) { |
|
return; |
|
} |
|
|
|
const requiredAlignment: Alignment = "start"; |
|
const popoverPadding = STAGE_PADDING; |
|
|
|
const popoverDimensions = getPopoverDimensions(); |
|
const popoverArrowDimensions = popover.arrow.getBoundingClientRect(); |
|
const elementDimensions = element.getBoundingClientRect(); |
|
|
|
const topValue = elementDimensions.top - popoverDimensions!.height; |
|
const isTopOptimal = topValue >= 0; |
|
|
|
const bottomValue = window.innerHeight - (elementDimensions.bottom + popoverDimensions!.height); |
|
const isBottomOptimal = bottomValue >= 0; |
|
|
|
const leftValue = elementDimensions.left - popoverDimensions!.width; |
|
const isLeftOptimal = leftValue >= 0; |
|
|
|
const rightValue = window.innerWidth - (elementDimensions.right + popoverDimensions!.width); |
|
const isRightOptimal = rightValue >= 0; |
|
|
|
const noneOptimal = !isTopOptimal && !isBottomOptimal && !isLeftOptimal && !isRightOptimal; |
|
|
|
if (noneOptimal) { |
|
const leftValue = window.innerWidth / 2 - popoverDimensions?.realWidth! / 2; |
|
const bottomValue = 10; |
|
|
|
popover.wrapper.style.left = `${leftValue}px`; |
|
popover.wrapper.style.right = `auto`; |
|
popover.wrapper.style.bottom = `${bottomValue}px`; |
|
popover.wrapper.style.top = `auto`; |
|
|
|
popover.arrow.classList.add("driver-popover-arrow-none"); |
|
} else if (isTopOptimal) { |
|
const topToSet = Math.min(topValue, window.innerHeight - popoverDimensions!.height - popoverArrowDimensions.width); |
|
let leftToSet = calculateLeftForTopBottom(requiredAlignment, { |
|
elementDimensions, |
|
popoverDimensions, |
|
popoverPadding, |
|
popoverArrowDimensions, |
|
}); |
|
|
|
popover.wrapper.style.top = `${topToSet}px`; |
|
popover.wrapper.style.left = `${leftToSet}px`; |
|
popover.wrapper.style.bottom = `auto`; |
|
popover.wrapper.style.right = "auto"; |
|
} else if (isBottomOptimal) { |
|
const bottomToSet = Math.min( |
|
bottomValue, |
|
window.innerHeight - popoverDimensions?.realHeight - popoverArrowDimensions.width |
|
); |
|
|
|
let leftToSet = calculateLeftForTopBottom(requiredAlignment, { |
|
elementDimensions, |
|
popoverDimensions, |
|
popoverPadding, |
|
popoverArrowDimensions, |
|
}); |
|
|
|
popover.wrapper.style.left = `${leftToSet}px`; |
|
popover.wrapper.style.bottom = `${bottomToSet}px`; |
|
popover.wrapper.style.top = `auto`; |
|
popover.wrapper.style.right = "auto"; |
|
} |
|
|
|
|
|
|
|
renderPopoverArrow(); |
|
} |
|
|
|
function renderPopoverArrow() { |
|
|
|
} |
|
|
|
function createPopover(): PopoverDOM { |
|
const wrapper = document.createElement("div"); |
|
wrapper.classList.add("driver-popover"); |
|
|
|
const arrow = document.createElement("div"); |
|
arrow.classList.add("driver-popover-arrow"); |
|
|
|
const title = document.createElement("div"); |
|
title.classList.add("driver-popover-title"); |
|
title.innerText = "Popover Title"; |
|
|
|
const description = document.createElement("div"); |
|
description.classList.add("driver-popover-description"); |
|
description.innerText = "Popover description is here"; |
|
|
|
const footer = document.createElement("div"); |
|
footer.classList.add("driver-popover-footer"); |
|
|
|
const closeButton = document.createElement("button"); |
|
closeButton.classList.add("driver-popover-close-btn"); |
|
closeButton.innerText = "Close"; |
|
|
|
const footerButtons = document.createElement("span"); |
|
footerButtons.classList.add("driver-popover-footer-btns"); |
|
|
|
const previousButton = document.createElement("button"); |
|
previousButton.classList.add("driver-popover-prev-btn"); |
|
previousButton.innerHTML = "← Previous"; |
|
|
|
const nextButton = document.createElement("button"); |
|
nextButton.classList.add("driver-popover-next-btn"); |
|
nextButton.innerHTML = "Next →"; |
|
|
|
footerButtons.appendChild(previousButton); |
|
footerButtons.appendChild(nextButton); |
|
|
|
footer.appendChild(closeButton); |
|
footer.appendChild(footerButtons); |
|
|
|
wrapper.appendChild(arrow); |
|
wrapper.appendChild(title); |
|
wrapper.appendChild(description); |
|
wrapper.appendChild(footer); |
|
|
|
return { |
|
wrapper, |
|
arrow, |
|
title, |
|
description, |
|
footer, |
|
previousButton, |
|
nextButton, |
|
closeButton, |
|
footerButtons, |
|
}; |
|
} |
|
|
|
export function destroyPopover() { |
|
if (!popover) { |
|
return; |
|
} |
|
|
|
popover.wrapper.parentElement?.removeChild(popover.wrapper); |
|
popover = undefined; |
|
} |
|
|