mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
1654a2681f
Summary: This moves all client code to core, and makes minimal fix-ups to get grist and grist-core to compile correctly. The client works in core, but I'm leaving clean-up around the build and bundles to follow-up. Test Plan: existing tests pass; server-dev bundle looks sane Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D2627
174 lines
5.2 KiB
TypeScript
174 lines
5.2 KiB
TypeScript
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;
|
|
`);
|