From 9c6b72b7c0dddf24615287d3a718fac236163d87 Mon Sep 17 00:00:00 2001 From: binrysearch Date: Sun, 8 Sep 2024 15:55:59 +0100 Subject: [PATCH] HintsRoot --- src/packages/hint/components/HintIcon.ts | 59 +++++++++ .../HintTooltip.ts} | 7 +- src/packages/hint/components/HintsRoot.ts | 86 +++++++++++++ .../hint/components/ReferenceLayer.ts | 48 ++++++++ src/packages/hint/hide.ts | 2 +- src/packages/hint/hint.ts | 10 ++ src/packages/hint/render.ts | 85 ++++++------- src/packages/hint/tooltip.ts | 115 +++++++++--------- 8 files changed, 311 insertions(+), 101 deletions(-) create mode 100644 src/packages/hint/components/HintIcon.ts rename src/packages/hint/{hintTooltip.ts => components/HintTooltip.ts} (81%) create mode 100644 src/packages/hint/components/HintsRoot.ts create mode 100644 src/packages/hint/components/ReferenceLayer.ts diff --git a/src/packages/hint/components/HintIcon.ts b/src/packages/hint/components/HintIcon.ts new file mode 100644 index 000000000..0e0ba7470 --- /dev/null +++ b/src/packages/hint/components/HintIcon.ts @@ -0,0 +1,59 @@ +import isFixed from "../../../util/isFixed"; +import van from "../../dom/van"; +import { + fixedHintClassName, + hintClassName, + hintDotClassName, + hintNoAnimationClassName, + hintPulseClassName, +} from "../className"; +import { HintItem, HintPosition } from "../hintItem"; +import { dataStepAttribute } from "../dataAttributes"; +import { alignHintPosition } from "../position"; + +const { a, div } = van.tags; + +export type HintProps = { + index: number; + hintItem: HintItem; + onClick: (e: any) => void; +}; + +const className = (hintItem: HintItem) => { + const classNames = [hintClassName]; + + if (!hintItem.hintAnimation) { + classNames.push(hintNoAnimationClassName); + } + + if (isFixed(hintItem.element as HTMLElement)) { + classNames.push(fixedHintClassName); + } + + return classNames.join(" "); +}; + +const HintDot = () => div({ className: hintDotClassName }); +const HintPulse = () => div({ className: hintPulseClassName }); + +export const HintIcon = ({ index, hintItem, onClick }: HintProps) => { + const hintElement = a( + { + [dataStepAttribute]: index.toString(), + className: className(hintItem), + role: "button", + tabindex: 0, + onclick: onClick, + }, + HintDot(), + HintPulse() + ); + + alignHintPosition( + hintItem.hintPosition as HintPosition, + hintElement, + hintItem.element as HTMLElement + ); + + return hintElement; +}; diff --git a/src/packages/hint/hintTooltip.ts b/src/packages/hint/components/HintTooltip.ts similarity index 81% rename from src/packages/hint/hintTooltip.ts rename to src/packages/hint/components/HintTooltip.ts index 0f83426dc..c07b6e330 100644 --- a/src/packages/hint/hintTooltip.ts +++ b/src/packages/hint/components/HintTooltip.ts @@ -1,6 +1,7 @@ -import { Tooltip, TooltipProps } from "../tooltip/tooltip"; -import van from "../dom/van"; -import { tooltipTextClassName } from "./className"; +import { Tooltip, TooltipProps } from "../../tooltip/tooltip"; +import van from "../../dom/van"; +import { tooltipTextClassName } from "../className"; +import { HintItem } from "../hintItem"; const { a, p, div } = van.tags; diff --git a/src/packages/hint/components/HintsRoot.ts b/src/packages/hint/components/HintsRoot.ts new file mode 100644 index 000000000..aac3f4886 --- /dev/null +++ b/src/packages/hint/components/HintsRoot.ts @@ -0,0 +1,86 @@ +import van from "../../dom/van"; +import { hintsClassName } from "../className"; +import { hideHint } from "../hide"; +import { Hint } from "../hint"; +import { showHintDialog } from "../tooltip"; +import { HintIcon } from "./HintIcon"; +import { ReferenceLayer } from "./ReferenceLayer"; + +const { div } = van.tags; + +export type HintsRootProps = { + hint: Hint; +}; + +/** + * Returns an event handler unique to the hint iteration + */ +const getHintClick = (hint: Hint, i: number) => (e: Event) => { + const evt = e ? e : window.event; + + if (evt && evt.stopPropagation) { + evt.stopPropagation(); + } + + if (evt && evt.cancelBubble !== null) { + evt.cancelBubble = true; + } + + showHintDialog(hint, i); +}; + +export const HintsRoot = ({ hint }: HintsRootProps) => { + const hintElements = []; + + for (const [i, hintItem] of hint.getHints().entries()) { + hintElements.push( + HintIcon({ + index: i, + hintItem, + onClick: getHintClick(hint, i), + }) + ); + } + + const root = div( + { + className: hintsClassName, + }, + ...hintElements + ); + + van.derive(() => { + if (hint._activeHintSignal.val === undefined) return; + + const stepId = hint._activeHintSignal.val; + const hints = hint.getHints(); + const hintItem = hints[stepId]; + + const referenceLayer = ReferenceLayer({ + activeHintSignal: hint._activeHintSignal, + text: hintItem.hint || "", + element: hintItem.element as HTMLElement, + position: hintItem.position, + + helperElementPadding: hint.getOption("helperElementPadding"), + targetElement: hint.getTargetElement(), + + refreshes: hint._refreshes, + + // hints don't have step numbers + showStepNumbers: false, + + autoPosition: hint.getOption("autoPosition"), + positionPrecedence: hint.getOption("positionPrecedence"), + + closeButtonEnabled: hint.getOption("hintShowButton"), + closeButtonLabel: hint.getOption("hintButtonLabel"), + closeButtonClassName: hint.getOption("buttonClass"), + closeButtonOnClick: () => hideHint(hint, stepId), + }); + + van.add(root, referenceLayer); + }); + + return root; +}; diff --git a/src/packages/hint/components/ReferenceLayer.ts b/src/packages/hint/components/ReferenceLayer.ts new file mode 100644 index 000000000..3726e6355 --- /dev/null +++ b/src/packages/hint/components/ReferenceLayer.ts @@ -0,0 +1,48 @@ +import { setPositionRelativeTo } from "../../../util/positionRelativeTo"; +import van, { State } from "../../dom/van"; +import { + hintReferenceClassName, + tooltipReferenceLayerClassName, +} from "../className"; +import { dataStepAttribute } from "../dataAttributes"; +import { HintTooltip, HintTooltipProps } from "./HintTooltip"; + +const { div } = van.tags; + +export type ReferenceLayerProps = HintTooltipProps & { + activeHintSignal: State; + targetElement: HTMLElement; + helperElementPadding: number; +}; + +export const ReferenceLayer = ({ + activeHintSignal, + targetElement, + helperElementPadding, + ...props +}: ReferenceLayerProps) => { + return () => { + // remove the reference layer if the active hint signal is set to undefined + // e.g. when the user clicks outside the hint + if (activeHintSignal.val == undefined) return null; + + const referenceLayer = div( + { + [dataStepAttribute]: activeHintSignal.val, + className: `${tooltipReferenceLayerClassName} ${hintReferenceClassName}`, + }, + HintTooltip(props) + ); + + setTimeout(() => { + setPositionRelativeTo( + targetElement, + referenceLayer, + props.element as HTMLElement, + helperElementPadding + ); + }, 1); + + return referenceLayer; + }; +}; diff --git a/src/packages/hint/hide.ts b/src/packages/hint/hide.ts index 029007339..d7b183434 100644 --- a/src/packages/hint/hide.ts +++ b/src/packages/hint/hide.ts @@ -13,7 +13,7 @@ import { hintElement, hintElements } from "./selector"; export async function hideHint(hint: Hint, stepId: number) { const element = hintElement(stepId); - removeHintTooltip(); + //removeHintTooltip(); if (element) { addClass(element, hideHintClassName); diff --git a/src/packages/hint/hint.ts b/src/packages/hint/hint.ts index 29819ff21..df35aa6e7 100644 --- a/src/packages/hint/hint.ts +++ b/src/packages/hint/hint.ts @@ -12,6 +12,8 @@ import { hideHint, hideHints } from "./hide"; import { showHint, showHints } from "./show"; import { removeHint, removeHints } from "./remove"; import { showHintDialog } from "./tooltip"; +import van from "../dom/van"; +import { HintsRoot } from "./components/HintsRoot"; type hintsAddedCallback = (this: Hint) => void | Promise; type hintClickCallback = ( @@ -26,6 +28,8 @@ export class Hint implements Package { private _hints: HintItem[] = []; private readonly _targetElement: HTMLElement; private _options: HintOptions; + public _activeHintSignal = van.state(undefined); + public _refreshes = van.state(0); private readonly callbacks: { hintsAdded?: hintsAddedCallback; @@ -105,6 +109,10 @@ export class Hint implements Package { return this; } + private createRoot() { + van.add(this._targetElement, HintsRoot({ hint: this })); + } + /** * Render hints on the page */ @@ -115,6 +123,7 @@ export class Hint implements Package { fetchHintItems(this); await renderHints(this); + this.createRoot(); return this; } @@ -193,6 +202,7 @@ export class Hint implements Package { * @param stepId The hint step ID */ async showHintDialog(stepId: number) { + this._activeHintSignal.val = stepId; await showHintDialog(this, stepId); return this; } diff --git a/src/packages/hint/render.ts b/src/packages/hint/render.ts index 92e71a86b..e5da1e301 100644 --- a/src/packages/hint/render.ts +++ b/src/packages/hint/render.ts @@ -16,6 +16,7 @@ import { addClass } from "../../util/className"; import isFixed from "../../util/isFixed"; import { alignHintPosition } from "./position"; import { showHintDialog } from "./tooltip"; +import { HintsRoot } from "./components/HintsRoot"; /** * Returns an event handler unique to the hint iteration @@ -48,60 +49,62 @@ export async function renderHints(hint: Hint) { }); } - const hints = hint.getHints(); - for (let i = 0; i < hints.length; i++) { - const hintItem = hints[i]; + //const hints = hint.getHints(); + //for (let i = 0; i < hints.length; i++) { + // const hintItem = hints[i]; - // avoid append a hint twice - if (queryElement(`.${hintClassName}[${dataStepAttribute}="${i}"]`)) { - return; - } + // // avoid append a hint twice + // if (queryElement(`.${hintClassName}[${dataStepAttribute}="${i}"]`)) { + // return; + // } - const hintElement = createElement("a", { - className: hintClassName, - }); - setAnchorAsButton(hintElement); + // const hintElement = createElement("a", { + // className: hintClassName, + // }); + // setAnchorAsButton(hintElement); - hintElement.onclick = getHintClick(hint, i); + // hintElement.onclick = getHintClick(hint, i); - if (!hintItem.hintAnimation) { - addClass(hintElement, hintNoAnimationClassName); - } + // if (!hintItem.hintAnimation) { + // addClass(hintElement, hintNoAnimationClassName); + // } - // hint's position should be fixed if the target element's position is fixed - if (isFixed(hintItem.element as HTMLElement)) { - addClass(hintElement, fixedHintClassName); - } + // // hint's position should be fixed if the target element's position is fixed + // if (isFixed(hintItem.element as HTMLElement)) { + // addClass(hintElement, fixedHintClassName); + // } - const hintDot = createElement("div", { - className: hintDotClassName, - }); + // const hintDot = createElement("div", { + // className: hintDotClassName, + // }); - const hintPulse = createElement("div", { - className: hintPulseClassName, - }); + // const hintPulse = createElement("div", { + // className: hintPulseClassName, + // }); - hintElement.appendChild(hintDot); - hintElement.appendChild(hintPulse); - hintElement.setAttribute(dataStepAttribute, i.toString()); + // hintElement.appendChild(hintDot); + // hintElement.appendChild(hintPulse); + // hintElement.setAttribute(dataStepAttribute, i.toString()); - // we swap the hint element with target element - // because _setHelperLayerPosition uses `element` property - hintItem.hintTargetElement = hintItem.element as HTMLElement; - hintItem.element = hintElement; + // // we swap the hint element with target element + // // because _setHelperLayerPosition uses `element` property + // hintItem.hintTargetElement = hintItem.element as HTMLElement; + // hintItem.element = hintElement; - // align the hint position - alignHintPosition( - hintItem.hintPosition as HintPosition, - hintElement, - hintItem.hintTargetElement as HTMLElement - ); + // // align the hint position + // alignHintPosition( + // hintItem.hintPosition as HintPosition, + // hintElement, + // hintItem.hintTargetElement as HTMLElement + // ); - hintsWrapper.appendChild(hintElement); - } + // hintsWrapper.appendChild(hintElement); + //} + + //HintsRoot({ hint }); // adding the hints wrapper - document.body.appendChild(hintsWrapper); + //document.body.appendChild(HintsRoot({ hint })); // call the callback function (if any) hint.callback("hintsAdded")?.call(hint); diff --git a/src/packages/hint/tooltip.ts b/src/packages/hint/tooltip.ts index b34050cab..f67c1c62e 100644 --- a/src/packages/hint/tooltip.ts +++ b/src/packages/hint/tooltip.ts @@ -12,7 +12,7 @@ import { hideHint } from "./hide"; import { setPositionRelativeTo } from "../../util/positionRelativeTo"; import DOMEvent from "../../util/DOMEvent"; import getOffset from "../../util/getOffset"; -import { HintTooltip } from "./hintTooltip"; +import { HintTooltip } from "./components/HintTooltip"; import van from "../dom/van"; // The hint close function used when the user clicks outside the hint @@ -52,6 +52,8 @@ export async function showHintDialog(hint: Hint, stepId: number) { if (!hintElement || !item) return; + hint._activeHintSignal.val = stepId; + // call the callback function (if any) await hint.callback("hintClick")?.call(hint, hintElement, item, stepId); @@ -83,62 +85,62 @@ export async function showHintDialog(hint: Hint, stepId: number) { //tooltipLayer.appendChild(tooltipTextLayer); - const step = hintElement.getAttribute(dataStepAttribute) || ""; - - // set current step for _placeTooltip function - const hintItem = hint.getHint(parseInt(step, 10)); - - if (!hintItem) return; - - const tooltipLayer = HintTooltip({ - position: van.state(hintItem.position), - text: item.hint || "", - targetOffset: van.state(getOffset(hintItem.element as HTMLElement)), - // hints don't have step numbers - showStepNumbers: false, - - autoPosition: hint.getOption("autoPosition"), - positionPrecedence: hint.getOption("positionPrecedence"), - - closeButtonEnabled: hint.getOption("hintShowButton"), - closeButtonLabel: hint.getOption("hintButtonLabel"), - closeButtonClassName: hint.getOption("buttonClass"), - closeButtonOnClick: () => hideHint(hint, stepId), - }); - - //const tooltipTextLayer = createElement("div"); - //const arrowLayer = createElement("div"); - const referenceLayer = createElement("div"); - - tooltipLayer.onclick = (e: Event) => { - //IE9 & Other Browsers - if (e.stopPropagation) { - e.stopPropagation(); - } - //IE8 and Lower - else { - e.cancelBubble = true; - } - }; + //const step = hintElement.getAttribute(dataStepAttribute) || ""; - // align reference layer position - setClass( - referenceLayer, - tooltipReferenceLayerClassName, - hintReferenceClassName - ); - referenceLayer.setAttribute(dataStepAttribute, step); - - const helperLayerPadding = hint.getOption("helperElementPadding"); - setPositionRelativeTo( - hint.getTargetElement(), - referenceLayer, - hintItem.element as HTMLElement, - helperLayerPadding - ); + //// set current step for _placeTooltip function + //const hintItem = hint.getHint(parseInt(step, 10)); + + //if (!hintItem) return; + + //const tooltipLayer = HintTooltip({ + // position: van.state(hintItem.position), + // text: item.hint || "", + // targetOffset: van.state(getOffset(hintItem.element as HTMLElement)), + // // hints don't have step numbers + // showStepNumbers: false, + + // autoPosition: hint.getOption("autoPosition"), + // positionPrecedence: hint.getOption("positionPrecedence"), + + // closeButtonEnabled: hint.getOption("hintShowButton"), + // closeButtonLabel: hint.getOption("hintButtonLabel"), + // closeButtonClassName: hint.getOption("buttonClass"), + // closeButtonOnClick: () => hideHint(hint, stepId), + //}); + + ////const tooltipTextLayer = createElement("div"); + ////const arrowLayer = createElement("div"); + //const referenceLayer = createElement("div"); + + //tooltipLayer.onclick = (e: Event) => { + // //IE9 & Other Browsers + // if (e.stopPropagation) { + // e.stopPropagation(); + // } + // //IE8 and Lower + // else { + // e.cancelBubble = true; + // } + //}; + + //// align reference layer position + //setClass( + // referenceLayer, + // tooltipReferenceLayerClassName, + // hintReferenceClassName + //); + //referenceLayer.setAttribute(dataStepAttribute, step); + + //const helperLayerPadding = hint.getOption("helperElementPadding"); + //setPositionRelativeTo( + // hint.getTargetElement(), + // referenceLayer, + // hintItem.element as HTMLElement, + // helperLayerPadding + //); - referenceLayer.appendChild(tooltipLayer); - document.body.appendChild(referenceLayer); + //referenceLayer.appendChild(tooltipLayer); + //document.body.appendChild(referenceLayer); // set proper position //placeTooltip( @@ -154,7 +156,8 @@ export async function showHintDialog(hint: Hint, stepId: number) { //); _hintCloseFunction = () => { - removeHintTooltip(); + //removeHintTooltip(); + hint._activeHintSignal.val = undefined; DOMEvent.off(document, "click", _hintCloseFunction, false); };