UNPKG

terriajs

Version:

Geospatial data visualization platform.

173 lines 11 kB
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; /** * TourPortal.jsx * Framework for tour * * Not a real "portal" in the sense of a react portal, even though it * started out as wanting to be that. Our not-yet-invented "new modal system" * will probably utilise a react portal, though. * * TODO: loop through configparameters for ability to customise at runtime * , then add docs for customisation */ import { autorun } from "mobx"; import { observer } from "mobx-react"; import PropTypes from "prop-types"; import { useEffect } from "react"; import { useTranslation } from "react-i18next"; import { useTheme, withTheme } from "styled-components"; import Box from "../../Styled/Box"; import Button from "../../Styled/Button"; import Spacing from "../../Styled/Spacing"; import Text from "../../Styled/Text"; import { parseCustomMarkdownToReactWithOptions } from "../Custom/parseCustomMarkdownToReact"; import Caret from "../Generic/Caret"; import CloseButton from "../Generic/CloseButton"; import { useWindowSize } from "../Hooks/useWindowSize"; import { useViewState } from "../Context"; import { applyTranslationIfExists } from "../../Language/languageHelpers"; import { calculateLeftPosition, calculateTopPosition, getOffsetsFromTourPoint } from "./tour-helpers.ts"; import TourExplanationBox, { TourExplanationBoxZIndex } from "./TourExplanationBox"; import TourIndicator from "./TourIndicator"; import TourOverlay from "./TourOverlay.jsx"; import TourPrefaceBox from "./TourPrefaceBox"; import TourProgressDot from "./TourProgressDot.jsx"; /** * Indicator bar/"dots" on progress of tour. * Fill in indicator dot depending on progress determined from count & max count */ const TourProgress = ({ max, step, setTourIndex }) => { const countArray = Array.from(Array(max).keys()).map((e) => e++); const countStep = step; return (_jsx(Box, { centered: true, children: countArray.map((count) => { return (_jsx(TourProgressDot, { onClick: () => setTourIndex(count), active: count < countStep }, count)); }) })); }; TourProgress.propTypes = { setTourIndex: PropTypes.func.isRequired, max: PropTypes.number.isRequired, step: PropTypes.number.isRequired }; export const TourExplanation = ({ topStyle, leftStyle, caretOffsetTop, caretOffsetLeft, indicatorOffsetTop, indicatorOffsetLeft, setTourIndex, onTourIndicatorClick, onPrevious, onNext, onSkip, currentStep, maxSteps, active, isFirstTourPoint, isLastTourPoint, children }) => { const { t } = useTranslation(); const theme = useTheme(); if (!active) { // Tour explanation requires the various positioning even if only just // showing the "tour indicator" button, as it is offset against the caret // which is offset against the original box return (_jsx(Box, { position: "absolute", style: { zIndex: TourExplanationBoxZIndex - 1, top: topStyle, left: leftStyle }, children: _jsx(Box, { position: "absolute", style: { top: `${caretOffsetTop}px`, left: `${caretOffsetLeft}px` }, children: _jsx(TourIndicator, { onClick: onTourIndicatorClick, style: { top: `${indicatorOffsetTop}px`, left: `${indicatorOffsetLeft}px` } }) }) })); } return (_jsxs(TourExplanationBox, { paddedRatio: 3, column: true, style: { top: topStyle, left: leftStyle }, children: [_jsx(CloseButton, { color: theme.darkWithOverlay, topRight: true, onClick: () => onSkip?.() }), _jsx(Spacing, { bottom: 2 }), _jsx(Caret, { style: { top: `${caretOffsetTop}px`, left: `${caretOffsetLeft}px` } }), _jsxs(Text, { light: true, medium: true, textDarker: true, children: [_jsx(Text, { light: true, medium: true, noFontSize: true, textDarker: true, children: children }), _jsx(Spacing, { bottom: 3 }), _jsxs(Box, { centered: true, justifySpaceBetween: true, children: [_jsx(TourProgress, { setTourIndex: setTourIndex, step: currentStep, max: maxSteps }), _jsxs(Box, { centered: true, children: [!isFirstTourPoint && (_jsxs(_Fragment, { children: [_jsx(Button, { secondary: true, shortMinHeight: true, onClick: () => onPrevious?.(), children: t("general.back") }), _jsx(Spacing, { right: 2 })] })), isLastTourPoint ? (_jsx(Button, { primary: true, shortMinHeight: true, onClick: () => onSkip?.(), children: t("tour.finish") })) : (_jsx(Button, { primary: true, shortMinHeight: true, onClick: () => onNext?.(), children: t("general.next") }))] })] })] })] })); }; TourExplanation.propTypes = { children: PropTypes.node.isRequired, currentStep: PropTypes.number.isRequired, maxSteps: PropTypes.number.isRequired, caretOffsetTop: PropTypes.number, caretOffsetLeft: PropTypes.number, indicatorOffsetTop: PropTypes.number, indicatorOffsetLeft: PropTypes.number, setTourIndex: PropTypes.func.isRequired, onTourIndicatorClick: PropTypes.func.isRequired, onPrevious: PropTypes.func.isRequired, onNext: PropTypes.func.isRequired, onSkip: PropTypes.func.isRequired, topStyle: PropTypes.string, leftStyle: PropTypes.string, isFirstTourPoint: PropTypes.bool.isRequired, isLastTourPoint: PropTypes.bool.isRequired, active: PropTypes.bool }; const TourGrouping = observer(({ tourPoints }) => { const { i18n } = useTranslation(); const viewState = useViewState(); const currentTourPoint = tourPoints[viewState.currentTourIndex]; const currentTourPointRef = viewState.appRefs.get(currentTourPoint?.appRefName); const currentRectangle = currentTourPointRef?.current?.getBoundingClientRect?.(); if (!currentRectangle) { console.log("Tried to show guidance portal with no rectangle available from ref"); } return (_jsxs(_Fragment, { children: [currentRectangle && (_jsx(TourOverlay, { rectangle: currentRectangle, onCancel: () => viewState.nextTourPoint() })), tourPoints.map((tourPoint, index) => { const tourPointRef = viewState.appRefs.get(tourPoint?.appRefName); const currentRectangle = tourPointRef?.current?.getBoundingClientRect?.(); const { offsetTop, offsetLeft, caretOffsetTop, caretOffsetLeft, indicatorOffsetTop, indicatorOffsetLeft } = getOffsetsFromTourPoint(tourPoint); // To match old HelpScreenWindow / HelpOverlay API const currentScreen = { rectangle: currentRectangle, positionTop: tourPoint?.positionTop || viewState.relativePosition.RECT_BOTTOM, positionLeft: tourPoint?.positionLeft || viewState.relativePosition.RECT_LEFT, offsetTop: offsetTop, offsetLeft: offsetLeft }; const positionLeft = calculateLeftPosition(currentScreen); const positionTop = calculateTopPosition(currentScreen); const currentTourIndex = viewState.currentTourIndex; const maxSteps = tourPoints.length; if (!tourPoint) return null; return (_jsx(TourExplanation, { active: currentTourIndex === index, currentStep: currentTourIndex + 1, maxSteps: maxSteps, setTourIndex: (idx) => viewState.setTourIndex(idx), onTourIndicatorClick: () => viewState.setTourIndex(index), onPrevious: () => viewState.previousTourPoint(), onNext: () => viewState.nextTourPoint(), onSkip: () => viewState.closeTour(), isFirstTourPoint: index === 0, isLastTourPoint: index === tourPoints.length - 1, topStyle: `${positionTop}px`, leftStyle: `${positionLeft}px`, caretOffsetTop: caretOffsetTop, caretOffsetLeft: caretOffsetLeft, indicatorOffsetTop: indicatorOffsetTop, indicatorOffsetLeft: indicatorOffsetLeft, children: parseCustomMarkdownToReactWithOptions(applyTranslationIfExists(tourPoint?.content, i18n), { injectTermsAsTooltips: true, tooltipTerms: viewState.terria.configParameters.helpContentTerms }) }, tourPoint.appRefName)); })] })); }); export const TourPreface = () => { const { t } = useTranslation(); const viewState = useViewState(); const theme = useTheme(); return (_jsxs(_Fragment, { children: [_jsx(TourPrefaceBox, { onClick: () => viewState.closeTour(), role: "presentation", "aria-hidden": "true", pseudoBg: true }), _jsxs(TourExplanationBox, { longer: true, paddedRatio: 4, column: true, style: { right: 25, bottom: 45 }, children: [_jsx(CloseButton, { color: theme.darkWithOverlay, // color={"green"} topRight: true, onClick: () => viewState.closeTour() }), _jsx(Spacing, { bottom: 2 }), _jsx(Text, { extraExtraLarge: true, bold: true, textDarker: true, children: t("tour.preface.title") }), _jsx(Spacing, { bottom: 3 }), _jsx(Text, { light: true, medium: true, textDarker: true, children: t("tour.preface.content") }), _jsx(Spacing, { bottom: 4 }), _jsx(Text, { medium: true, children: _jsxs(Box, { children: [_jsx(Button, { fullWidth: true, secondary: true, onClick: (e) => { e.stopPropagation(); viewState.closeTour(); }, children: t("tour.preface.close") }), _jsx(Spacing, { right: 3 }), _jsx(Button, { primary: true, fullWidth: true, textProps: { noFontSize: true }, onClick: (e) => { e.stopPropagation(); viewState.setShowTour(true); }, children: t("tour.preface.start") })] }) }), _jsx(Spacing, { bottom: 1 })] })] })); }; export const TourPortalDisplayName = "TourPortal"; export const TourPortal = observer(() => { const viewState = useViewState(); const showPortal = viewState.currentTourIndex !== -1; const showPreface = showPortal && !viewState.showTour; // should we bump up the debounce here? feels like 16ms is quite aggressive // and almost to the point of not debouncing at all, but the render logic // is quite cheap so it makes for a better resizing/zooming experience const width = useWindowSize({ debounceOverride: 16 }); useEffect(() => autorun(() => { if (showPortal && viewState.topElement !== TourPortalDisplayName) { viewState.setTopElement(TourPortalDisplayName); } })); if (viewState.useSmallScreenInterface || !showPortal) { return null; } if (showPreface) { return _jsx(TourPreface, { viewState: viewState }); } return (_jsx(TourGrouping, { viewState: viewState, tourPoints: viewState.tourPointsWithValidRefs }, width)); }); TourPortal.propTypes = { children: PropTypes.node }; export default withTheme(TourPortal); //# sourceMappingURL=TourPortal.js.map