import Element from "./element"; import { CLASS_BTN_DISABLED, CLASS_CLOSE_BTN, CLASS_CLOSE_ONLY_BTN, CLASS_NEXT_STEP_BTN, CLASS_POPOVER_DESCRIPTION, CLASS_POPOVER_FOOTER, CLASS_POPOVER_TIP, CLASS_POPOVER_TITLE, CLASS_PREV_STEP_BTN, ID_POPOVER, POPOVER_HTML, } from "../common/constants"; import { createNodeFromString } from "../common/utils"; /** * Popover that is displayed on top of the highlighted element */ export default class Popover extends Element { options: Record; document: Document; window: Window; node?: HTMLElement; tipNode?: HTMLElement; titleNode?: HTMLElement; descriptionNode?: HTMLElement; footerNode?: HTMLElement; nextBtnNode?: HTMLElement; prevBtnNode?: HTMLElement; closeBtnNode?: HTMLElement; constructor(options, window, document) { super(); this.options = { isFirst: true, isLast: true, totalCount: 1, currentIndex: 0, offset: 0, showButtons: true, closeBtnText: "Close", doneBtnText: "Done", startBtnText: "Next →", nextBtnText: "Next →", prevBtnText: "← Previous", ...options, }; this.window = window; this.document = document; } /** * Prepares the dom element for popover * @private */ attachNode() { let popover = this.document.getElementById(ID_POPOVER); if (popover) { popover.parentElement.removeChild(popover); } popover = createNodeFromString(POPOVER_HTML(this.options.className)); document.body.appendChild(popover); this.node = popover; this.tipNode = popover.querySelector(`.${CLASS_POPOVER_TIP}`); this.titleNode = popover.querySelector(`.${CLASS_POPOVER_TITLE}`); this.descriptionNode = popover.querySelector( `.${CLASS_POPOVER_DESCRIPTION}`, ); this.footerNode = popover.querySelector(`.${CLASS_POPOVER_FOOTER}`); this.nextBtnNode = popover.querySelector(`.${CLASS_NEXT_STEP_BTN}`); this.prevBtnNode = popover.querySelector(`.${CLASS_PREV_STEP_BTN}`); this.closeBtnNode = popover.querySelector(`.${CLASS_CLOSE_BTN}`); } /** * Gets the title node for the popover * @returns {Element | null | *} * @public */ getTitleNode() { return this.titleNode; } /** * Gets the description node for the popover * @returns {Element | null | *} * @public */ getDescriptionNode() { return this.descriptionNode; } /** * Hides the popover * @public */ hide() { // If hide is called when the node isn't created yet if (!this.node) { return; } this.node.style.display = "none"; } /** * Sets the default state for the popover * @private */ setInitialState() { this.node.style.display = "block"; this.node.style.left = "0"; this.node.style.top = "0"; this.node.style.bottom = ""; this.node.style.right = ""; // Remove the positional classes from tip this.node.querySelector( `.${CLASS_POPOVER_TIP}`, ).className = CLASS_POPOVER_TIP; } /** * Shows the popover at the given position * @param {Position} position * @public */ show(position) { this.attachNode(); this.setInitialState(); // Set the title and descriptions this.titleNode.innerHTML = this.options.title; this.descriptionNode.innerHTML = this.options.description || ""; this.renderFooter(); // Position the popover around the given position switch (this.options.position) { case "left": case "left-top": this.positionOnLeft(position); break; case "left-center": this.positionOnLeftCenter(position); break; case "left-bottom": this.positionOnLeftBottom(position); break; case "right": case "right-top": this.positionOnRight(position); break; case "right-center": this.positionOnRightCenter(position); break; case "right-bottom": this.positionOnRightBottom(position); break; case "top": case "top-left": this.positionOnTop(position); break; case "top-center": this.positionOnTopCenter(position); break; case "top-right": this.positionOnTopRight(position); break; case "bottom": case "bottom-left": this.positionOnBottom(position); break; case "bottom-center": this.positionOnBottomCenter(position); break; case "bottom-right": this.positionOnBottomRight(position); break; case "mid-center": this.positionOnMidCenter(position); break; case "auto": default: this.autoPosition(position); break; } // Bring the popover in view port once it is displayed this.bringInView(); } /** * Enables, disables buttons, sets the text and * decides if to show them or not * @private */ renderFooter() { this.nextBtnNode.innerHTML = this.options.nextBtnText; this.prevBtnNode.innerHTML = this.options.prevBtnText; this.closeBtnNode.innerHTML = this.options.closeBtnText; const hasSteps = this.options.totalCount && this.options.totalCount !== 1; // If there was only one item, hide the buttons if (!this.options.showButtons) { this.footerNode.style.display = "none"; return; } // If this is just a single highlighted element i.e. there // are no other steps to go to – just hide the navigation buttons if (!hasSteps) { this.nextBtnNode.style.display = "none"; this.prevBtnNode.style.display = "none"; this.closeBtnNode.classList.add(CLASS_CLOSE_ONLY_BTN); } else { // @todo modify CSS to use block this.nextBtnNode.style.display = "inline-block"; this.prevBtnNode.style.display = "inline-block"; this.closeBtnNode.classList.remove(CLASS_CLOSE_ONLY_BTN); } this.footerNode.style.display = "block"; if (this.options.isFirst) { this.prevBtnNode.classList.add(CLASS_BTN_DISABLED); this.nextBtnNode.innerHTML = this.options.startBtnText; } else { this.prevBtnNode.classList.remove(CLASS_BTN_DISABLED); } if (this.options.isLast) { this.nextBtnNode.innerHTML = this.options.doneBtnText; } else { this.nextBtnNode.innerHTML = this.options.nextBtnText; } } /** * Shows the popover on the left of the given position * @param {Position} elementPosition * @private */ positionOnLeft(elementPosition) { const popoverWidth = this.getSize().width; const popoverMargin = this.options.padding + 10; // adding 10 to give it a little distance from the element this.node.style.left = `${ elementPosition.left - popoverWidth - popoverMargin }px`; this.node.style.top = `${ elementPosition.top + this.options.offset - this.options.padding }px`; this.node.style.right = ""; this.node.style.bottom = ""; this.tipNode.classList.add("right"); } /** * Shows the popover on the left of the given position * @param {Position} elementPosition * @private */ positionOnLeftBottom(elementPosition) { const popoverDimensions = this.getSize(); const popoverWidth = popoverDimensions.width; const popoverMargin = this.options.padding + 10; // adding 10 to give it a little distance from the element this.node.style.left = `${ elementPosition.left - popoverWidth - popoverMargin }px`; this.node.style.top = `${ elementPosition.bottom + this.options.padding + this.options.offset - popoverDimensions.height }px`; this.node.style.bottom = ""; this.node.style.right = ""; this.tipNode.classList.add("right", "position-bottom"); } /** * Shows the popover on the left center of the given position * @param {Position} elementPosition * @private */ positionOnLeftCenter(elementPosition) { const popoverDimensions = this.getSize(); const popoverWidth = popoverDimensions.width; const popoverHeight = popoverDimensions.height; const popoverCenter = popoverHeight / 2; const popoverMargin = this.options.padding + 10; // adding 10 to give it a little distance from the element const elementCenter = (elementPosition.bottom - elementPosition.top) / 2; const topCenterPosition = elementPosition.top - popoverCenter + elementCenter + this.options.offset; this.node.style.left = `${ elementPosition.left - popoverWidth - popoverMargin }px`; this.node.style.top = `${topCenterPosition}px`; this.node.style.right = ""; this.node.style.bottom = ""; this.tipNode.classList.add("right", "position-center"); } /** * Shows the popover on the right of the given position * @param {Position} elementPosition * @private */ positionOnRight(elementPosition) { const popoverMargin = this.options.padding + 10; // adding 10 to give it a little distance from the element this.node.style.left = `${elementPosition.right + popoverMargin}px`; this.node.style.top = `${ elementPosition.top + this.options.offset - this.options.padding }px`; this.node.style.right = ""; this.node.style.bottom = ""; this.tipNode.classList.add("left"); } /** * Shows the popover on the right of the given position * @param {Position} elementPosition * @private */ positionOnRightCenter(elementPosition) { const popoverDimensions = this.getSize(); const popoverMargin = this.options.padding + 10; // adding 10 to give it a little distance from the element const popoverHeight = popoverDimensions.height; const popoverCenter = popoverHeight / 2; const elementCenter = (elementPosition.bottom - elementPosition.top) / 2; const topCenterPosition = elementPosition.top - popoverCenter + elementCenter + this.options.offset; this.node.style.left = `${elementPosition.right + popoverMargin}px`; this.node.style.top = `${topCenterPosition}px`; this.node.style.right = ""; this.node.style.bottom = ""; this.tipNode.classList.add("left", "position-center"); } /** * Shows the popover on the right of the given position * @param {Position} elementPosition * @private */ positionOnRightBottom(elementPosition) { const popoverMargin = this.options.padding + 10; // adding 10 to give it a little distance from the element const popoverDimensions = this.getSize(); this.node.style.left = `${elementPosition.right + popoverMargin}px`; this.node.style.top = `${ elementPosition.bottom + this.options.padding + this.options.offset - popoverDimensions.height }px`; this.node.style.bottom = ""; this.node.style.right = ""; this.tipNode.classList.add("left", "position-bottom"); } /** * Shows the popover on the top of the given position * @param {Position} elementPosition * @private */ positionOnTop(elementPosition) { const popoverHeight = this.getSize().height; const popoverMargin = this.options.padding + 10; // adding 10 to give it a little distance from the element this.node.style.top = `${ elementPosition.top - popoverHeight - popoverMargin }px`; this.node.style.left = `${ elementPosition.left - this.options.padding + this.options.offset }px`; this.node.style.right = ""; this.node.style.bottom = ""; this.tipNode.classList.add("bottom"); } /** * Shows the popover on the top center of the given position * @param {Position} elementPosition * @private */ positionOnTopCenter(elementPosition) { const dimensions = this.getSize(); const popoverHeight = dimensions.height; const popoverWidth = dimensions.width / 2; const popoverMargin = this.options.padding + 10; // adding 10 to give it a little distance from the element const nodeCenter = this.options.offset + elementPosition.left + (elementPosition.right - elementPosition.left) / 2; this.node.style.top = `${ elementPosition.top - popoverHeight - popoverMargin }px`; this.node.style.left = `${ nodeCenter - popoverWidth - this.options.padding }px`; this.node.style.right = ""; this.node.style.bottom = ""; // Add the tip at the top center this.tipNode.classList.add("bottom", "position-center"); } /** * Shows the popover on the top right of the given position * @param {Position} elementPosition * @private */ positionOnTopRight(elementPosition) { const dimensions = this.getSize(); const popoverHeight = dimensions.height; const popoverMargin = this.options.padding + 10; // adding 10 to give it a little distance from the element this.node.style.top = `${ elementPosition.top - popoverHeight - popoverMargin }px`; this.node.style.left = `${ elementPosition.right + this.options.padding + this.options.offset - dimensions.width }px`; this.node.style.right = ""; this.node.style.bottom = ""; // Add the tip at the top center this.tipNode.classList.add("bottom", "position-right"); } /** * Shows the popover on the bottom of the given position * @param {Position} elementPosition * @private */ positionOnBottom(elementPosition) { const popoverMargin = this.options.padding + 10; // adding 10 to give it a little distance from the element this.node.style.top = `${elementPosition.bottom + popoverMargin}px`; this.node.style.left = `${ elementPosition.left - this.options.padding + this.options.offset }px`; this.node.style.right = ""; this.node.style.bottom = ""; this.tipNode.classList.add("top"); } /** * Shows the popover on the bottom-center of the given position * @param {Position} elementPosition * @private */ positionOnBottomCenter(elementPosition) { const popoverWidth = this.getSize().width / 2; const popoverMargin = this.options.padding + 10; // adding 10 to give it a little distance from the element const nodeCenter = this.options.offset + elementPosition.left + (elementPosition.right - elementPosition.left) / 2; this.node.style.top = `${elementPosition.bottom + popoverMargin}px`; this.node.style.left = `${ nodeCenter - popoverWidth - this.options.padding }px`; this.node.style.right = ""; this.node.style.bottom = ""; // Add the tip at the top center this.tipNode.classList.add("top", "position-center"); } /** * Shows the popover on the bottom-right of the given position * @param {Position} elementPosition * @private */ positionOnBottomRight(elementPosition) { const dimensions = this.getSize(); const popoverMargin = this.options.padding + 10; // adding 10 to give it a little distance from the element this.node.style.top = `${elementPosition.bottom + popoverMargin}px`; this.node.style.left = `${ elementPosition.right + this.options.padding + this.options.offset - dimensions.width }px`; this.node.style.right = ""; this.node.style.bottom = ""; // Add the tip at the top center this.tipNode.classList.add("top", "position-right"); } /** * Shows the popover on the mid-center of the given position * @param {Position} elementPosition * @private */ positionOnMidCenter(elementPosition) { const popoverDimensions = this.getSize(); const popoverHeight = popoverDimensions.height; const popoverWidth = popoverDimensions.width / 2; const popoverCenter = popoverHeight / 2; const elementCenter = (elementPosition.bottom - elementPosition.top) / 2; const topCenterPosition = elementPosition.top - popoverCenter + elementCenter + this.options.offset; const nodeCenter = this.options.offset + elementPosition.left + (elementPosition.right - elementPosition.left) / 2; this.node.style.top = `${topCenterPosition}px`; this.node.style.left = `${ nodeCenter - popoverWidth - this.options.padding }px`; this.node.style.right = ""; this.node.style.bottom = ""; // Add the tip at the top center this.tipNode.classList.add("mid-center"); } /** * Automatically positions the popover around the given position * such that the element and popover remain in view * @todo add the left and right positioning decisions * @param {Position} elementPosition * @private */ autoPosition(elementPosition) { const pageSize = this.getFullPageSize(); const popoverSize = this.getSize(); const pageHeight = pageSize.height; const popoverHeight = popoverSize.height; const popoverMargin = this.options.padding + 10; // adding 10 to give it a little distance from the element const pageHeightAfterPopOver = elementPosition.bottom + popoverHeight + popoverMargin; // If adding popover would go out of the window height, then show it to the top if (pageHeightAfterPopOver >= pageHeight) { this.positionOnTop(elementPosition); } else { this.positionOnBottom(elementPosition); } } }