Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] Halloween easter egg! #157

Merged
merged 15 commits into from
Apr 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions _sass/spec/base.scss
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,15 @@ $INFO_BORDER_COLOR: rgba(4, 66, 137, 0.2);
// On mobile, make space for the topbar.
padding-top: $ICON_FONT_SIZE;
}
&-frozen {
// When the settings pane is shown as an overlay, we want to prevent
// scrolling on the underlying main content. These styles prevent
// scrolling.
// (They also reset the scroll position, so JS is responsible for restoring
// the original scroll state after the settings pane is closed.)
position: fixed;
overflow: hidden;
}
}

// These are section headers in the sidebar
Expand Down
40 changes: 39 additions & 1 deletion src_js/components/PrimerSpec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
getStoredSubthemeMode,
getStoredSubthemeName,
updateTheme,
normalizeSubthemeMode,
} from '../subthemes';
import getChromeVersion from '../utils/getChromeVersion';
import { useAfterPrint, useBeforePrint } from '../utils/hooks/print';
Expand All @@ -17,6 +18,8 @@ import Storage from '../utils/Storage';

type PropsType = { contentHTML: string };

let mainElScrollPosition: null | { top: number; left: number } = null;

/**
* This component encapsulates the JS controlling Primer Spec, including the
* Sidebar, the Topbar and the Settings pane.
Expand All @@ -37,12 +40,22 @@ export default function PrimerSpec(props: PropsType): h.JSX.Element {
Config.INIT_SITEMAP_ENABLED,
);

const main_content_visible = !settings_shown;

// Define derived methods to manipulate state
const toggleSidebarShown = () => {
Storage.setForPage('sidebar_hidden', sidebar_shown.toString());
setSidebarShown(!sidebar_shown);
};
const toggleSettingsShown = () => setSettingsShown(!settings_shown);
const toggleSettingsShown = () => {
// Before toggling the settings, save the current scroll position of the
// main content. We'll need it later to restore the scroll position after
// the settings pane is closed.
if (main_content_visible) {
mainElScrollPosition = { top: window.scrollY, left: window.scrollX };
}
setSettingsShown(!settings_shown);
};
const setTheme = (themeDelta: Partial<SubthemeSelectionType>) => {
updateTheme(themeDelta);
setSubthemeName(getStoredSubthemeName());
Expand Down Expand Up @@ -72,6 +85,28 @@ export default function PrimerSpec(props: PropsType): h.JSX.Element {
});
}, [sitemap_enabled]);

// Lazy-load the conditional plugins. These are purely cosmetic and
// don't affect the functionality of the page.
useEffect(() => {
import('../conditional_plugins/conditional_plugins').then(
({ executePlugins }) => {
executePlugins({
is_small_screen,
sidebar_shown,
settings_shown,
subtheme_name,
subtheme_mode: normalizeSubthemeMode(subtheme_mode),
});
},
);
}, [
is_small_screen,
sidebar_shown,
settings_shown,
subtheme_name,
subtheme_mode,
]);

const sidebar = Config.DISABLE_SIDEBAR ? null : (
<Sidebar
contentNodeSelector={`#${Config.PRIMER_SPEC_CONTENT_PREACT_NODE_ID}`}
Expand Down Expand Up @@ -99,6 +134,9 @@ export default function PrimerSpec(props: PropsType): h.JSX.Element {
/>
<MainContent
innerHTML={props.contentHTML}
visible={main_content_visible}
// Only attempt to restore scroll-state if the main content is visible
scrollToPosition={main_content_visible ? mainElScrollPosition : null}
isSmallScreen={is_small_screen}
sidebarShown={sidebar_shown}
currentSubthemeName={subtheme_name}
Expand Down
45 changes: 31 additions & 14 deletions src_js/components/main_content/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { h } from 'preact';
import { useCallback, useEffect, useRef } from 'preact/hooks';
import { useCallback, useEffect, useLayoutEffect, useRef } from 'preact/hooks';
import clsx from 'clsx';
import Config from '../../Config';
import { usePrintInProgress } from '../../utils/hooks/print';
Expand All @@ -11,33 +11,43 @@ import usePrefersDarkMode from '../../utils/hooks/usePrefersDarkMode';

type PropsType = {
innerHTML: string;
visible: boolean;
scrollToPosition: null | { top: number; left: number };
isSmallScreen: boolean;
sidebarShown: boolean;
currentSubthemeName: string;
currentSubthemeMode: SubthemeModeSelectorType;
};

export default function MainContent(props: PropsType): h.JSX.Element {
export default function MainContent({
innerHTML,
visible,
scrollToPosition,
isSmallScreen,
sidebarShown,
currentSubthemeName,
currentSubthemeMode,
}: PropsType): h.JSX.Element | null {
const is_print_in_progress = usePrintInProgress();
const prefers_dark_mode = usePrefersDarkMode();
const main_el_ref = useRef<HTMLElement>(null);

const taskListCheckboxEffect = useCallback(useTaskListCheckboxes, [
props.innerHTML,
innerHTML,
]);
useEffect(() => {
return taskListCheckboxEffect(main_el_ref);
}, [taskListCheckboxEffect]);

const enhancedCodeBlocksEffect = useCallback(useEnhancedCodeBlocks, [
props.innerHTML,
innerHTML,
]);
useEffect(() => {
return enhancedCodeBlocksEffect(main_el_ref);
}, [enhancedCodeBlocksEffect]);

let should_use_dark_mode = false;
switch (props.currentSubthemeMode) {
switch (currentSubthemeMode) {
case 'system':
should_use_dark_mode = prefers_dark_mode;
break;
Expand All @@ -47,36 +57,43 @@ export default function MainContent(props: PropsType): h.JSX.Element {
default:
should_use_dark_mode = false;
}
if (props.currentSubthemeName === 'xcode-civic') {
if (
currentSubthemeName === 'xcode-civic' ||
currentSubthemeName === 'spooky'
) {
should_use_dark_mode = true;
}
const mermaidDiagramsEffect = useCallback(useMermaidDiagrams, [
props.innerHTML,
]);
const mermaidDiagramsEffect = useCallback(useMermaidDiagrams, [innerHTML]);
useEffect(() => {
return mermaidDiagramsEffect(main_el_ref, should_use_dark_mode);
}, [mermaidDiagramsEffect, should_use_dark_mode]);

const tooltippedAbbreviationsEffect = useCallback(
useTooltippedAbbreviations,
[props.innerHTML],
[innerHTML],
);
useEffect(() => {
return tooltippedAbbreviationsEffect(main_el_ref);
}, [tooltippedAbbreviationsEffect]);

useLayoutEffect(() => {
if (scrollToPosition != null) {
window.scrollTo(scrollToPosition);
}
}, [scrollToPosition]);

return (
<main
ref={main_el_ref}
id={Config.PRIMER_SPEC_CONTENT_PREACT_NODE_ID}
class={clsx('container-lg', 'px-3', 'my-5', 'markdown-body', {
'primer-spec-content-margin-extra':
props.sidebarShown && !props.isSmallScreen && !is_print_in_progress,
'primer-spec-content-mobile':
props.isSmallScreen && !is_print_in_progress,
sidebarShown && !isSmallScreen && !is_print_in_progress,
'primer-spec-content-mobile': isSmallScreen && !is_print_in_progress,
'primer-spec-content-frozen': !visible,
})}
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: props.innerHTML }}
dangerouslySetInnerHTML={{ __html: innerHTML }}
/>
);
}
33 changes: 33 additions & 0 deletions src_js/conditional_plugins/conditional_plugins.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { initialize as initializeHalloweenPlugin } from './halloween.plugin';

import type { ConditionalPluginInput } from './types.d';

const PLUGINS = [initializeHalloweenPlugin()]
.filter((pluginDefinition) => {
const forceEnableOption = pluginForceEnableOption(pluginDefinition.id);
if (forceEnableOption !== null) {
return forceEnableOption;
}
return pluginDefinition.shouldRun();
})
.map((pluginDefinition) => pluginDefinition.plugin);

export async function executePlugins(
input: ConditionalPluginInput,
): Promise<void> {
await Promise.all(
PLUGINS.map(async (plugin) => {
await plugin(input);
}),
);
}

function pluginForceEnableOption(pluginId: string): boolean | null {
const match = window.location.search.match(
new RegExp(`enable_${pluginId}=([0|1])`),
);
if (match) {
return match[1] === '1';
}
return null;
}
Loading