mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) Add April Fools easter egg
Summary: What happens when you type "rr" instead of "r" in an anchor link's row number? Test Plan: Browser tests. Reviewers: jarek Reviewed By: jarek Differential Revision: https://phab.getgrist.com/D3829
This commit is contained in:
parent
f59c1edf16
commit
01fbe871aa
@ -9,8 +9,16 @@ import {Computed, Disposable, dom, Observable} from 'grainjs';
|
|||||||
import {IPopupOptions} from 'popweasel';
|
import {IPopupOptions} from 'popweasel';
|
||||||
|
|
||||||
export interface AttachOptions {
|
export interface AttachOptions {
|
||||||
|
/** Defaults to false. */
|
||||||
|
forceShow?: boolean;
|
||||||
/** Defaults to false. */
|
/** Defaults to false. */
|
||||||
hideArrow?: boolean;
|
hideArrow?: boolean;
|
||||||
|
/** Defaults to false. */
|
||||||
|
hideDontShowTips?: boolean;
|
||||||
|
/** Defaults to true. */
|
||||||
|
markAsSeen?: boolean;
|
||||||
|
/** Defaults to false. */
|
||||||
|
showOnMobile?: boolean;
|
||||||
popupOptions?: IPopupOptions;
|
popupOptions?: IPopupOptions;
|
||||||
onDispose?(): void;
|
onDispose?(): void;
|
||||||
}
|
}
|
||||||
@ -60,11 +68,11 @@ export class BehavioralPromptsManager extends Disposable {
|
|||||||
// Don't show tips if surveying is disabled.
|
// Don't show tips if surveying is disabled.
|
||||||
// TODO: Move this into a dedicated variable - this is only a short-term fix for hiding
|
// TODO: Move this into a dedicated variable - this is only a short-term fix for hiding
|
||||||
// tips in grist-core.
|
// tips in grist-core.
|
||||||
!getGristConfig().survey ||
|
(!getGristConfig().survey && prompt !== 'rickRow') ||
|
||||||
// Or on mobile - the design currently isn't mobile-friendly.
|
// Or if this tip shouldn't be shown on mobile.
|
||||||
isNarrowScreen() ||
|
(isNarrowScreen() && !options.showOnMobile) ||
|
||||||
// Or if "Don't show tips" was checked in the past.
|
// Or if "Don't show tips" was checked in the past.
|
||||||
this._prefs.get().dontShowTips ||
|
(this._prefs.get().dontShowTips && !options.forceShow) ||
|
||||||
// Or if this tip has been shown and dismissed in the past.
|
// Or if this tip has been shown and dismissed in the past.
|
||||||
this.hasSeenTip(prompt)
|
this.hasSeenTip(prompt)
|
||||||
) {
|
) {
|
||||||
@ -88,15 +96,16 @@ export class BehavioralPromptsManager extends Disposable {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const {hideArrow = false, onDispose, popupOptions} = options;
|
const {hideArrow, hideDontShowTips, markAsSeen = true, onDispose, popupOptions} = options;
|
||||||
const {title, content} = GristBehavioralPrompts[prompt];
|
const {title, content} = GristBehavioralPrompts[prompt];
|
||||||
const ctl = showBehavioralPrompt(refElement, title(), content(), {
|
const ctl = showBehavioralPrompt(refElement, title(), content(), {
|
||||||
onClose: (dontShowTips) => {
|
onClose: (dontShowTips) => {
|
||||||
if (dontShowTips) { this._dontShowTips(); }
|
if (dontShowTips) { this._dontShowTips(); }
|
||||||
this._markAsSeen(prompt);
|
if (markAsSeen) { this._markAsSeen(prompt); }
|
||||||
},
|
},
|
||||||
hideArrow,
|
hideArrow,
|
||||||
popupOptions,
|
popupOptions,
|
||||||
|
hideDontShowTips,
|
||||||
});
|
});
|
||||||
|
|
||||||
ctl.onDispose(() => {
|
ctl.onDispose(() => {
|
||||||
|
@ -36,7 +36,7 @@ import {DocData} from 'app/client/models/DocData';
|
|||||||
import {DocInfoRec, DocModel, ViewFieldRec, ViewRec, ViewSectionRec} from 'app/client/models/DocModel';
|
import {DocInfoRec, DocModel, ViewFieldRec, ViewRec, ViewSectionRec} from 'app/client/models/DocModel';
|
||||||
import {DocPageModel} from 'app/client/models/DocPageModel';
|
import {DocPageModel} from 'app/client/models/DocPageModel';
|
||||||
import {UserError} from 'app/client/models/errors';
|
import {UserError} from 'app/client/models/errors';
|
||||||
import {urlState} from 'app/client/models/gristUrlState';
|
import {getMainOrgUrl, urlState} from 'app/client/models/gristUrlState';
|
||||||
import {getFilterFunc, QuerySetManager} from 'app/client/models/QuerySet';
|
import {getFilterFunc, QuerySetManager} from 'app/client/models/QuerySet';
|
||||||
import {getUserOrgPrefObs, getUserOrgPrefsObs, markAsSeen} from 'app/client/models/UserPrefs';
|
import {getUserOrgPrefObs, getUserOrgPrefsObs, markAsSeen} from 'app/client/models/UserPrefs';
|
||||||
import {App} from 'app/client/ui/App';
|
import {App} from 'app/client/ui/App';
|
||||||
@ -48,8 +48,10 @@ import {IPageWidget, toPageWidget} from 'app/client/ui/PageWidgetPicker';
|
|||||||
import {linkFromId, selectBy} from 'app/client/ui/selectBy';
|
import {linkFromId, selectBy} from 'app/client/ui/selectBy';
|
||||||
import {startWelcomeTour} from 'app/client/ui/welcomeTour';
|
import {startWelcomeTour} from 'app/client/ui/welcomeTour';
|
||||||
import {IWidgetType} from 'app/client/ui/widgetTypes';
|
import {IWidgetType} from 'app/client/ui/widgetTypes';
|
||||||
import {isNarrowScreen, mediaSmall, testId} from 'app/client/ui2018/cssVars';
|
import {PlayerState, YouTubePlayer} from 'app/client/ui/YouTubePlayer';
|
||||||
|
import {isNarrowScreen, mediaSmall, mediaXSmall, testId, theme} from 'app/client/ui2018/cssVars';
|
||||||
import {IconName} from 'app/client/ui2018/IconList';
|
import {IconName} from 'app/client/ui2018/IconList';
|
||||||
|
import {icon} from 'app/client/ui2018/icons';
|
||||||
import {invokePrompt} from 'app/client/ui2018/modals';
|
import {invokePrompt} from 'app/client/ui2018/modals';
|
||||||
import {FieldEditor} from "app/client/widgets/FieldEditor";
|
import {FieldEditor} from "app/client/widgets/FieldEditor";
|
||||||
import {DiscussionPanel} from 'app/client/widgets/DiscussionEditor';
|
import {DiscussionPanel} from 'app/client/widgets/DiscussionEditor';
|
||||||
@ -78,6 +80,7 @@ import {
|
|||||||
Holder,
|
Holder,
|
||||||
IDisposable,
|
IDisposable,
|
||||||
IDomComponent,
|
IDomComponent,
|
||||||
|
keyframes,
|
||||||
Observable,
|
Observable,
|
||||||
styled,
|
styled,
|
||||||
subscribe,
|
subscribe,
|
||||||
@ -87,6 +90,8 @@ import * as ko from 'knockout';
|
|||||||
import cloneDeepWith = require('lodash/cloneDeepWith');
|
import cloneDeepWith = require('lodash/cloneDeepWith');
|
||||||
import isEqual = require('lodash/isEqual');
|
import isEqual = require('lodash/isEqual');
|
||||||
|
|
||||||
|
const RICK_ROLL_YOUTUBE_EMBED_ID = 'dQw4w9WgXcQ';
|
||||||
|
|
||||||
const t = makeT('GristDoc');
|
const t = makeT('GristDoc');
|
||||||
|
|
||||||
const G = getBrowserGlobals('document', 'window');
|
const G = getBrowserGlobals('document', 'window');
|
||||||
@ -190,6 +195,9 @@ export class GristDoc extends DisposableWithEvents {
|
|||||||
private _rawSectionOptions: Observable<RawSectionOptions|null> = Observable.create(this, null);
|
private _rawSectionOptions: Observable<RawSectionOptions|null> = Observable.create(this, null);
|
||||||
private _activeContent: Computed<IDocPage|RawSectionOptions>;
|
private _activeContent: Computed<IDocPage|RawSectionOptions>;
|
||||||
private _docTutorialHolder = Holder.create<DocTutorial>(this);
|
private _docTutorialHolder = Holder.create<DocTutorial>(this);
|
||||||
|
private _isRickRowing: Observable<boolean> = Observable.create(this, false);
|
||||||
|
private _showBackgroundVideoPlayer: Observable<boolean> = Observable.create(this, false);
|
||||||
|
private _backgroundVideoPlayerHolder: Holder<YouTubePlayer> = Holder.create(this);
|
||||||
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -280,6 +288,40 @@ export class GristDoc extends DisposableWithEvents {
|
|||||||
const cursorPos = this._getCursorPosFromHash(state.hash);
|
const cursorPos = this._getCursorPosFromHash(state.hash);
|
||||||
await this.recursiveMoveToCursorPos(cursorPos, true);
|
await this.recursiveMoveToCursorPos(cursorPos, true);
|
||||||
}
|
}
|
||||||
|
if (state.hash.rickRow && !this._isRickRowing.get()) {
|
||||||
|
YouTubePlayer.create(this._backgroundVideoPlayerHolder, RICK_ROLL_YOUTUBE_EMBED_ID, {
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
origin: getMainOrgUrl(),
|
||||||
|
playerVars: {
|
||||||
|
controls: 0,
|
||||||
|
disablekb: 1,
|
||||||
|
fs: 0,
|
||||||
|
iv_load_policy: 3,
|
||||||
|
modestbranding: 1,
|
||||||
|
},
|
||||||
|
onPlayerStateChange: (_player, event) => {
|
||||||
|
if (event.data === PlayerState.Playing) {
|
||||||
|
this._isRickRowing.set(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}, cssYouTubePlayer.cls(''));
|
||||||
|
this._showBackgroundVideoPlayer.set(true);
|
||||||
|
this._waitForView()
|
||||||
|
.then(() => {
|
||||||
|
const cursor = document.querySelector('.selected_cursor.active_cursor');
|
||||||
|
if (cursor) {
|
||||||
|
this.behavioralPromptsManager.showTip(cursor, 'rickRow', {
|
||||||
|
forceShow: true,
|
||||||
|
hideDontShowTips: true,
|
||||||
|
markAsSeen: false,
|
||||||
|
showOnMobile: true,
|
||||||
|
onDispose: () => this.playRickRollVideo(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(reportError);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
reportError(e);
|
reportError(e);
|
||||||
} finally {
|
} finally {
|
||||||
@ -482,6 +524,14 @@ export class GristDoc extends DisposableWithEvents {
|
|||||||
return cssViewContentPane(
|
return cssViewContentPane(
|
||||||
testId('gristdoc'),
|
testId('gristdoc'),
|
||||||
cssViewContentPane.cls("-contents", isPopup),
|
cssViewContentPane.cls("-contents", isPopup),
|
||||||
|
dom.maybe(this._isRickRowing, () => cssStopRickRowingButton(
|
||||||
|
cssCloseIcon('CrossBig'),
|
||||||
|
dom.on('click', () => {
|
||||||
|
this._isRickRowing.set(false);
|
||||||
|
this._showBackgroundVideoPlayer.set(false);
|
||||||
|
}),
|
||||||
|
testId('gristdoc-stop-rick-rowing'),
|
||||||
|
)),
|
||||||
dom.domComputed(this._activeContent, (content) => {
|
dom.domComputed(this._activeContent, (content) => {
|
||||||
return (
|
return (
|
||||||
content === 'code' ? dom.create(CodeEditorPanel, this) :
|
content === 'code' ? dom.create(CodeEditorPanel, this) :
|
||||||
@ -504,6 +554,13 @@ export class GristDoc extends DisposableWithEvents {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
dom.maybe(this._showBackgroundVideoPlayer, () => [
|
||||||
|
cssBackgroundVideo(
|
||||||
|
this._backgroundVideoPlayerHolder.get()?.buildDom(),
|
||||||
|
cssBackgroundVideo.cls('-fade-in-and-out', this._isRickRowing),
|
||||||
|
testId('gristdoc-background-video'),
|
||||||
|
),
|
||||||
|
]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1123,6 +1180,41 @@ export class GristDoc extends DisposableWithEvents {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts playing the music video for Never Gonna Give You Up in the background.
|
||||||
|
*/
|
||||||
|
public async playRickRollVideo() {
|
||||||
|
const backgroundVideoPlayer = this._backgroundVideoPlayerHolder.get();
|
||||||
|
if (!backgroundVideoPlayer) { return; }
|
||||||
|
|
||||||
|
await backgroundVideoPlayer.isLoaded();
|
||||||
|
backgroundVideoPlayer.play();
|
||||||
|
|
||||||
|
const setVolume = async (start: number, end: number, step: number) => {
|
||||||
|
let volume: number;
|
||||||
|
const condition = start <= end
|
||||||
|
? () => volume <= end
|
||||||
|
: () => volume >= end;
|
||||||
|
const afterthought = start <= end
|
||||||
|
? () => volume += step
|
||||||
|
: () => volume -= step;
|
||||||
|
for (volume = start; condition(); afterthought()) {
|
||||||
|
backgroundVideoPlayer.setVolume(volume);
|
||||||
|
await delay(250);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await setVolume(0, 100, 5);
|
||||||
|
|
||||||
|
await delay(190 * 1000);
|
||||||
|
if (!this._isRickRowing.get()) { return; }
|
||||||
|
|
||||||
|
await setVolume(100, 0, 5);
|
||||||
|
|
||||||
|
this._isRickRowing.set(false);
|
||||||
|
this._showBackgroundVideoPlayer.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Waits for a view to be ready
|
* Waits for a view to be ready
|
||||||
*/
|
*/
|
||||||
@ -1448,3 +1540,63 @@ const cssViewContentPane = styled('div', `
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
const fadeInAndOut = keyframes(`
|
||||||
|
0% {
|
||||||
|
opacity: 0.01;
|
||||||
|
}
|
||||||
|
5%, 95% {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0.01;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssBackgroundVideo = styled('div', `
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
&-fade-in-and-out {
|
||||||
|
animation: ${fadeInAndOut} 200s;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssYouTubePlayer = styled('div', `
|
||||||
|
position: absolute;
|
||||||
|
width: 450%;
|
||||||
|
height: 450%;
|
||||||
|
top: -175%;
|
||||||
|
left: -175%;
|
||||||
|
|
||||||
|
@media ${mediaXSmall} {
|
||||||
|
& {
|
||||||
|
width: 450%;
|
||||||
|
height: 450%;
|
||||||
|
top: -175%;
|
||||||
|
left: -175%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssStopRickRowingButton = styled('div', `
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 8px;
|
||||||
|
margin: 16px;
|
||||||
|
border-radius: 24px;
|
||||||
|
background-color: ${theme.toastBg};
|
||||||
|
cursor: pointer;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssCloseIcon = styled(icon, `
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
|
--icon-color: ${theme.toastControlFg};
|
||||||
|
`);
|
||||||
|
@ -4,7 +4,7 @@ import {FocusLayer} from 'app/client/lib/FocusLayer';
|
|||||||
import {reportSuccess} from 'app/client/models/errors';
|
import {reportSuccess} from 'app/client/models/errors';
|
||||||
import {basicButton, bigPrimaryButton, primaryButton} from 'app/client/ui2018/buttons';
|
import {basicButton, bigPrimaryButton, primaryButton} from 'app/client/ui2018/buttons';
|
||||||
import {labeledSquareCheckbox} from 'app/client/ui2018/checkbox';
|
import {labeledSquareCheckbox} from 'app/client/ui2018/checkbox';
|
||||||
import {testId, theme, vars} from 'app/client/ui2018/cssVars';
|
import {mediaXSmall, testId, theme, vars} from 'app/client/ui2018/cssVars';
|
||||||
import {icon} from 'app/client/ui2018/icons';
|
import {icon} from 'app/client/ui2018/icons';
|
||||||
import {cssModalTooltip, modalTooltip} from 'app/client/ui2018/modals';
|
import {cssModalTooltip, modalTooltip} from 'app/client/ui2018/modals';
|
||||||
import {dom, DomContents, keyframes, observable, styled, svg} from 'grainjs';
|
import {dom, DomContents, keyframes, observable, styled, svg} from 'grainjs';
|
||||||
@ -138,6 +138,8 @@ export interface ShowBehavioralPromptOptions {
|
|||||||
onClose: (dontShowTips: boolean) => void;
|
onClose: (dontShowTips: boolean) => void;
|
||||||
/** Defaults to false. */
|
/** Defaults to false. */
|
||||||
hideArrow?: boolean;
|
hideArrow?: boolean;
|
||||||
|
/** Defaults to false. */
|
||||||
|
hideDontShowTips?: boolean;
|
||||||
popupOptions?: IPopupOptions;
|
popupOptions?: IPopupOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,7 +149,7 @@ export function showBehavioralPrompt(
|
|||||||
content: DomContents,
|
content: DomContents,
|
||||||
options: ShowBehavioralPromptOptions
|
options: ShowBehavioralPromptOptions
|
||||||
) {
|
) {
|
||||||
const {onClose, hideArrow, popupOptions} = options;
|
const {onClose, hideArrow = false, hideDontShowTips = false, popupOptions} = options;
|
||||||
const arrow = hideArrow ? null : buildArrow();
|
const arrow = hideArrow ? null : buildArrow();
|
||||||
const dontShowTips = observable(false);
|
const dontShowTips = observable(false);
|
||||||
const tooltip = modalTooltip(refElement,
|
const tooltip = modalTooltip(refElement,
|
||||||
@ -180,6 +182,7 @@ export function showBehavioralPrompt(
|
|||||||
cssSkipTipsCheckboxLabel("Don't show tips"),
|
cssSkipTipsCheckboxLabel("Don't show tips"),
|
||||||
testId('behavioral-prompt-dont-show-tips')
|
testId('behavioral-prompt-dont-show-tips')
|
||||||
),
|
),
|
||||||
|
dom.style('visibility', hideDontShowTips ? 'hidden' : ''),
|
||||||
),
|
),
|
||||||
cssDismissPromptButton('Got it', testId('behavioral-prompt-dismiss'),
|
cssDismissPromptButton('Got it', testId('behavioral-prompt-dismiss'),
|
||||||
dom.on('click', () => { onClose(dontShowTips.get()); ctl.close(); })
|
dom.on('click', () => { onClose(dontShowTips.get()); ctl.close(); })
|
||||||
@ -194,6 +197,10 @@ export function showBehavioralPrompt(
|
|||||||
offset: {
|
offset: {
|
||||||
offset: '0,12',
|
offset: '0,12',
|
||||||
},
|
},
|
||||||
|
preventOverflow: {
|
||||||
|
boundariesElement: 'window',
|
||||||
|
padding: 32,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -340,6 +347,12 @@ const cssBehavioralPromptModal = styled('div', `
|
|||||||
&[x-placement^=right] {
|
&[x-placement^=right] {
|
||||||
animation-name: ${cssFadeInFromRight};
|
animation-name: ${cssFadeInFromRight};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media ${mediaXSmall} {
|
||||||
|
& {
|
||||||
|
width: 320px;
|
||||||
|
}
|
||||||
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const cssBehavioralPromptContainer = styled(cssTheme, `
|
const cssBehavioralPromptContainer = styled(cssTheme, `
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
|
import * as commands from 'app/client/components/commands';
|
||||||
|
import {makeT} from 'app/client/lib/localization';
|
||||||
|
import {ShortcutKey, ShortcutKeyContent} from 'app/client/ui/ShortcutKey';
|
||||||
|
import {icon} from 'app/client/ui2018/icons';
|
||||||
import {cssLink} from 'app/client/ui2018/links';
|
import {cssLink} from 'app/client/ui2018/links';
|
||||||
import {commonUrls} from 'app/common/gristUrls';
|
import {commonUrls} from 'app/common/gristUrls';
|
||||||
import {BehavioralPrompt} from 'app/common/Prefs';
|
import {BehavioralPrompt} from 'app/common/Prefs';
|
||||||
import {dom, DomContents, DomElementArg, styled} from 'grainjs';
|
import {dom, DomContents, DomElementArg, styled} from 'grainjs';
|
||||||
import {icon} from 'app/client/ui2018/icons';
|
|
||||||
import {makeT} from 'app/client/lib/localization';
|
|
||||||
|
|
||||||
const t = makeT('GristTooltips');
|
const t = makeT('GristTooltips');
|
||||||
|
|
||||||
@ -202,4 +204,18 @@ export const GristBehavioralPrompts: Record<BehavioralPrompt, BehavioralPromptCo
|
|||||||
...args,
|
...args,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
rickRow: {
|
||||||
|
title: () => t('Anchor Links'),
|
||||||
|
content: (...args: DomElementArg[]) => cssTooltipContent(
|
||||||
|
dom('div',
|
||||||
|
t('To make an anchor link that takes the user to a specific cell, click on'
|
||||||
|
+ ' a row and press {{shortcut}}.',
|
||||||
|
{
|
||||||
|
shortcut: ShortcutKey(ShortcutKeyContent(commands.allCommands.copyLink.humanKeys[0])),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
...args,
|
||||||
|
),
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
@ -1,16 +1,20 @@
|
|||||||
import * as commands from 'app/client/components/commands';
|
import * as commands from 'app/client/components/commands';
|
||||||
import {makeT} from 'app/client/lib/localization';
|
import {makeT} from 'app/client/lib/localization';
|
||||||
|
import {getMainOrgUrl} from 'app/client/models/gristUrlState';
|
||||||
import {cssLinkText, cssPageEntryMain, cssPageIcon, cssPageLink} from 'app/client/ui/LeftPanelCommon';
|
import {cssLinkText, cssPageEntryMain, cssPageIcon, cssPageLink} from 'app/client/ui/LeftPanelCommon';
|
||||||
|
import {YouTubePlayer} from 'app/client/ui/YouTubePlayer';
|
||||||
import {theme} from 'app/client/ui2018/cssVars';
|
import {theme} from 'app/client/ui2018/cssVars';
|
||||||
import {icon} from 'app/client/ui2018/icons';
|
import {icon} from 'app/client/ui2018/icons';
|
||||||
import {cssModalCloseButton, modal} from 'app/client/ui2018/modals';
|
import {cssModalCloseButton, modal} from 'app/client/ui2018/modals';
|
||||||
import {commonUrls, shouldHideUiElement} from 'app/common/gristUrls';
|
import {shouldHideUiElement} from 'app/common/gristUrls';
|
||||||
import {dom, makeTestId, styled} from 'grainjs';
|
import {dom, makeTestId, styled} from 'grainjs';
|
||||||
|
|
||||||
const t = makeT('OpenVideoTour');
|
const t = makeT('OpenVideoTour');
|
||||||
|
|
||||||
const testId = makeTestId('test-video-tour-');
|
const testId = makeTestId('test-video-tour-');
|
||||||
|
|
||||||
|
const VIDEO_TOUR_YOUTUBE_EMBED_ID = 'qnr2Pfnxdlc';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens a modal containing a video tour of Grist.
|
* Opens a modal containing a video tour of Grist.
|
||||||
*/
|
*/
|
||||||
@ -24,15 +28,16 @@ const testId = makeTestId('test-video-tour-');
|
|||||||
dom.on('click', () => ctl.close()),
|
dom.on('click', () => ctl.close()),
|
||||||
testId('close'),
|
testId('close'),
|
||||||
),
|
),
|
||||||
cssVideoWrap(
|
cssYouTubePlayerContainer(
|
||||||
cssVideo(
|
dom.create(YouTubePlayer,
|
||||||
|
VIDEO_TOUR_YOUTUBE_EMBED_ID,
|
||||||
{
|
{
|
||||||
src: commonUrls.videoTour,
|
onPlayerReady: (player) => player.playVideo(),
|
||||||
title: t("YouTube video player"),
|
height: '100%',
|
||||||
frameborder: '0',
|
width: '100%',
|
||||||
allow: 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture',
|
origin: getMainOrgUrl(),
|
||||||
allowfullscreen: '',
|
|
||||||
},
|
},
|
||||||
|
cssYouTubePlayer.cls(''),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
testId('modal'),
|
testId('modal'),
|
||||||
@ -94,18 +99,16 @@ const cssModal = styled('div', `
|
|||||||
max-width: 864px;
|
max-width: 864px;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const cssVideoWrap = styled('div', `
|
const cssYouTubePlayerContainer = styled('div', `
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-bottom: 56.25%;
|
padding-bottom: 56.25%;
|
||||||
height: 0;
|
height: 0;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const cssVideo = styled('iframe', `
|
const cssYouTubePlayer = styled('div', `
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const cssVideoTourTextButton = styled('div', `
|
const cssVideoTourTextButton = styled('div', `
|
||||||
|
25
app/client/ui/ShortcutKey.ts
Normal file
25
app/client/ui/ShortcutKey.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import {theme} from 'app/client/ui2018/cssVars';
|
||||||
|
import {styled} from 'grainjs';
|
||||||
|
|
||||||
|
export const ShortcutKeyContent = styled('span', `
|
||||||
|
font-style: normal;
|
||||||
|
font-family: inherit;
|
||||||
|
color: ${theme.shortcutKeyPrimaryFg};
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const ShortcutKeyContentStrong = styled(ShortcutKeyContent, `
|
||||||
|
font-weight: 700;
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const ShortcutKey = styled('div', `
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 0px 2px;
|
||||||
|
border: 1px solid ${theme.shortcutKeyBorder};
|
||||||
|
color: ${theme.shortcutKeyFg};
|
||||||
|
background-color: ${theme.shortcutKeyBg};
|
||||||
|
font-family: inherit;
|
||||||
|
font-style: normal;
|
||||||
|
white-space: nowrap;
|
||||||
|
`);
|
115
app/client/ui/YouTubePlayer.ts
Normal file
115
app/client/ui/YouTubePlayer.ts
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import {get as getBrowserGlobals} from 'app/client/lib/browserGlobals';
|
||||||
|
import {waitObs} from 'app/common/gutil';
|
||||||
|
import {Disposable, dom, DomElementArg} from 'grainjs';
|
||||||
|
import ko from 'knockout';
|
||||||
|
|
||||||
|
export interface Player {
|
||||||
|
playVideo(): void;
|
||||||
|
pauseVideo(): void;
|
||||||
|
stopVideo(): void;
|
||||||
|
mute(): void;
|
||||||
|
unMute(): void;
|
||||||
|
setVolume(volume: number): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayerOptions {
|
||||||
|
height?: string;
|
||||||
|
width?: string;
|
||||||
|
origin?: string;
|
||||||
|
playerVars?: PlayerVars;
|
||||||
|
onPlayerReady?(player: Player): void
|
||||||
|
onPlayerStateChange?(player: Player, event: PlayerStateChangeEvent): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayerVars {
|
||||||
|
controls?: 0 | 1;
|
||||||
|
disablekb?: 0 | 1;
|
||||||
|
fs?: 0 | 1;
|
||||||
|
iv_load_policy?: 1 | 3;
|
||||||
|
modestbranding?: 0 | 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayerStateChangeEvent {
|
||||||
|
data: PlayerState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PlayerState {
|
||||||
|
Unstarted = -1,
|
||||||
|
Ended = 0,
|
||||||
|
Playing = 1,
|
||||||
|
Paused = 2,
|
||||||
|
Buffering = 3,
|
||||||
|
VideoCued = 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
const G = getBrowserGlobals('document', 'window');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper component for the YouTube IFrame Player API.
|
||||||
|
*
|
||||||
|
* Fetches the JavaScript code for the API if needed, and creates an iframe that
|
||||||
|
* points to a YouTube video with the specified id.
|
||||||
|
*
|
||||||
|
* For more documentation, see https://developers.google.com/youtube/iframe_api_reference.
|
||||||
|
*/
|
||||||
|
export class YouTubePlayer extends Disposable {
|
||||||
|
private _domArgs: DomElementArg[];
|
||||||
|
private _isLoading: ko.Observable<boolean> = ko.observable(true);
|
||||||
|
private _playerId = `youtube-player-${this._videoId}`;
|
||||||
|
private _player: Player;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private _videoId: string,
|
||||||
|
private _options: PlayerOptions,
|
||||||
|
...domArgs: DomElementArg[]
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this._domArgs = domArgs;
|
||||||
|
|
||||||
|
if (!G.window.YT) {
|
||||||
|
const tag = document.createElement('script');
|
||||||
|
|
||||||
|
tag.src = 'https://www.youtube.com/iframe_api';
|
||||||
|
const firstScriptTag = document.getElementsByTagName('script')[0];
|
||||||
|
firstScriptTag?.parentNode?.insertBefore(tag, firstScriptTag);
|
||||||
|
|
||||||
|
G.window.onYouTubeIframeAPIReady = () => this._handleYouTubeIframeAPIReady();
|
||||||
|
} else {
|
||||||
|
setTimeout(() => this._handleYouTubeIframeAPIReady(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public isLoaded() {
|
||||||
|
return waitObs(this._isLoading, (val) => !val);
|
||||||
|
}
|
||||||
|
|
||||||
|
public play() {
|
||||||
|
this._player.playVideo();
|
||||||
|
}
|
||||||
|
|
||||||
|
public setVolume(volume: number) {
|
||||||
|
this._player.setVolume(volume);
|
||||||
|
}
|
||||||
|
|
||||||
|
public buildDom() {
|
||||||
|
return dom('div', {id: this._playerId}, ...this._domArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleYouTubeIframeAPIReady() {
|
||||||
|
const {onPlayerReady, onPlayerStateChange, playerVars, ...otherOptions} = this._options;
|
||||||
|
this._player = new G.window.YT.Player(this._playerId, {
|
||||||
|
videoId: this._videoId,
|
||||||
|
playerVars,
|
||||||
|
events: {
|
||||||
|
onReady: () => {
|
||||||
|
this._isLoading(false);
|
||||||
|
onPlayerReady?.(this._player);
|
||||||
|
},
|
||||||
|
onStateChange: (event: PlayerStateChangeEvent) =>
|
||||||
|
onPlayerStateChange?.(this._player, event),
|
||||||
|
},
|
||||||
|
...otherOptions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@ import { makeT } from 'app/client/lib/localization';
|
|||||||
import * as commands from 'app/client/components/commands';
|
import * as commands from 'app/client/components/commands';
|
||||||
import { urlState } from 'app/client/models/gristUrlState';
|
import { urlState } from 'app/client/models/gristUrlState';
|
||||||
import { IOnBoardingMsg, startOnBoarding } from "app/client/ui/OnBoardingPopups";
|
import { IOnBoardingMsg, startOnBoarding } from "app/client/ui/OnBoardingPopups";
|
||||||
|
import { ShortcutKey, ShortcutKeyContent } from 'app/client/ui/ShortcutKey';
|
||||||
import { theme } from 'app/client/ui2018/cssVars';
|
import { theme } from 'app/client/ui2018/cssVars';
|
||||||
import { icon } from "app/client/ui2018/icons";
|
import { icon } from "app/client/ui2018/icons";
|
||||||
import { cssLink } from "app/client/ui2018/links";
|
import { cssLink } from "app/client/ui2018/links";
|
||||||
@ -14,8 +15,12 @@ export const welcomeTour: IOnBoardingMsg[] = [
|
|||||||
title: t('Editing Data'),
|
title: t('Editing Data'),
|
||||||
body: () => [
|
body: () => [
|
||||||
dom('p',
|
dom('p',
|
||||||
t('Double-click or hit {{enter}} on a cell to edit it. ', {enter: Key(KeyContent(t('Enter')))}),
|
t('Double-click or hit {{enter}} on a cell to edit it. ', {
|
||||||
t('Start with {{equal}} to enter a formula.', { equal: Key(KeyStrong('=')) }))
|
enter: ShortcutKey(ShortcutKeyContent(t('Enter'))),
|
||||||
|
}),
|
||||||
|
t('Start with {{equal}} to enter a formula.', {
|
||||||
|
equal: ShortcutKey(ShortcutKeyContent('=')),
|
||||||
|
})),
|
||||||
],
|
],
|
||||||
selector: '.field_clip',
|
selector: '.field_clip',
|
||||||
placement: 'bottom',
|
placement: 'bottom',
|
||||||
@ -39,8 +44,9 @@ export const welcomeTour: IOnBoardingMsg[] = [
|
|||||||
dom('p',
|
dom('p',
|
||||||
t('Set formatting options, formulas, or column types, such as dates, choices, or attachments. ')),
|
t('Set formatting options, formulas, or column types, such as dates, choices, or attachments. ')),
|
||||||
dom('p',
|
dom('p',
|
||||||
t('Make it relational! Use the {{ref}} type to link tables. ', {ref: Key(t('Reference'))}),
|
t('Make it relational! Use the {{ref}} type to link tables. ', {
|
||||||
)
|
ref: ShortcutKey(t('Reference')),
|
||||||
|
})),
|
||||||
],
|
],
|
||||||
placement: 'right',
|
placement: 'right',
|
||||||
},
|
},
|
||||||
@ -48,7 +54,9 @@ export const welcomeTour: IOnBoardingMsg[] = [
|
|||||||
selector: '.tour-add-new',
|
selector: '.tour-add-new',
|
||||||
title: t('Building up'),
|
title: t('Building up'),
|
||||||
body: () => [
|
body: () => [
|
||||||
dom('p', t('Use {{addNew}} to add widgets, pages, or import more data. ', {addNew: Key(t('Add New'))}))
|
dom('p', t('Use {{addNew}} to add widgets, pages, or import more data. ', {
|
||||||
|
addNew: ShortcutKey(t('Add New')),
|
||||||
|
})),
|
||||||
],
|
],
|
||||||
placement: 'right',
|
placement: 'right',
|
||||||
},
|
},
|
||||||
@ -67,7 +75,7 @@ export const welcomeTour: IOnBoardingMsg[] = [
|
|||||||
title: t('Flying higher'),
|
title: t('Flying higher'),
|
||||||
body: () => [
|
body: () => [
|
||||||
dom('p', t('Use {{helpCenter}} for documentation or questions.',
|
dom('p', t('Use {{helpCenter}} for documentation or questions.',
|
||||||
{helpCenter: Key(GreyIcon('Help'), t('Help Center'))})),
|
{helpCenter: ShortcutKey(GreyIcon('Help'), t('Help Center'))}))
|
||||||
],
|
],
|
||||||
placement: 'right',
|
placement: 'right',
|
||||||
},
|
},
|
||||||
@ -92,29 +100,6 @@ export function startWelcomeTour(onFinishCB: () => void) {
|
|||||||
startOnBoarding(welcomeTour, onFinishCB);
|
startOnBoarding(welcomeTour, onFinishCB);
|
||||||
}
|
}
|
||||||
|
|
||||||
const KeyContent = styled('span', `
|
|
||||||
font-style: normal;
|
|
||||||
font-family: inherit;
|
|
||||||
color: ${theme.shortcutKeyPrimaryFg};
|
|
||||||
`);
|
|
||||||
|
|
||||||
const KeyStrong = styled(KeyContent, `
|
|
||||||
font-weight: 700;
|
|
||||||
`);
|
|
||||||
|
|
||||||
const Key = styled('div', `
|
|
||||||
display: inline-block;
|
|
||||||
padding: 2px 5px;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin: 0px 2px;
|
|
||||||
border: 1px solid ${theme.shortcutKeyBorder};
|
|
||||||
color: ${theme.shortcutKeyFg};
|
|
||||||
background-color: ${theme.shortcutKeyBg};
|
|
||||||
font-family: inherit;
|
|
||||||
font-style: normal;
|
|
||||||
white-space: nowrap;
|
|
||||||
`);
|
|
||||||
|
|
||||||
const TopBarButtonIcon = styled(icon, `
|
const TopBarButtonIcon = styled(icon, `
|
||||||
--icon-color: ${theme.topBarButtonPrimaryFg};
|
--icon-color: ${theme.topBarButtonPrimaryFg};
|
||||||
`);
|
`);
|
||||||
|
@ -85,6 +85,7 @@ export const BehavioralPrompt = StringUnion(
|
|||||||
'pageWidgetPickerSelectBy',
|
'pageWidgetPickerSelectBy',
|
||||||
'editCardLayout',
|
'editCardLayout',
|
||||||
'addNew',
|
'addNew',
|
||||||
|
'rickRow',
|
||||||
);
|
);
|
||||||
export type BehavioralPrompt = typeof BehavioralPrompt.type;
|
export type BehavioralPrompt = typeof BehavioralPrompt.type;
|
||||||
|
|
||||||
|
@ -77,7 +77,6 @@ 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',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -261,7 +260,11 @@ export function encodeUrl(gristConfig: Partial<GristLoadConfig>,
|
|||||||
const hash = state.hash;
|
const hash = state.hash;
|
||||||
hashParts.push(state.hash?.popup ? 'a2' : `a1`);
|
hashParts.push(state.hash?.popup ? 'a2' : `a1`);
|
||||||
for (const key of ['sectionId', 'rowId', 'colRef'] as Array<keyof HashLink>) {
|
for (const key of ['sectionId', 'rowId', 'colRef'] as Array<keyof HashLink>) {
|
||||||
if (hash[key]) { hashParts.push(`${key[0]}${hash[key]}`); }
|
const partValue = hash[key];
|
||||||
|
if (partValue) {
|
||||||
|
const partKey = key === 'rowId' && state.hash?.rickRow ? 'rr' : key[0];
|
||||||
|
hashParts.push(`${partKey}${partValue}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const queryStr = encodeQueryParams(queryParams);
|
const queryStr = encodeQueryParams(queryParams);
|
||||||
@ -379,13 +382,28 @@ export function decodeUrl(gristConfig: Partial<GristLoadConfig>, location: Locat
|
|||||||
const hashParts = hash.split('.');
|
const hashParts = hash.split('.');
|
||||||
const hashMap = new Map<string, string>();
|
const hashMap = new Map<string, string>();
|
||||||
for (const part of hashParts) {
|
for (const part of hashParts) {
|
||||||
hashMap.set(part.slice(0, 1), part.slice(1));
|
if (part.startsWith('rr')) {
|
||||||
|
hashMap.set(part.slice(0, 2), part.slice(2));
|
||||||
|
} else {
|
||||||
|
hashMap.set(part.slice(0, 1), part.slice(1));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (hashMap.has('#') && ['a1', 'a2'].includes(hashMap.get('#') || '')) {
|
if (hashMap.has('#') && ['a1', 'a2'].includes(hashMap.get('#') || '')) {
|
||||||
const link: HashLink = {};
|
const link: HashLink = {};
|
||||||
for (const key of ['sectionId', 'rowId', 'colRef'] as Array<Exclude<keyof HashLink, 'popup'>>) {
|
const keys = [
|
||||||
const ch = key.substr(0, 1);
|
'sectionId',
|
||||||
if (!hashMap.has(ch)) { continue; }
|
'rowId',
|
||||||
|
'colRef',
|
||||||
|
] as Array<Exclude<keyof HashLink, 'popup' | 'rickRow'>>;
|
||||||
|
for (const key of keys) {
|
||||||
|
let ch: string;
|
||||||
|
if (key === 'rowId' && hashMap.has('rr')) {
|
||||||
|
ch = 'rr';
|
||||||
|
link.rickRow = true;
|
||||||
|
} else {
|
||||||
|
ch = key.substr(0, 1);
|
||||||
|
if (!hashMap.has(ch)) { continue; }
|
||||||
|
}
|
||||||
const value = hashMap.get(ch);
|
const value = hashMap.get(ch);
|
||||||
if (key === 'rowId' && value === 'new') {
|
if (key === 'rowId' && value === 'new') {
|
||||||
link[key] = 'new';
|
link[key] = 'new';
|
||||||
@ -819,6 +837,7 @@ export interface HashLink {
|
|||||||
rowId?: UIRowId;
|
rowId?: UIRowId;
|
||||||
colRef?: number;
|
colRef?: number;
|
||||||
popup?: boolean;
|
popup?: boolean;
|
||||||
|
rickRow?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check whether a urlId is a prefix of the docId, and adequately long to be
|
// Check whether a urlId is a prefix of the docId, and adequately long to be
|
||||||
|
Loading…
Reference in New Issue
Block a user