@eccenca/gui-elements
Version:
GUI elements based on other libraries, usable in React application, written in Typescript.
200 lines • 11.8 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.VisualTour = void 0;
const react_1 = __importDefault(require("react"));
const react_dom_1 = require("react-dom");
const core_1 = require("@blueprintjs/core");
const core_2 = require("@popperjs/core");
const constants_1 = require("../../configuration/constants");
const index_1 = require("../../index");
const highlightElementBaseClass = `${constants_1.CLASSPREFIX}-visual-tour__highlighted-element`;
/** A visual tour multi-step tour of the current view. */
const VisualTour = ({ steps, onClose, closeLabel = "Close", nextLabel = "Next", prevLabel = "Back", usableStepTarget = false, isOpen = false, }) => {
if (isOpen === false) {
return null;
}
const [currentStepIndex, setCurrentStepIndex] = react_1.default.useState(0);
const [currentStepComponent, setCurrentStepComponent] = react_1.default.useState(null);
react_1.default.useEffect(() => {
const closeTour = () => {
var _a, _b;
// clear observer and disconnect
if (lastObserver) {
lastObserver.takeRecords();
lastObserver.disconnect();
}
// empty step
setCurrentStepComponent(null);
// remove highlight classes
(_a = document.querySelector(`.${highlightElementBaseClass}`)) === null || _a === void 0 ? void 0 : _a.classList.remove(highlightElementBaseClass);
(_b = document
.querySelector(`.${highlightElementBaseClass}--useable`)) === null || _b === void 0 ? void 0 : _b.classList.remove(`${highlightElementBaseClass}--useable`);
// call callback function from outside
onClose();
};
const step = steps[currentStepIndex];
if (!step) {
// This should not happen
closeTour();
return;
}
const highlightElementClass = (typeof step["usableStepTarget"] === "undefined" ? usableStepTarget : step["usableStepTarget"])
? `${highlightElementBaseClass}--useable`
: highlightElementBaseClass;
const hasNextStep = currentStepIndex + 1 < steps.length;
const hasPreviousStep = currentStepIndex > 0;
// Configure optional highlighting
let elementToHighlight = null;
let lastObserver = null;
const setStepComponent = () => {
const stepDisplay = (react_1.default.createElement(index_1.Badge, { tagProps: { emphasis: "weaker" }, size: "large" }, ` ${currentStepIndex + 1}/${steps.length} `));
const closeButton = react_1.default.createElement(index_1.IconButton, { name: "navigation-close", text: closeLabel, onClick: closeTour });
const titleOptions = (react_1.default.createElement(react_1.default.Fragment, null,
stepDisplay,
closeButton));
const actionButtons = [
hasNextStep ? (react_1.default.createElement(index_1.Button, { key: "next", variant: "outlined", intent: "primary", onClick: () => {
setCurrentStepIndex(currentStepIndex + 1);
}, rightIcon: "navigation-next" },
nextLabel,
": ",
steps[currentStepIndex + 1].title)) : (react_1.default.createElement(index_1.Button, { key: "close", text: closeLabel, onClick: closeTour, variant: "outlined", intent: "primary", rightIcon: "navigation-close" })),
hasPreviousStep ? (react_1.default.createElement(index_1.CardActionsAux, null,
react_1.default.createElement(index_1.Button, { key: "prev", variant: "outlined", onClick: () => {
setCurrentStepIndex(currentStepIndex - 1);
}, icon: "navigation-previous" }, prevLabel))) : null,
];
// TODO: What to do if an element should have been highlighted, but none was found?
if (elementToHighlight) {
setCurrentStepComponent(react_1.default.createElement(StepPopover, { highlightedElement: elementToHighlight, titleOption: titleOptions, actionButtons: actionButtons, step: step }));
}
else {
setCurrentStepComponent(react_1.default.createElement(StepModal, { titleOption: titleOptions, actionButtons: actionButtons, step: step, onClose: closeTour }));
}
};
const addElementHighlighting = () => {
if (step.highlightElementQuery) {
const queries = typeof step.highlightElementQuery === "string"
? [step.highlightElementQuery]
: step.highlightElementQuery;
queries.forEach((query) => {
if (elementToHighlight == null) {
elementToHighlight = document.querySelector(query);
}
});
}
else {
elementToHighlight = null;
}
if (elementToHighlight) {
// Typescript for some reason incorrectly infers the type of elementToHighlight as never
elementToHighlight.classList.add(highlightElementClass);
elementToHighlight.scrollIntoView({
behavior: "smooth",
block: "center",
});
if (lastObserver) {
lastObserver.disconnect();
}
lastObserver = new MutationObserver(function () {
// Re-new element highlighting
if (step.highlightElementQuery) {
if (!document.body.contains(elementToHighlight)) {
// Element has been removed or replaced
elementToHighlight = null;
addElementHighlighting();
}
else if (!(elementToHighlight === null || elementToHighlight === void 0 ? void 0 : elementToHighlight.classList.contains(highlightElementClass))) {
// Only the classes have been removed
elementToHighlight === null || elementToHighlight === void 0 ? void 0 : elementToHighlight.classList.add(highlightElementClass);
}
}
});
lastObserver.observe(document.body, { childList: true, subtree: true });
}
setStepComponent();
};
addElementHighlighting();
return () => {
var _a;
// Remove previous element highlight
(_a = document.querySelector(`.${highlightElementClass}`)) === null || _a === void 0 ? void 0 : _a.classList.remove(highlightElementClass);
if (lastObserver) {
lastObserver.disconnect();
}
};
}, [currentStepIndex, usableStepTarget]);
return currentStepComponent;
};
exports.VisualTour = VisualTour;
// Main content of a step
const StepContent = ({ step }) => {
return (react_1.default.createElement(react_1.default.Fragment, null,
step.image && (react_1.default.createElement(react_1.default.Fragment, null,
react_1.default.createElement("img", { src: step.image }),
react_1.default.createElement(index_1.Spacing, { size: "small" }))),
typeof step.content === "string" ? react_1.default.createElement(index_1.Markdown, null, step.content) : step.content()));
};
/** Modal that is displayed for a step. */
const StepModal = ({ step, titleOption, onClose, actionButtons }) => {
var _a;
return (react_1.default.createElement(index_1.SimpleDialog, { title: step.title, headerOptions: titleOption, isOpen: true, preventSimpleClosing: true, onClose: onClose, actions: actionButtons, size: step.size === "medium" ? "regular" : (_a = step.size) !== null && _a !== void 0 ? _a : "regular", overlayClassName: `${constants_1.CLASSPREFIX}-visual-tour__dialog` },
react_1.default.createElement(StepContent, { step: step })));
};
/** Popover that is displayed and points at the highlighted element. */
const StepPopover = ({ highlightedElement, step, titleOption, actionButtons }) => {
var _a;
const tooltipRef = react_1.default.useCallback((tooltip) => {
if (tooltip) {
(0, core_2.createPopper)(highlightedElement, tooltip, {
placement: "auto",
modifiers: [
{
name: "offset",
options: {
offset: [0, 15],
},
},
],
});
}
}, [highlightedElement]);
const backdropRef = react_1.default.useCallback((backdrop) => {
const highlightStencil = () => {
const targetRect = highlightedElement.getBoundingClientRect();
backdrop.style.left = `calc(${targetRect.left + window.scrollX + "px"} - var(--${constants_1.CLASSPREFIX}-visual-tour-focus-padding))`;
backdrop.style.top = `calc(${targetRect.top + window.scrollY + "px"} - var(--${constants_1.CLASSPREFIX}-visual-tour-focus-padding))`;
backdrop.style.width = `calc(${targetRect.width + "px"} + 2 * var(--${constants_1.CLASSPREFIX}-visual-tour-focus-padding))`;
backdrop.style.height = `calc(${targetRect.height + "px"} + 2 * var(--${constants_1.CLASSPREFIX}-visual-tour-focus-padding))`;
};
if (backdrop) {
highlightStencil();
window.addEventListener("resize", highlightStencil);
return () => {
window.removeEventListener("resize", highlightStencil);
};
}
return;
}, [highlightedElement]);
return (0, react_dom_1.createPortal)(react_1.default.createElement("div", { className: `${constants_1.CLASSPREFIX}-visual-tour` },
react_1.default.createElement("div", { className: `${constants_1.CLASSPREFIX}-visual-tour__focushelper`, ref: backdropRef }),
react_1.default.createElement("div", null,
react_1.default.createElement("div", { className: `${constants_1.CLASSPREFIX}-visual-tour__backdrop` })),
react_1.default.createElement("div", { className: `${constants_1.CLASSPREFIX}-visual-tour__overlay` +
` ${constants_1.CLASSPREFIX}-visual-tour__overlay--${(_a = step.size) !== null && _a !== void 0 ? _a : "large"}` +
` ${core_1.Classes.POPOVER}`, role: "tooltip", ref: tooltipRef },
react_1.default.createElement("div", { className: `${constants_1.CLASSPREFIX}-visual-tour__arrow ${core_1.Classes.POPOVER_ARROW}`, "data-popper-arrow": true, "aria-hidden": true }),
react_1.default.createElement("div", { className: `${core_1.Classes.POPOVER_CONTENT} ${constants_1.CLASSPREFIX}-visual-tour__overlay__content` },
react_1.default.createElement(index_1.Card, { isOnlyLayout: true, elevation: -1, whitespaceAmount: "small" },
react_1.default.createElement(index_1.CardHeader, null,
react_1.default.createElement(index_1.CardTitle, null, step.title),
react_1.default.createElement(index_1.CardOptions, null, titleOption)),
react_1.default.createElement(index_1.CardContent, null,
react_1.default.createElement(StepContent, { step: step })),
react_1.default.createElement(index_1.CardActions, { inverseDirection: true }, actionButtons))))), document.body);
};
exports.default = exports.VisualTour;
//# sourceMappingURL=VisualTour.js.map