mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) Show Grist video tour after welcome questions
Summary: After the welcome questions are dismissed, a video tour modal will now be displayed. The video tour is also accessible via a tool button in the left panel of the home page, as well as a text button next to the Examples & Templates header. Test Plan: Browser tests. Reviewers: dsagal Reviewed By: dsagal Subscribers: dsagal Differential Revision: https://phab.getgrist.com/D3477
This commit is contained in:
parent
a91d493ffc
commit
abebe812db
@ -102,7 +102,17 @@ exports.groups = [{
|
|||||||
name: 'openWidgetConfiguration',
|
name: 'openWidgetConfiguration',
|
||||||
keys: [],
|
keys: [],
|
||||||
desc: 'Open Custom widget configuration screen',
|
desc: 'Open Custom widget configuration screen',
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
name: 'leftPanelOpen',
|
||||||
|
keys: [],
|
||||||
|
desc: 'Shortcut to open the left panel',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'videoTourToolsOpen',
|
||||||
|
keys: [],
|
||||||
|
desc: 'Shortcut to open video tour from home left panel',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}, {
|
}, {
|
||||||
group: 'Navigation',
|
group: 'Navigation',
|
||||||
|
@ -10,6 +10,7 @@ import {getTimeFromNow, HomeModel, makeLocalViewSettings, ViewSettings} from 'ap
|
|||||||
import {getWorkspaceInfo, workspaceName} from 'app/client/models/WorkspaceInfo';
|
import {getWorkspaceInfo, workspaceName} from 'app/client/models/WorkspaceInfo';
|
||||||
import * as css from 'app/client/ui/DocMenuCss';
|
import * as css from 'app/client/ui/DocMenuCss';
|
||||||
import {buildHomeIntro} from 'app/client/ui/HomeIntro';
|
import {buildHomeIntro} from 'app/client/ui/HomeIntro';
|
||||||
|
import {createVideoTourTextButton} from 'app/client/ui/OpenVideoTour';
|
||||||
import {buildUpgradeNudge} from 'app/client/ui/ProductUpgrades';
|
import {buildUpgradeNudge} from 'app/client/ui/ProductUpgrades';
|
||||||
import {buildPinnedDoc, createPinnedDocs} from 'app/client/ui/PinnedDocs';
|
import {buildPinnedDoc, createPinnedDocs} from 'app/client/ui/PinnedDocs';
|
||||||
import {shadowScroll} from 'app/client/ui/shadowScroll';
|
import {shadowScroll} from 'app/client/ui/shadowScroll';
|
||||||
@ -25,7 +26,7 @@ import {IHomePage} from 'app/common/gristUrls';
|
|||||||
import {SortPref, ViewPref} from 'app/common/Prefs';
|
import {SortPref, ViewPref} from 'app/common/Prefs';
|
||||||
import * as roles from 'app/common/roles';
|
import * as roles from 'app/common/roles';
|
||||||
import {Document, Workspace} from 'app/common/UserAPI';
|
import {Document, Workspace} from 'app/common/UserAPI';
|
||||||
import {Computed, computed, dom, DomContents, makeTestId, Observable, observable, styled} from 'grainjs';
|
import {Computed, computed, dom, DomContents, makeTestId, Observable, observable} from 'grainjs';
|
||||||
import sortBy = require('lodash/sortBy');
|
import sortBy = require('lodash/sortBy');
|
||||||
import {buildTemplateDocs} from 'app/client/ui/TemplateDocs';
|
import {buildTemplateDocs} from 'app/client/ui/TemplateDocs';
|
||||||
import {localStorageBoolObs} from 'app/client/lib/localStorageObs';
|
import {localStorageBoolObs} from 'app/client/lib/localStorageObs';
|
||||||
@ -67,7 +68,7 @@ function createLoadedDocMenu(home: HomeModel) {
|
|||||||
const flashDocId = observable<string|null>(null);
|
const flashDocId = observable<string|null>(null);
|
||||||
return css.docList(
|
return css.docList(
|
||||||
showWelcomeQuestions(home.app.userPrefsObs),
|
showWelcomeQuestions(home.app.userPrefsObs),
|
||||||
cssDocMenu(
|
css.docMenu(
|
||||||
dom.maybe(!home.app.currentFeatures.workspaces, () => [
|
dom.maybe(!home.app.currentFeatures.workspaces, () => [
|
||||||
css.docListHeader('This service is not available right now'),
|
css.docListHeader('This service is not available right now'),
|
||||||
dom('span', '(The organization needs a paid plan)')
|
dom('span', '(The organization needs a paid plan)')
|
||||||
@ -208,8 +209,9 @@ function buildAllDocsTemplates(home: HomeModel, viewSettings: ViewSettings) {
|
|||||||
if (templates.length === 0) { return null; }
|
if (templates.length === 0) { return null; }
|
||||||
|
|
||||||
const hideTemplatesObs = localStorageBoolObs('hide-examples');
|
const hideTemplatesObs = localStorageBoolObs('hide-examples');
|
||||||
return css.templatesDocBlock(
|
return css.allDocsTemplates(css.templatesDocBlock(
|
||||||
dom.autoDispose(hideTemplatesObs),
|
dom.autoDispose(hideTemplatesObs),
|
||||||
|
css.templatesHeaderWrap(
|
||||||
css.templatesHeader(
|
css.templatesHeader(
|
||||||
'Examples & Templates',
|
'Examples & Templates',
|
||||||
dom.domComputed(hideTemplatesObs, (collapsed) =>
|
dom.domComputed(hideTemplatesObs, (collapsed) =>
|
||||||
@ -218,6 +220,8 @@ function buildAllDocsTemplates(home: HomeModel, viewSettings: ViewSettings) {
|
|||||||
dom.on('click', () => hideTemplatesObs.set(!hideTemplatesObs.get())),
|
dom.on('click', () => hideTemplatesObs.set(!hideTemplatesObs.get())),
|
||||||
testId('all-docs-templates-header'),
|
testId('all-docs-templates-header'),
|
||||||
),
|
),
|
||||||
|
createVideoTourTextButton(),
|
||||||
|
),
|
||||||
dom.maybe((use) => !use(hideTemplatesObs), () => [
|
dom.maybe((use) => !use(hideTemplatesObs), () => [
|
||||||
buildTemplateDocs(home, templates, viewSettings),
|
buildTemplateDocs(home, templates, viewSettings),
|
||||||
bigBasicButton(
|
bigBasicButton(
|
||||||
@ -228,7 +232,7 @@ function buildAllDocsTemplates(home: HomeModel, viewSettings: ViewSettings) {
|
|||||||
]),
|
]),
|
||||||
css.docBlock.cls((use) => '-' + use(home.currentView)),
|
css.docBlock.cls((use) => '-' + use(home.currentView)),
|
||||||
testId('all-docs-templates'),
|
testId('all-docs-templates'),
|
||||||
);
|
));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -586,7 +590,3 @@ function shouldShowTemplates(home: HomeModel, showIntro: boolean): boolean {
|
|||||||
// Show templates for all personal orgs, and for non-personal orgs when showing intro.
|
// Show templates for all personal orgs, and for non-personal orgs when showing intro.
|
||||||
return isPersonalOrg || showIntro;
|
return isPersonalOrg || showIntro;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cssDocMenu = styled('div', `
|
|
||||||
flex-grow: 1;
|
|
||||||
`);
|
|
||||||
|
@ -8,6 +8,11 @@ import {bigBasicButton} from 'app/client/ui2018/buttons';
|
|||||||
// styles, which gives it priority.
|
// styles, which gives it priority.
|
||||||
import 'popweasel';
|
import 'popweasel';
|
||||||
|
|
||||||
|
export const docMenu = styled('div', `
|
||||||
|
flex-grow: 1;
|
||||||
|
max-width: 100%;
|
||||||
|
`);
|
||||||
|
|
||||||
// The "&:after" clause forces some padding below all docs.
|
// The "&:after" clause forces some padding below all docs.
|
||||||
export const docList = styled('div', `
|
export const docList = styled('div', `
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -33,16 +38,34 @@ export const docList = styled('div', `
|
|||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
export const docListHeader = styled('div', `
|
const listHeader = styled('div', `
|
||||||
min-height: 32px;
|
min-height: 32px;
|
||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
margin-bottom: 24px;
|
|
||||||
color: ${colors.dark};
|
color: ${colors.dark};
|
||||||
font-size: ${vars.xxxlargeFontSize};
|
font-size: ${vars.xxxlargeFontSize};
|
||||||
font-weight: ${vars.headerControlTextWeight};
|
font-weight: ${vars.headerControlTextWeight};
|
||||||
`);
|
`);
|
||||||
|
|
||||||
export const templatesHeader = styled(docListHeader, `
|
export const docListHeader = styled(listHeader, `
|
||||||
|
margin-bottom: 24px;
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const templatesHeaderWrap = styled('div', `
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
@media ${mediaSmall} {
|
||||||
|
& {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const templatesHeader = styled(listHeader, `
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
@ -53,13 +76,18 @@ export const featuredTemplatesHeader = styled(docListHeader, `
|
|||||||
|
|
||||||
export const otherSitesHeader = templatesHeader;
|
export const otherSitesHeader = templatesHeader;
|
||||||
|
|
||||||
|
export const allDocsTemplates = styled('div', `
|
||||||
|
display: flex;
|
||||||
|
`);
|
||||||
|
|
||||||
export const docBlock = styled('div', `
|
export const docBlock = styled('div', `
|
||||||
max-width: 550px;
|
max-width: 550px;
|
||||||
min-width: 300px;
|
min-width: 300px;
|
||||||
margin-bottom: 28px;
|
margin-bottom: 28px;
|
||||||
|
|
||||||
&-icons {
|
&-icons {
|
||||||
max-width: unset;
|
max-width: max-content;
|
||||||
|
min-width: calc(min(550px, 100%));
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
@ -6,7 +6,8 @@ import {HomeModel} from 'app/client/models/HomeModel';
|
|||||||
import {getWorkspaceInfo, workspaceName} from 'app/client/models/WorkspaceInfo';
|
import {getWorkspaceInfo, workspaceName} from 'app/client/models/WorkspaceInfo';
|
||||||
import {addNewButton, cssAddNewButton} from 'app/client/ui/AddNewButton';
|
import {addNewButton, cssAddNewButton} from 'app/client/ui/AddNewButton';
|
||||||
import {docImport, importFromPlugin} from 'app/client/ui/HomeImports';
|
import {docImport, importFromPlugin} from 'app/client/ui/HomeImports';
|
||||||
import {cssLinkText, cssPageEntry, cssPageIcon, cssPageLink} from 'app/client/ui/LeftPanelCommon';
|
import {cssLinkText, cssPageEntry, cssPageIcon, cssPageLink, cssSpacer} from 'app/client/ui/LeftPanelCommon';
|
||||||
|
import {createVideoTourToolsButton} from 'app/client/ui/OpenVideoTour';
|
||||||
import {transientInput} from 'app/client/ui/transientInput';
|
import {transientInput} from 'app/client/ui/transientInput';
|
||||||
import {colors, testId} from 'app/client/ui2018/cssVars';
|
import {colors, testId} from 'app/client/ui2018/cssVars';
|
||||||
import {icon} from 'app/client/ui2018/icons';
|
import {icon} from 'app/client/ui2018/icons';
|
||||||
@ -111,6 +112,8 @@ export function createHomeLeftPane(leftPanelOpen: Observable<boolean>, home: Hom
|
|||||||
testId('dm-trash'),
|
testId('dm-trash'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
cssSpacer(),
|
||||||
|
createVideoTourToolsButton(),
|
||||||
createHelpTools(home.app),
|
createHelpTools(home.app),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -24,13 +24,11 @@ import {dom, DomContents, Observable, styled} from 'grainjs';
|
|||||||
* Creates the "help tools", a button/link to open HelpScout beacon, and one to open the
|
* Creates the "help tools", a button/link to open HelpScout beacon, and one to open the
|
||||||
* HelpCenter in a new tab.
|
* HelpCenter in a new tab.
|
||||||
*/
|
*/
|
||||||
export function createHelpTools(appModel: AppModel, spacer = true): DomContents {
|
export function createHelpTools(appModel: AppModel): DomContents {
|
||||||
if (shouldHideUiElement("helpCenter")) {
|
if (shouldHideUiElement("helpCenter")) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return [
|
return cssSplitPageEntry(
|
||||||
spacer ? cssSpacer() : null,
|
|
||||||
cssSplitPageEntry(
|
|
||||||
cssPageEntryMain(
|
cssPageEntryMain(
|
||||||
cssPageLink(cssPageIcon('Help'),
|
cssPageLink(cssPageIcon('Help'),
|
||||||
cssLinkText('Help Center'),
|
cssLinkText('Help Center'),
|
||||||
@ -43,9 +41,8 @@ export function createHelpTools(appModel: AppModel, spacer = true): DomContents
|
|||||||
cssPageLink(cssPageIcon('FieldLink'),
|
cssPageLink(cssPageIcon('FieldLink'),
|
||||||
{href: commonUrls.help, target: '_blank'},
|
{href: commonUrls.help, target: '_blank'},
|
||||||
),
|
),
|
||||||
)
|
|
||||||
),
|
),
|
||||||
];
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -56,6 +53,7 @@ export function leftPanelBasic(appModel: AppModel, panelOpen: Observable<boolean
|
|||||||
cssScrollPane(
|
cssScrollPane(
|
||||||
cssTools(
|
cssTools(
|
||||||
cssTools.cls('-collapsed', (use) => !use(panelOpen)),
|
cssTools.cls('-collapsed', (use) => !use(panelOpen)),
|
||||||
|
cssSpacer(),
|
||||||
createHelpTools(appModel),
|
createHelpTools(appModel),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
142
app/client/ui/OpenVideoTour.ts
Normal file
142
app/client/ui/OpenVideoTour.ts
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import * as commands from 'app/client/components/commands';
|
||||||
|
import {cssLinkText, cssPageEntryMain, cssPageIcon, cssPageLink} from 'app/client/ui/LeftPanelCommon';
|
||||||
|
import {colors} from 'app/client/ui2018/cssVars';
|
||||||
|
import {icon} from 'app/client/ui2018/icons';
|
||||||
|
import {modal} from 'app/client/ui2018/modals';
|
||||||
|
import {commonUrls, shouldHideUiElement} from 'app/common/gristUrls';
|
||||||
|
import {dom, makeTestId, styled} from 'grainjs';
|
||||||
|
|
||||||
|
const testId = makeTestId('test-video-tour-');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a modal containing a video tour of Grist.
|
||||||
|
*/
|
||||||
|
export function openVideoTour(refElement: HTMLElement) {
|
||||||
|
return modal(
|
||||||
|
(ctl) => {
|
||||||
|
return [
|
||||||
|
cssModal.cls(''),
|
||||||
|
cssCloseButton(
|
||||||
|
cssCloseIcon('CrossBig'),
|
||||||
|
dom.on('click', () => ctl.close()),
|
||||||
|
testId('close'),
|
||||||
|
),
|
||||||
|
cssVideoWrap(
|
||||||
|
cssVideo(
|
||||||
|
{
|
||||||
|
src: commonUrls.videoTour,
|
||||||
|
title: 'YouTube video player',
|
||||||
|
frameborder: '0',
|
||||||
|
allow: 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture',
|
||||||
|
allowfullscreen: '',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
testId('modal'),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
{
|
||||||
|
refElement,
|
||||||
|
variant: 'collapsing',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a text button that shows the video tour on click.
|
||||||
|
*/
|
||||||
|
export function createVideoTourTextButton(): HTMLDivElement {
|
||||||
|
const elem: HTMLDivElement = cssVideoTourTextButton(
|
||||||
|
cssVideoIcon('Video'),
|
||||||
|
'Grist Video Tour',
|
||||||
|
dom.on('click', () => openVideoTour(elem)),
|
||||||
|
testId('text-button'),
|
||||||
|
);
|
||||||
|
|
||||||
|
return elem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the "Video Tour" button for the "Tools" section of the left panel.
|
||||||
|
*
|
||||||
|
* Shows the video tour on click.
|
||||||
|
*/
|
||||||
|
export function createVideoTourToolsButton(): HTMLDivElement | null {
|
||||||
|
if (shouldHideUiElement('helpCenter')) { return null; }
|
||||||
|
|
||||||
|
let iconElement: HTMLElement;
|
||||||
|
|
||||||
|
const commandsGroup = commands.createGroup({
|
||||||
|
videoTourToolsOpen: () => openVideoTour(iconElement),
|
||||||
|
}, null, true);
|
||||||
|
|
||||||
|
return cssPageEntryMain(
|
||||||
|
dom.autoDispose(commandsGroup),
|
||||||
|
cssPageLink(
|
||||||
|
iconElement = cssPageIcon('Video'),
|
||||||
|
cssLinkText('Video Tour'),
|
||||||
|
dom.cls('tour-help-center'),
|
||||||
|
dom.on('click', () => openVideoTour(iconElement)),
|
||||||
|
testId('tools-button'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cssModal = styled('div', `
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 864px;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssVideoWrap = styled('div', `
|
||||||
|
position: relative;
|
||||||
|
padding-bottom: 56.25%;
|
||||||
|
height: 0;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssVideo = styled('iframe', `
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssVideoTourTextButton = styled('div', `
|
||||||
|
color: ${colors.lightGreen};
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: ${colors.darkGreen};
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssVideoIcon = styled(icon, `
|
||||||
|
background-color: ${colors.lightGreen};
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 0px 4px 3px 0;
|
||||||
|
|
||||||
|
.${cssVideoTourTextButton.className}:hover > & {
|
||||||
|
background-color: ${colors.darkGreen};
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssCloseButton = styled('div', `
|
||||||
|
align-self: flex-end;
|
||||||
|
margin: -8px;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
--icon-color: ${colors.slate};
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: ${colors.mediumGreyOpaque};
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssCloseIcon = styled(icon, `
|
||||||
|
padding: 12px;
|
||||||
|
`);
|
@ -1,9 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* Note that it assumes the presence of cssVars.cssRootVars on <body>.
|
* Note that it assumes the presence of cssVars.cssRootVars on <body>.
|
||||||
*/
|
*/
|
||||||
|
import * as commands from 'app/client/components/commands';
|
||||||
import {urlState} from "app/client/models/gristUrlState";
|
import {urlState} from "app/client/models/gristUrlState";
|
||||||
import {resizeFlexVHandle} from 'app/client/ui/resizeHandle';
|
import {resizeFlexVHandle} from 'app/client/ui/resizeHandle';
|
||||||
import {transition} from 'app/client/ui/transitions';
|
import {transition, TransitionWatcher} from 'app/client/ui/transitions';
|
||||||
import {colors, cssHideForNarrowScreen, mediaNotSmall, mediaSmall} from 'app/client/ui2018/cssVars';
|
import {colors, cssHideForNarrowScreen, mediaNotSmall, mediaSmall} from 'app/client/ui2018/cssVars';
|
||||||
import {isNarrowScreenObs} from 'app/client/ui2018/cssVars';
|
import {isNarrowScreenObs} from 'app/client/ui2018/cssVars';
|
||||||
import {icon} from 'app/client/ui2018/icons';
|
import {icon} from 'app/client/ui2018/icons';
|
||||||
@ -40,6 +41,7 @@ export function pagePanels(page: PageContents) {
|
|||||||
|
|
||||||
let lastLeftOpen = left.panelOpen.get();
|
let lastLeftOpen = left.panelOpen.get();
|
||||||
let lastRightOpen = right?.panelOpen.get() || false;
|
let lastRightOpen = right?.panelOpen.get() || false;
|
||||||
|
let leftPaneDom: HTMLElement;
|
||||||
|
|
||||||
// When switching to mobile mode, close panels; when switching to desktop, restore the
|
// When switching to mobile mode, close panels; when switching to desktop, restore the
|
||||||
// last desktop state.
|
// last desktop state.
|
||||||
@ -60,12 +62,21 @@ export function pagePanels(page: PageContents) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const commandsGroup = commands.createGroup({
|
||||||
|
leftPanelOpen: () => new Promise((resolve) => {
|
||||||
|
const watcher = new TransitionWatcher(leftPaneDom);
|
||||||
|
watcher.onDispose(() => resolve(undefined));
|
||||||
|
left.panelOpen.set(true);
|
||||||
|
}),
|
||||||
|
}, null, true);
|
||||||
|
|
||||||
return cssPageContainer(
|
return cssPageContainer(
|
||||||
dom.autoDispose(sub1),
|
dom.autoDispose(sub1),
|
||||||
dom.autoDispose(sub2),
|
dom.autoDispose(sub2),
|
||||||
|
dom.autoDispose(commandsGroup),
|
||||||
page.contentTop,
|
page.contentTop,
|
||||||
cssContentMain(
|
cssContentMain(
|
||||||
cssLeftPane(
|
leftPaneDom = cssLeftPane(
|
||||||
testId('left-panel'),
|
testId('left-panel'),
|
||||||
cssTopHeader(left.header),
|
cssTopHeader(left.header),
|
||||||
left.content,
|
left.content,
|
||||||
@ -187,6 +198,7 @@ function toggleObs(boolObs: Observable<boolean>) {
|
|||||||
boolObs.set(!boolObs.get());
|
boolObs.set(!boolObs.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bottomFooterHeightPx = 48;
|
||||||
const cssVBox = styled('div', `
|
const cssVBox = styled('div', `
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -207,7 +219,7 @@ const cssPageContainer = styled(cssVBox, `
|
|||||||
|
|
||||||
@media ${mediaSmall} {
|
@media ${mediaSmall} {
|
||||||
& {
|
& {
|
||||||
padding-bottom: 48px;
|
padding-bottom: ${bottomFooterHeightPx}px;
|
||||||
min-width: 240px;
|
min-width: 240px;
|
||||||
}
|
}
|
||||||
.interface-light & {
|
.interface-light & {
|
||||||
@ -232,7 +244,7 @@ export const cssLeftPane = styled(cssVBox, `
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: ${bottomFooterHeightPx}px;
|
||||||
left: -${240 + 15}px; /* adds an extra 15 pixels to also hide the box shadow */
|
left: -${240 + 15}px; /* adds an extra 15 pixels to also hide the box shadow */
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
box-shadow: 10px 0 5px rgba(0, 0, 0, 0.2);
|
box-shadow: 10px 0 5px rgba(0, 0, 0, 0.2);
|
||||||
@ -279,7 +291,7 @@ const cssRightPane = styled(cssVBox, `
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: ${bottomFooterHeightPx}px;
|
||||||
right: -${240 + 15}px; /* adds an extra 15 pixels to also hide the box shadow */
|
right: -${240 + 15}px; /* adds an extra 15 pixels to also hide the box shadow */
|
||||||
box-shadow: -10px 0 5px rgba(0, 0, 0, 0.2);
|
box-shadow: -10px 0 5px rgba(0, 0, 0, 0.2);
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
@ -324,7 +336,7 @@ const cssTopHeader = styled('div', `
|
|||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
const cssBottomFooter = styled ('div', `
|
const cssBottomFooter = styled ('div', `
|
||||||
height: 48px;
|
height: ${bottomFooterHeightPx}px;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -121,6 +121,11 @@ const pinnedDocWrapper = styled('div', `
|
|||||||
&:hover {
|
&:hover {
|
||||||
border: 1px solid ${colors.slate};
|
border: 1px solid ${colors.slate};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* TODO: Specify a gap on flexbox parents of pinnedDocWrapper instead. */
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0px;
|
||||||
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const pinnedDoc = styled('a', `
|
const pinnedDoc = styled('a', `
|
||||||
|
@ -114,7 +114,7 @@ export function tools(owner: Disposable, gristDoc: GristDoc, leftPanelOpen: Obse
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
createHelpTools(docPageModel.appModel, false)
|
createHelpTools(docPageModel.appModel),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import * as commands from 'app/client/components/commands';
|
||||||
import {getUserPrefObs} from 'app/client/models/UserPrefs';
|
import {getUserPrefObs} from 'app/client/models/UserPrefs';
|
||||||
import {colors, testId} from 'app/client/ui2018/cssVars';
|
import {colors, testId} from 'app/client/ui2018/cssVars';
|
||||||
import {icon} from 'app/client/ui2018/icons';
|
import {icon} from 'app/client/ui2018/icons';
|
||||||
@ -29,9 +30,16 @@ export function showWelcomeQuestions(userPrefsObs: Observable<UserPrefs>) {
|
|||||||
{method: 'POST', body: JSON.stringify({use_cases, use_other})});
|
{method: 'POST', body: JSON.stringify({use_cases, use_other})});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
owner.onDispose(async () => {
|
||||||
// Whichever way the modal is closed, don't show the questions again. (We set the value to
|
// Whichever way the modal is closed, don't show the questions again. (We set the value to
|
||||||
// undefined to remove it from the JSON prefs object entirely; it's never used again.)
|
// undefined to remove it from the JSON prefs object entirely; it's never used again.)
|
||||||
owner.onDispose(() => showQuestions.set(undefined));
|
showQuestions.set(undefined);
|
||||||
|
|
||||||
|
// Show the Grist video tour when the modal is closed.
|
||||||
|
await commands.allCommands.leftPanelOpen.run();
|
||||||
|
commands.allCommands.videoTourToolsOpen.run();
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: [cssLogo(), dom('div', 'Welcome to Grist!')],
|
title: [cssLogo(), dom('div', 'Welcome to Grist!')],
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import {FocusLayer} from 'app/client/lib/FocusLayer';
|
import {FocusLayer} from 'app/client/lib/FocusLayer';
|
||||||
import {reportError} from 'app/client/models/errors';
|
import {reportError} from 'app/client/models/errors';
|
||||||
import {cssInput} from 'app/client/ui/MakeCopyMenu';
|
import {cssInput} from 'app/client/ui/MakeCopyMenu';
|
||||||
|
import {prepareForTransition, TransitionWatcher} from 'app/client/ui/transitions';
|
||||||
import {bigBasicButton, bigPrimaryButton, cssButton} from 'app/client/ui2018/buttons';
|
import {bigBasicButton, bigPrimaryButton, cssButton} from 'app/client/ui2018/buttons';
|
||||||
import {colors, mediaSmall, testId, vars} from 'app/client/ui2018/cssVars';
|
import {colors, mediaSmall, testId, vars} from 'app/client/ui2018/cssVars';
|
||||||
import {loadingSpinner} from 'app/client/ui2018/loaders';
|
import {loadingSpinner} from 'app/client/ui2018/loaders';
|
||||||
@ -112,10 +113,25 @@ export class ModalControl extends Disposable implements IModalControl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IModalOptions {
|
/**
|
||||||
noEscapeKey?: boolean; // If set, escape key does not close the dialog
|
* The modal variant.
|
||||||
noClickAway?: boolean; // If set, clicking into background does not close dialog.
|
*
|
||||||
|
* Fade-in modals open with a fade-in background animation, and close immediately.
|
||||||
|
*
|
||||||
|
* Collapsing modals open with a expanding animation from a referenced DOM element, and
|
||||||
|
* close with a collapsing animation into the referenced element.
|
||||||
|
*/
|
||||||
|
export type IModalVariant = 'fade-in' | 'collapsing';
|
||||||
|
|
||||||
|
export interface IModalOptions {
|
||||||
|
// The modal variant. Defaults to "fade-in".
|
||||||
|
variant?: IModalVariant;
|
||||||
|
// Required for "collapsing" variant modals. This is the anchor element for animations.
|
||||||
|
refElement?: HTMLElement;
|
||||||
|
// If set, escape key does not close the dialog.
|
||||||
|
noEscapeKey?: boolean;
|
||||||
|
// If set, clicking into background does not close dialog.
|
||||||
|
noClickAway?: boolean;
|
||||||
// If given, call and wait for this before closing the dialog. If it returns false, don't close.
|
// If given, call and wait for this before closing the dialog. If it returns false, don't close.
|
||||||
// Error also prevents closing, and is reported as an unexpected error.
|
// Error also prevents closing, and is reported as an unexpected error.
|
||||||
beforeClose?: () => Promise<boolean>;
|
beforeClose?: () => Promise<boolean>;
|
||||||
@ -153,41 +169,78 @@ export function modal(
|
|||||||
createFn: (ctl: IModalControl, owner: MultiHolder) => DomElementArg,
|
createFn: (ctl: IModalControl, owner: MultiHolder) => DomElementArg,
|
||||||
options: IModalOptions = {}
|
options: IModalOptions = {}
|
||||||
): void {
|
): void {
|
||||||
|
const {noEscapeKey, noClickAway, refElement = document.body, variant = 'fade-in'} = options;
|
||||||
|
|
||||||
function doClose() {
|
function doClose() {
|
||||||
if (!modalDom.isConnected) { return; }
|
if (!modalDom.isConnected) { return; }
|
||||||
|
|
||||||
|
variant === 'collapsing' ? collapseAndCloseModal() : closeModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
document.body.removeChild(modalDom);
|
document.body.removeChild(modalDom);
|
||||||
// Ensure we run the disposers for the DOM contained in the modal.
|
// Ensure we run the disposers for the DOM contained in the modal.
|
||||||
dom.domDispose(modalDom);
|
dom.domDispose(modalDom);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function collapseAndCloseModal() {
|
||||||
|
const watcher = new TransitionWatcher(dialogDom);
|
||||||
|
watcher.onDispose(() => closeModal());
|
||||||
|
modalDom.classList.add(cssModalBacker.className + '-collapsing');
|
||||||
|
collapseModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
function expandModal() {
|
||||||
|
prepareForTransition(dialogDom, () => collapseModal());
|
||||||
|
Object.assign(dialogDom.style, {
|
||||||
|
transform: '',
|
||||||
|
opacity: '',
|
||||||
|
visibility: 'visible',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function collapseModal() {
|
||||||
|
const rect = dialogDom.getBoundingClientRect();
|
||||||
|
const collapsedRect = refElement.getBoundingClientRect();
|
||||||
|
const originX = (collapsedRect.left + collapsedRect.width / 2) - rect.left;
|
||||||
|
const originY = (collapsedRect.top + collapsedRect.height / 2) - rect.top;
|
||||||
|
Object.assign(dialogDom.style, {
|
||||||
|
transform: `scale(${collapsedRect.width / rect.width}, ${collapsedRect.height / rect.height})`,
|
||||||
|
transformOrigin: `${originX}px ${originY}px`,
|
||||||
|
opacity: '0',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let close = doClose;
|
let close = doClose;
|
||||||
|
let dialogDom: HTMLElement;
|
||||||
|
|
||||||
const modalDom = cssModalBacker(
|
const modalDom = cssModalBacker(
|
||||||
dom.create((owner) => {
|
dom.create((owner) => {
|
||||||
const focus = () => dialog.focus();
|
const focus = () => dialogDom.focus();
|
||||||
const ctl = ModalControl.create(owner, doClose, focus);
|
const ctl = ModalControl.create(owner, doClose, focus);
|
||||||
close = () => ctl.close();
|
close = () => ctl.close();
|
||||||
|
|
||||||
const dialog = cssModalDialog(
|
dialogDom = cssModalDialog(
|
||||||
createFn(ctl, owner),
|
createFn(ctl, owner),
|
||||||
|
cssModalDialog.cls('-collapsing', variant === 'collapsing'),
|
||||||
dom.on('click', (ev) => ev.stopPropagation()),
|
dom.on('click', (ev) => ev.stopPropagation()),
|
||||||
options.noEscapeKey ? null : dom.onKeyDown({ Escape: close }),
|
noEscapeKey ? null : dom.onKeyDown({ Escape: close }),
|
||||||
testId('modal-dialog')
|
testId('modal-dialog'),
|
||||||
);
|
);
|
||||||
FocusLayer.create(owner, {
|
FocusLayer.create(owner, {
|
||||||
defaultFocusElem: dialog,
|
defaultFocusElem: dialogDom,
|
||||||
allowFocus: (elem) => (elem !== document.body),
|
allowFocus: (elem) => (elem !== document.body),
|
||||||
// Pause mousetrap keyboard shortcuts while the modal is shown. Without this, arrow keys
|
// Pause mousetrap keyboard shortcuts while the modal is shown. Without this, arrow keys
|
||||||
// will navigate in a grid underneath the modal, and Enter may open a cell there.
|
// will navigate in a grid underneath the modal, and Enter may open a cell there.
|
||||||
pauseMousetrap: true
|
pauseMousetrap: true
|
||||||
});
|
});
|
||||||
return dialog;
|
return dialogDom;
|
||||||
}),
|
}),
|
||||||
options.noClickAway ? null : dom.on('click', () => close()),
|
noClickAway ? null : dom.on('click', () => close()),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
document.body.appendChild(modalDom);
|
document.body.appendChild(modalDom);
|
||||||
|
if (variant === 'collapsing') { expandModal(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ISaveModalOptions {
|
export interface ISaveModalOptions {
|
||||||
@ -436,6 +489,11 @@ const cssModalDialog = styled('div', `
|
|||||||
&-fixed-wide {
|
&-fixed-wide {
|
||||||
width: 600px;
|
width: 600px;
|
||||||
}
|
}
|
||||||
|
&-collapsing {
|
||||||
|
transition-property: opacity, transform;
|
||||||
|
transition-duration: 0.4s;
|
||||||
|
transition-timing-function: ease-in-out;
|
||||||
|
}
|
||||||
@media ${mediaSmall} {
|
@media ${mediaSmall} {
|
||||||
& {
|
& {
|
||||||
width: unset;
|
width: unset;
|
||||||
@ -471,6 +529,10 @@ const cssFadeIn = keyframes(`
|
|||||||
from {background-color: transparent}
|
from {background-color: transparent}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
const cssFadeOut = keyframes(`
|
||||||
|
from {background-color: ${colors.backdrop}}
|
||||||
|
`);
|
||||||
|
|
||||||
const cssModalBacker = styled('div', `
|
const cssModalBacker = styled('div', `
|
||||||
position: fixed;
|
position: fixed;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -485,6 +547,11 @@ const cssModalBacker = styled('div', `
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
animation-name: ${cssFadeIn};
|
animation-name: ${cssFadeIn};
|
||||||
animation-duration: 0.4s;
|
animation-duration: 0.4s;
|
||||||
|
|
||||||
|
&-collapsing {
|
||||||
|
animation-name: ${cssFadeOut};
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const cssSpinner = styled('div', `
|
const cssSpinner = styled('div', `
|
||||||
|
@ -64,6 +64,7 @@ export const commonUrls = {
|
|||||||
|
|
||||||
efcrConnect: 'https://efc-r.com/connect',
|
efcrConnect: 'https://efc-r.com/connect',
|
||||||
efcrHelp: 'https://www.nioxus.info/eFCR-Help',
|
efcrHelp: 'https://www.nioxus.info/eFCR-Help',
|
||||||
|
videoTour: 'https://www.youtube.com/embed/qnr2Pfnxdlc?autoplay=1',
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user