diff --git a/src/packages/tooltip/tooltip.ts b/src/packages/tooltip/tooltip.ts index a29c0a326..51b1fe68c 100644 --- a/src/packages/tooltip/tooltip.ts +++ b/src/packages/tooltip/tooltip.ts @@ -1,4 +1,4 @@ -import { Offset } from "../../util/getOffset"; +import getOffset, { Offset } from "../../util/getOffset"; import getWindowSize from "../../util/getWindowSize"; import van, { ChildDom, State } from "../dom/van"; import { arrowClassName, tooltipClassName } from "../tour/classNames"; @@ -121,12 +121,14 @@ function checkRight( width: number; height: number; }, + windowSize: { + width: number; + height: number; + }, tooltipLayerStyleLeft: number, tooltipWidth: number, tooltipLeft: State ): boolean { - const windowSize = getWindowSize(); - if ( targetOffset.left + tooltipLayerStyleLeft + tooltipWidth > windowSize.width @@ -145,6 +147,7 @@ function checkRight( const alignTooltip = ( position: TooltipPosition, targetOffset: { width: number; height: number; left: number; top: number }, + windowSize: { width: number; height: number }, tooltipWidth: number, tooltipHeight: number, tooltipTop: State, @@ -197,6 +200,7 @@ const alignTooltip = ( tooltipRight.val = undefined; checkRight( targetOffset, + windowSize, tooltipLayerStyleLeftRight, tooltipWidth, tooltipLeft @@ -212,6 +216,7 @@ const alignTooltip = ( checkRight( targetOffset, + windowSize, tooltipLayerStyleLeft, tooltipWidth, tooltipLeft @@ -278,6 +283,7 @@ const alignTooltip = ( tooltipRight.val = ""; checkRight( targetOffset, + windowSize, tooltipLayerStyleLeftRight, tooltipWidth, tooltipLeft @@ -291,14 +297,15 @@ const alignTooltip = ( // case 'bottom': // Bottom going to follow the default behavior default: - checkRight(targetOffset, 0, tooltipWidth, tooltipLeft); + checkRight(targetOffset, windowSize, 0, tooltipWidth, tooltipLeft); tooltipTop.val = `${targetOffset.height + 20}px`; } }; export type TooltipProps = { position: TooltipPosition; - targetOffset: Offset; + element: HTMLElement; + refreshes: State; hintMode: boolean; showStepNumbers: boolean; @@ -312,7 +319,8 @@ export type TooltipProps = { export const Tooltip = ( { position: initialPosition, - targetOffset, + element, + refreshes, hintMode = false, showStepNumbers = false, @@ -332,30 +340,45 @@ export const Tooltip = ( const marginTop = van.state("auto"); const opacity = van.state(0); // setting a default height for the tooltip instead of 0 to avoid flickering - const tooltipHeight = van.state(150); + // this default is coming from the CSS class and is overridden after the tooltip is rendered + const tooltipHeight = van.state(250); // max width of the tooltip according to its CSS class + // this default is coming from the CSS class and is overridden after the tooltip is rendered const tooltipWidth = van.state(300); const position = van.state(initialPosition); - const windowSize = getWindowSize(); + // windowSize can change if the window is resized + const windowSize = van.state(getWindowSize()); + const targetOffset = van.state(getOffset(element)); const tooltipBottomOverflow = van.derive( - () => targetOffset.top + tooltipHeight.val! > windowSize.height + () => targetOffset.val!.top + tooltipHeight.val! > windowSize.val!.height ); + van.derive(() => { + // set the new windowSize and targetOffset if the refreshes signal changes + if (refreshes.val !== undefined) { + windowSize.val = getWindowSize(); + targetOffset.val = getOffset(element); + } + }); + // auto-align tooltip based on position precedence and target offset van.derive(() => { if ( position.val !== undefined && - position.val !== "floating" && + initialPosition !== "floating" && autoPosition && tooltipWidth.val && - tooltipHeight.val + tooltipHeight.val && + targetOffset.val && + windowSize.val ) { position.val = determineAutoPosition( positionPrecedence, - targetOffset, + targetOffset.val, tooltipWidth.val, tooltipHeight.val, - position.val + initialPosition, + windowSize.val ); } }); @@ -366,11 +389,14 @@ export const Tooltip = ( tooltipWidth.val !== undefined && tooltipHeight.val !== undefined && tooltipBottomOverflow.val !== undefined && - position.val !== undefined + position.val !== undefined && + targetOffset.val !== undefined && + windowSize.val !== undefined ) { alignTooltip( position.val, - targetOffset, + targetOffset.val, + windowSize.val, tooltipWidth.val, tooltipHeight.val, top, @@ -407,5 +433,11 @@ export const Tooltip = ( opacity.val = 1; }, transitionDuration); + setTimeout(() => { + // set the correct height and width of the tooltip after it has been rendered + tooltipHeight.val = tooltip.offsetHeight; + tooltipWidth.val = tooltip.offsetWidth; + }, 1); + return tooltip; }; diff --git a/src/packages/tooltip/tooltipPosition.ts b/src/packages/tooltip/tooltipPosition.ts index be6d7741d..df66aa2a0 100644 --- a/src/packages/tooltip/tooltipPosition.ts +++ b/src/packages/tooltip/tooltipPosition.ts @@ -1,4 +1,3 @@ -import getWindowSize from "../../util/getWindowSize"; import removeEntry from "../../util/removeEntry"; import { Offset } from "../../util/getOffset"; @@ -67,13 +66,12 @@ export function determineAutoPosition( targetOffset: Offset, tooltipWidth: number, tooltipHeight: number, - desiredTooltipPosition: TooltipPosition + desiredTooltipPosition: TooltipPosition, + windowSize: { width: number; height: number } ): TooltipPosition { // Take a clone of position precedence. These will be the available const possiblePositions = positionPrecedence.slice(); - const windowSize = getWindowSize(); - // Add some padding to the tooltip height and width for better positioning tooltipHeight = tooltipHeight + 10; tooltipWidth = tooltipWidth + 20; diff --git a/src/packages/tour/components/DisableInteraction.ts b/src/packages/tour/components/DisableInteraction.ts index e70b270f3..50c9b974a 100644 --- a/src/packages/tour/components/DisableInteraction.ts +++ b/src/packages/tour/components/DisableInteraction.ts @@ -8,6 +8,7 @@ const { div } = van.tags; export type HelperLayerProps = { currentStep: State; steps: TourStep[]; + refreshes: State; targetElement: HTMLElement; helperElementPadding: number; }; @@ -15,6 +16,7 @@ export type HelperLayerProps = { export const DisableInteraction = ({ currentStep, steps, + refreshes, targetElement, helperElementPadding, }: HelperLayerProps) => { @@ -31,12 +33,17 @@ export const DisableInteraction = ({ className: disableInteractionClassName, }); - setPositionRelativeToStep( - targetElement, - disableInteraction, - step.val, - helperElementPadding - ); + van.derive(() => { + // set the position of the reference layer if the refreshes signal changes + if (!step.val || refreshes.val == undefined) return; + + setPositionRelativeToStep( + targetElement, + disableInteraction, + step.val, + helperElementPadding + ); + }); return disableInteraction; }; diff --git a/src/packages/tour/components/HelperLayer.ts b/src/packages/tour/components/HelperLayer.ts index 05c200626..821a917c2 100644 --- a/src/packages/tour/components/HelperLayer.ts +++ b/src/packages/tour/components/HelperLayer.ts @@ -31,6 +31,7 @@ const getClassName = ({ export type HelperLayerProps = { currentStep: State; steps: TourStep[]; + refreshes: State; targetElement: HTMLElement; tourHighlightClass: string; overlayOpacity: number; @@ -40,6 +41,7 @@ export type HelperLayerProps = { export const HelperLayer = ({ currentStep, steps, + refreshes, targetElement, tourHighlightClass, overlayOpacity, @@ -59,7 +61,8 @@ export const HelperLayer = ({ }); van.derive(() => { - if (!step.val) return; + // set the new position if the step or refreshes change + if (!step.val || refreshes.val === undefined) return; setPositionRelativeToStep( targetElement, diff --git a/src/packages/tour/components/ReferenceLayer.ts b/src/packages/tour/components/ReferenceLayer.ts index 74fee132d..516e3712e 100644 --- a/src/packages/tour/components/ReferenceLayer.ts +++ b/src/packages/tour/components/ReferenceLayer.ts @@ -22,12 +22,17 @@ export const ReferenceLayer = ({ TourTooltip(props) ); - setPositionRelativeToStep( - targetElement, - referenceLayer, - props.step, - helperElementPadding - ); + van.derive(() => { + // set the position of the reference layer if the refreshes signal changes + if (props.refreshes.val == undefined) return; + + setPositionRelativeToStep( + targetElement, + referenceLayer, + props.step, + helperElementPadding + ); + }); return referenceLayer; }; diff --git a/src/packages/tour/components/TourRoot.ts b/src/packages/tour/components/TourRoot.ts index 2754b716f..52bb69c11 100644 --- a/src/packages/tour/components/TourRoot.ts +++ b/src/packages/tour/components/TourRoot.ts @@ -15,12 +15,14 @@ export type TourRootProps = { }; export const TourRoot = ({ tour }: TourRootProps) => { - const currentStep = tour.getCurrentStepSignal(); + const currentStepSignal = tour.getCurrentStepSignal(); + const refreshesSignal = tour.getRefreshesSignal(); const steps = tour.getSteps(); const helperLayer = HelperLayer({ - currentStep, + currentStep: currentStepSignal, steps, + refreshes: refreshesSignal, targetElement: tour.getTargetElement(), tourHighlightClass: tour.getOption("highlightClass"), overlayOpacity: tour.getOption("overlayOpacity"), @@ -42,12 +44,14 @@ export const TourRoot = ({ tour }: TourRootProps) => { () => { // do not remove this check, it is necessary for this state-binding to work // and render the entire section every time the state changes - if (currentStep.val === undefined) { + if (currentStepSignal.val === undefined) { return null; } const step = van.derive(() => - currentStep.val !== undefined ? steps[currentStep.val] : null + currentStepSignal.val !== undefined + ? steps[currentStepSignal.val] + : null ); if (!step.val) { @@ -65,6 +69,7 @@ export const TourRoot = ({ tour }: TourRootProps) => { const referenceLayer = ReferenceLayer({ step: step.val, targetElement: tour.getTargetElement(), + refreshes: refreshesSignal, helperElementPadding: tour.getOption("helperElementPadding"), transitionDuration: tooltipTransitionDuration, @@ -74,7 +79,7 @@ export const TourRoot = ({ tour }: TourRootProps) => { showStepNumbers: tour.getOption("showStepNumbers"), steps: tour.getSteps(), - currentStep: currentStep.val, + currentStep: currentStepSignal.val, onBulletClick: (stepNumber: number) => { tour.goToStep(stepNumber); @@ -144,8 +149,9 @@ export const TourRoot = ({ tour }: TourRootProps) => { const disableInteraction = step.val.disableInteraction ? DisableInteraction({ - currentStep, + currentStep: currentStepSignal, steps: tour.getSteps(), + refreshes: refreshesSignal, targetElement: tour.getTargetElement(), helperElementPadding: tour.getOption("helperElementPadding"), }) @@ -162,7 +168,7 @@ export const TourRoot = ({ tour }: TourRootProps) => { van.derive(() => { // to clean up the root element when the tour is done - if (currentStep.val === undefined) { + if (currentStepSignal.val === undefined) { opacity.val = 0; setTimeout(() => { @@ -172,6 +178,7 @@ export const TourRoot = ({ tour }: TourRootProps) => { }); setTimeout(() => { + // fade in the root element opacity.val = 1; }, 1); diff --git a/src/packages/tour/components/TourTooltip.ts b/src/packages/tour/components/TourTooltip.ts index 0296540ce..903e2d087 100644 --- a/src/packages/tour/components/TourTooltip.ts +++ b/src/packages/tour/components/TourTooltip.ts @@ -1,5 +1,5 @@ import { Tooltip, type TooltipProps } from "../../tooltip/tooltip"; -import van, { PropValueOrDerived } from "../../dom/van"; +import van, { PropValueOrDerived, State } from "../../dom/van"; import { activeClassName, bulletsClassName, @@ -372,7 +372,7 @@ const scroll = ({ export type TourTooltipProps = Omit< TooltipProps, - "hintMode" | "position" | "targetOffset" + "hintMode" | "position" | "element" > & { step: TourStep; steps: TourStep[]; @@ -448,7 +448,6 @@ export const TourTooltip = ({ const title = step.title; const text = step.intro; const position = step.position; - const targetOffset = getOffset(step.element as HTMLElement); children.push(Header({ title, skipLabel, onSkipClick })); @@ -496,9 +495,9 @@ export const TourTooltip = ({ const tooltip = Tooltip( { ...props, + element: step.element as HTMLElement, hintMode: false, position, - targetOffset, }, children ); diff --git a/src/packages/tour/onResize.ts b/src/packages/tour/onResize.ts deleted file mode 100644 index 31e011dcd..000000000 --- a/src/packages/tour/onResize.ts +++ /dev/null @@ -1,6 +0,0 @@ -import refresh from "./refresh"; -import { Tour } from "./tour"; - -export default function onResize(tour: Tour) { - refresh(tour); -} diff --git a/src/packages/tour/refresh.ts b/src/packages/tour/refresh.ts deleted file mode 100644 index 2181c17fc..000000000 --- a/src/packages/tour/refresh.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { placeTooltip } from "../../packages/tooltip"; -import { Tour } from "./tour"; -import { - getElementByClassName, - queryElementByClassName, -} from "../../util/queryElement"; -import { - disableInteractionClassName, - helperLayerClassName, - tooltipReferenceLayerClassName, -} from "./classNames"; -import { setPositionRelativeToStep } from "./position"; -import { fetchSteps } from "./steps"; - -/** - * Update placement of the intro objects on the screen - * @api private - */ -export default function refresh(tour: Tour, refreshSteps?: boolean) { - const currentStep = tour.getCurrentStep(); - - if (currentStep === undefined || currentStep === null || currentStep == -1) { - return; - } - - const step = tour.getStep(currentStep); - - const referenceLayer = getElementByClassName(tooltipReferenceLayerClassName); - const helperLayer = getElementByClassName(helperLayerClassName); - const disableInteractionLayer = queryElementByClassName( - disableInteractionClassName - ); - - // re-align intros - const targetElement = tour.getTargetElement(); - const helperLayerPadding = tour.getOption("helperElementPadding"); - setPositionRelativeToStep( - targetElement, - helperLayer, - step, - helperLayerPadding - ); - setPositionRelativeToStep( - targetElement, - referenceLayer, - step, - helperLayerPadding - ); - - // not all steps have a disableInteractionLayer - if (disableInteractionLayer) { - setPositionRelativeToStep( - targetElement, - disableInteractionLayer, - step, - helperLayerPadding - ); - } - - if (refreshSteps) { - tour.setSteps(fetchSteps(tour)); - // TODO: how to refresh the tooltip here? do we need to convert the steps into a state? - } - - // re-align tooltip - const oldArrowLayer = document.querySelector(".introjs-arrow"); - const oldTooltipContainer = - document.querySelector(".introjs-tooltip"); - - if (oldTooltipContainer && oldArrowLayer) { - placeTooltip( - oldTooltipContainer, - oldArrowLayer, - step.element as HTMLElement, - step.position, - tour.getOption("positionPrecedence"), - tour.getOption("showStepNumbers"), - tour.getOption("autoPosition"), - step.tooltipClass ?? tour.getOption("tooltipClass") - ); - } - - return tour; -} diff --git a/src/packages/tour/tour.ts b/src/packages/tour/tour.ts index f57bc7922..72ba003d3 100644 --- a/src/packages/tour/tour.ts +++ b/src/packages/tour/tour.ts @@ -1,4 +1,4 @@ -import { nextStep, previousStep, TourStep } from "./steps"; +import { fetchSteps, nextStep, previousStep, TourStep } from "./steps"; import { Package } from "../package"; import { introAfterChangeCallback, @@ -16,11 +16,9 @@ import { start } from "./start"; import exitIntro from "./exitIntro"; import isFunction from "../../util/isFunction"; import { getDontShowAgain, setDontShowAgain } from "./dontShowAgain"; -import refresh from "./refresh"; import { getContainerElement } from "../../util/containerElement"; import DOMEvent from "../../util/DOMEvent"; import onKeyDown from "./onKeyDown"; -import onResize from "./onResize"; import van from "../dom/van"; import { TourRoot } from "./components/TourRoot"; import { FloatingElement } from "./components/FloatingElement"; @@ -31,6 +29,8 @@ import { FloatingElement } from "./components/FloatingElement"; export class Tour implements Package { private _steps: TourStep[] = []; private _currentStep = van.state(undefined); + private _refreshes = van.state(0); + private _root: Element | undefined; private _direction: "forward" | "backward"; private readonly _targetElement: HTMLElement; private _options: TourOptions; @@ -173,6 +173,14 @@ export class Tour implements Package { return this._currentStep; } + /** + * Returns the underlying state of the refreshes + * This is an internal method and should not be used outside of the package. + */ + getRefreshesSignal() { + return this._refreshes; + } + /** * Get the current step of the tour */ @@ -376,7 +384,7 @@ export class Tour implements Package { * Enable refresh on window resize for the tour */ enableRefreshOnResize() { - this._refreshOnResizeHandler = (_: Event) => onResize(this); + this._refreshOnResizeHandler = (_: Event) => this.refresh(); DOMEvent.on(window, "resize", this._refreshOnResizeHandler, true); } @@ -412,7 +420,18 @@ export class Tour implements Package { * Create the root element for the tour */ private createRoot() { - van.add(this.getTargetElement(), TourRoot({ tour: this })); + this._root = TourRoot({ tour: this }); + van.add(this.getTargetElement(), this._root); + } + + /** + * Deletes the root element and recreates it + */ + private recreateRoot() { + if (this._root) { + this._root.remove(); + this.createRoot(); + } } /** @@ -446,7 +465,22 @@ export class Tour implements Package { * @param {boolean} refreshSteps whether to refresh the tour steps */ refresh(refreshSteps?: boolean) { - refresh(this, refreshSteps); + const currentStep = this.getCurrentStep(); + + if (currentStep === undefined) { + return this; + } + + if (this._refreshes.val !== undefined) { + this._refreshes.val += 1; + } + + // fetch new steps and recreate the root element + if (refreshSteps) { + this.setSteps(fetchSteps(this)); + this.recreateRoot(); + } + return this; }