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

174 lines
5.2 KiB
TypeScript
Raw Normal View History

import {AppModel} from 'app/client/models/AppModel';
import {getUserOrgPrefObs} from 'app/client/models/UserPrefs';
import {IExampleInfo} from 'app/client/ui/ExampleInfo';
import {prepareForTransition, TransitionWatcher} from 'app/client/ui/transitions';
import {colors, testId, vars} from 'app/client/ui2018/cssVars';
import {icon} from 'app/client/ui2018/icons';
import {cssLink} from 'app/client/ui2018/links';
import {dom, Observable, styled} from 'grainjs';
// Open a popup with a card introducing this example, if the user hasn't dismissed it in the past.
export function showExampleCard(
example: IExampleInfo, appModel: AppModel, btnElem: HTMLElement, reopen: boolean = false
) {
const prefObs: Observable<number[]|undefined> = getUserOrgPrefObs(appModel, 'seenExamples');
const seenExamples = prefObs.get() || [];
// If this example was previously dismissed, don't show the card, unless the user is reopening it.
if (!reopen && seenExamples.includes(example.id)) {
return;
}
// When an example card is closed, if it's the first time it's dismissed, save this fact, to avoid
// opening the card again in the future.
async function markAsSeen() {
if (!seenExamples.includes(example.id)) {
const seen = new Set(seenExamples);
seen.add(example.id);
prefObs.set([...seen].sort());
}
}
// Close the example card.
function close() {
collapseAndRemoveCard(cardElem, btnElem.getBoundingClientRect());
// If we fail to save this preference, it's probably not worth alerting the user about,
// so just log to console.
// tslint:disable-next-line:no-console
markAsSeen().catch((err) => console.warn("Failed to save userPref", err));
}
const card = example.welcomeCard;
if (!card) { return null; }
const cardElem = cssCard(
cssImage({src: example.imgUrl}),
cssBody(
cssTitle(card.title),
cssInfo(card.text),
cssButtons(
cssLinkBtn(cssLinkIcon('Page'), card.tutorialName,
{href: example.tutorialUrl, target: '_blank'},
),
// TODO: Add a link to the overview video (as popup or to a support page that shows the
// video). Also include a 'Video' icon.
// cssLinkBtn(cssLinkIcon('Video'), 'Grist Video Tour'),
)
),
cssCloseButton(cssBigIcon('CrossBig'),
dom.on('click', close),
testId('example-card-close'),
),
testId('example-card'),
);
document.body.appendChild(cardElem);
// When reopening, open the card smoothly, for a nicer-looking effect.
if (reopen) {
expandCard(cardElem, btnElem.getBoundingClientRect());
}
}
// When closing the card, collapse it visually into the button that can open it again, to hint to
// the user where to find that button. Remove the card after the animation.
function collapseAndRemoveCard(card: HTMLElement, collapsedRect: DOMRect) {
const watcher = new TransitionWatcher(card);
watcher.onDispose(() => card.remove());
collapseCard(card, collapsedRect);
}
// Implements the collapsing animation by simply setting a scale transform with a suitable origin.
function collapseCard(card: HTMLElement, collapsedRect: DOMRect) {
const rect = card.getBoundingClientRect();
const originX = (collapsedRect.left + collapsedRect.width / 2) - rect.left;
const originY = (collapsedRect.top + collapsedRect.height / 2) - rect.top;
Object.assign(card.style, {
transform: `scale(${collapsedRect.width / rect.width}, ${collapsedRect.height / rect.height})`,
transformOrigin: `${originX}px ${originY}px`,
opacity: '0',
});
}
// To expand the card visually, we reverse the process by collapsing it first with transitions
// disabled, then resetting properties to their defaults with transitions enabled again.
function expandCard(card: HTMLElement, collapsedRect: DOMRect) {
prepareForTransition(card, () => collapseCard(card, collapsedRect));
Object.assign(card.style, {
transform: '',
opacity: '',
visibility: 'visible',
});
}
const cssCard = styled('div', `
position: absolute;
left: 24px;
bottom: 24px;
margin-right: 24px;
max-width: 624px;
padding: 32px 56px 32px 32px;
background-color: white;
box-shadow: 0 2px 18px 0 rgba(31,37,50,0.31), 0 0 1px 0 rgba(76,86,103,0.24);
display: flex;
overflow: hidden;
transition-property: opacity, transform;
transition-duration: 0.5s;
transition-timing-func: ease-in;
`);
const cssImage = styled('img', `
flex: none;
width: 180px;
height: 140px;
margin: 0 -8px;
`);
const cssBody = styled('div', `
margin-left: 24px;
min-width: 0px;
`);
const cssTitle = styled('div', `
font-size: ${vars.headerControlFontSize};
font-weight: ${vars.headerControlTextWeight};
margin-bottom: 16px;
`);
const cssInfo = styled('div', `
margin: 16px 0 24px 0;
line-height: 1.6;
`);
const cssButtons = styled('div', `
display: flex;
`);
const cssLinkBtn = styled(cssLink, `
&:not(:last-child) {
margin-right: 32px;
}
`);
const cssLinkIcon = styled(icon, `
margin-right: 8px;
margin-top: -2px;
`);
const cssCloseButton = styled('div', `
position: absolute;
top: 8px;
right: 8px;
padding: 4px;
border-radius: 4px;
cursor: pointer;
--icon-color: ${colors.slate};
&:hover {
background-color: ${colors.mediumGreyOpaque};
}
`);
const cssBigIcon = styled(icon, `
padding: 12px;
`);