mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +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:
@@ -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 {commonUrls} from 'app/common/gristUrls';
|
||||
import {BehavioralPrompt} from 'app/common/Prefs';
|
||||
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');
|
||||
|
||||
@@ -202,4 +204,18 @@ export const GristBehavioralPrompts: Record<BehavioralPrompt, BehavioralPromptCo
|
||||
...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 {makeT} from 'app/client/lib/localization';
|
||||
import {getMainOrgUrl} from 'app/client/models/gristUrlState';
|
||||
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 {icon} from 'app/client/ui2018/icons';
|
||||
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';
|
||||
|
||||
const t = makeT('OpenVideoTour');
|
||||
|
||||
const testId = makeTestId('test-video-tour-');
|
||||
|
||||
const VIDEO_TOUR_YOUTUBE_EMBED_ID = 'qnr2Pfnxdlc';
|
||||
|
||||
/**
|
||||
* Opens a modal containing a video tour of Grist.
|
||||
*/
|
||||
@@ -24,15 +28,16 @@ const testId = makeTestId('test-video-tour-');
|
||||
dom.on('click', () => ctl.close()),
|
||||
testId('close'),
|
||||
),
|
||||
cssVideoWrap(
|
||||
cssVideo(
|
||||
cssYouTubePlayerContainer(
|
||||
dom.create(YouTubePlayer,
|
||||
VIDEO_TOUR_YOUTUBE_EMBED_ID,
|
||||
{
|
||||
src: commonUrls.videoTour,
|
||||
title: t("YouTube video player"),
|
||||
frameborder: '0',
|
||||
allow: 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture',
|
||||
allowfullscreen: '',
|
||||
onPlayerReady: (player) => player.playVideo(),
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
origin: getMainOrgUrl(),
|
||||
},
|
||||
cssYouTubePlayer.cls(''),
|
||||
),
|
||||
),
|
||||
testId('modal'),
|
||||
@@ -94,18 +99,16 @@ const cssModal = styled('div', `
|
||||
max-width: 864px;
|
||||
`);
|
||||
|
||||
const cssVideoWrap = styled('div', `
|
||||
const cssYouTubePlayerContainer = styled('div', `
|
||||
position: relative;
|
||||
padding-bottom: 56.25%;
|
||||
height: 0;
|
||||
`);
|
||||
|
||||
const cssVideo = styled('iframe', `
|
||||
const cssYouTubePlayer = styled('div', `
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`);
|
||||
|
||||
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 { urlState } from 'app/client/models/gristUrlState';
|
||||
import { IOnBoardingMsg, startOnBoarding } from "app/client/ui/OnBoardingPopups";
|
||||
import { ShortcutKey, ShortcutKeyContent } from 'app/client/ui/ShortcutKey';
|
||||
import { theme } from 'app/client/ui2018/cssVars';
|
||||
import { icon } from "app/client/ui2018/icons";
|
||||
import { cssLink } from "app/client/ui2018/links";
|
||||
@@ -14,8 +15,12 @@ export const welcomeTour: IOnBoardingMsg[] = [
|
||||
title: t('Editing Data'),
|
||||
body: () => [
|
||||
dom('p',
|
||||
t('Double-click or hit {{enter}} on a cell to edit it. ', {enter: Key(KeyContent(t('Enter')))}),
|
||||
t('Start with {{equal}} to enter a formula.', { equal: Key(KeyStrong('=')) }))
|
||||
t('Double-click or hit {{enter}} on a cell to edit it. ', {
|
||||
enter: ShortcutKey(ShortcutKeyContent(t('Enter'))),
|
||||
}),
|
||||
t('Start with {{equal}} to enter a formula.', {
|
||||
equal: ShortcutKey(ShortcutKeyContent('=')),
|
||||
})),
|
||||
],
|
||||
selector: '.field_clip',
|
||||
placement: 'bottom',
|
||||
@@ -39,8 +44,9 @@ export const welcomeTour: IOnBoardingMsg[] = [
|
||||
dom('p',
|
||||
t('Set formatting options, formulas, or column types, such as dates, choices, or attachments. ')),
|
||||
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',
|
||||
},
|
||||
@@ -48,7 +54,9 @@ export const welcomeTour: IOnBoardingMsg[] = [
|
||||
selector: '.tour-add-new',
|
||||
title: t('Building up'),
|
||||
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',
|
||||
},
|
||||
@@ -67,7 +75,7 @@ export const welcomeTour: IOnBoardingMsg[] = [
|
||||
title: t('Flying higher'),
|
||||
body: () => [
|
||||
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',
|
||||
},
|
||||
@@ -92,29 +100,6 @@ export function startWelcomeTour(onFinishCB: () => void) {
|
||||
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, `
|
||||
--icon-color: ${theme.topBarButtonPrimaryFg};
|
||||
`);
|
||||
|
||||
Reference in New Issue
Block a user