gristlabs_grist-core/app/client/ui/DocTour.ts

111 lines
3.6 KiB
TypeScript
Raw Permalink Normal View History

import {Placement} from '@popperjs/core';
import {placements} from '@popperjs/core/lib/enums';
import {DocComm} from 'app/client/components/DocComm';
import {makeT} from 'app/client/lib/localization';
import {sameDocumentUrlState} from 'app/client/models/gristUrlState';
import {cssButtons, cssLinkBtn, cssLinkIcon} from 'app/client/ui/ExampleCard';
import {IOnBoardingMsg, startOnBoarding} from 'app/client/ui/OnBoardingPopups';
import {isNarrowScreen} from 'app/client/ui2018/cssVars';
import {IconList, IconName} from 'app/client/ui2018/IconList';
import {DocData} from 'app/common/DocData';
import {dom} from 'grainjs';
import sortBy = require('lodash/sortBy');
const t = makeT('DocTour');
export async function startDocTour(docData: DocData, docComm: DocComm, onFinishCB: () => void) {
const docTour: IOnBoardingMsg[] = await makeDocTour(docData, docComm) || invalidDocTour;
exposeDocTour(docTour);
startOnBoarding(docTour, onFinishCB);
}
const invalidDocTour: IOnBoardingMsg[] = [{
title: t("No valid document tour"),
body: t("Cannot construct a document tour from the data in this document. \
Ensure there is a table named GristDocTour with columns Title, Body, Placement, and Location."),
selector: 'document',
showHasModal: true,
}];
async function makeDocTour(docData: DocData, docComm: DocComm): Promise<IOnBoardingMsg[] | null> {
const tableId = "GristDocTour";
if (!docData.getTable(tableId)) {
return null;
}
// Make sure any formulas in GristDocTour table have had time to evaluate. For example, for a
// first time open of a new document copy, any use of SELF_HYPERLINK will be stale since the URL
// of the document has changed.
await docComm.waitForInitialization();
await docData.fetchTable(tableId);
const tableData = docData.getTable(tableId)!;
const result = sortBy(tableData.getRowIds(), tableData.getRowPropFunc('manualSort') as any).map(rowId => {
function getValue(colId: string): string {
return String(tableData.getValue(rowId, colId) || "");
}
const title = getValue("Title");
let body: HTMLElement | string = getValue("Body");
const linkText = getValue("Link_Text");
const linkUrl = getValue("Link_URL");
const linkIcon = getValue("Link_Icon") as IconName;
const locationValue = getValue("Location");
let placement = getValue("Placement");
if (!(title || body)) {
return null;
}
const urlState = sameDocumentUrlState(locationValue);
if (isNarrowScreen() || !placements.includes(placement as Placement)) {
placement = "auto";
}
let validLinkUrl = true;
try {
new URL(linkUrl);
} catch {
validLinkUrl = false;
}
if (validLinkUrl && linkText) {
body = dom(
'div',
dom('p', body),
dom('p',
cssButtons(cssLinkBtn(
IconList.includes(linkIcon) ? cssLinkIcon(linkIcon) : null,
linkText,
{href: linkUrl, target: '_blank'},
))
),
);
}
return {
title,
body,
placement,
urlState,
selector: '.active_cursor',
// Center the popup if the user doesn't provide a link to a cell
showHasModal: !urlState?.hash
};
}).filter(x => x !== null) as IOnBoardingMsg[];
if (!result.length) {
return null;
}
return result;
}
// for easy testing
function exposeDocTour(docTour: IOnBoardingMsg[]) {
(window as any)._gristDocTour = () =>
docTour.map(msg => ({
...msg,
body: typeof msg.body === "string" ? msg.body
: (msg.body as HTMLElement)?.outerHTML
.replace(/_grain\d+_/g, "_grainXXX_"),
urlState: msg.urlState?.hash
}));
}