diff --git a/demo/src/test.mdx b/demo/src/test.mdx index 0401a58..a2198b7 100644 --- a/demo/src/test.mdx +++ b/demo/src/test.mdx @@ -6,10 +6,15 @@ layout: center, className: test +++ # test + +// test note + --- + # test 2 + ```jsx showLineNumbers highlightLines="1,2,5-6" -function Counter({initialCount}) { +function Counter({ initialCount }) { const [count, setCount] = useState(initialCount); return ( <> @@ -21,8 +26,11 @@ function Counter({initialCount}) { ); } ``` + --- + # test 3 + {` flowchart TD diff --git a/packages/presentify/__tests__/PresentifyProvider.spec.tsx b/packages/presentify/__tests__/PresentifyProvider.spec.tsx index d57666d..4ea934c 100644 --- a/packages/presentify/__tests__/PresentifyProvider.spec.tsx +++ b/packages/presentify/__tests__/PresentifyProvider.spec.tsx @@ -16,10 +16,12 @@ const keys = [ ]; const string = '+++\nlayout: normal\n+++'; +const notes = '// test note'; const Test = () => (

{string}

+

{notes}


@@ -110,6 +112,19 @@ describe('PresentifyProvider', () => { '404 - Not Found', ); }); + it('Show correct nextSlide in presenter mode', async () => { + window.history.pushState( + undefined, + '', + `${window.location + .toString() + .replace(window.location.search, '')}?page=1`, + ); + render(); + await userEvent.keyboard('{alt>}{p}'); + screen.debug(); + expect(screen.getAllByTestId('slide')[1].children).toHaveLength(2); + }); }); describe('Keyboard', () => { @@ -142,4 +157,33 @@ describe('Keyboard', () => { await userEvent.keyboard('{Enter}'); expect(screen.getByTestId('slide').children).toHaveLength(2); }); + it('option + p key was pressed', async () => { + render(); + await userEvent.keyboard('{alt>}{p}'); + screen.debug(); + expect(screen.getByText('Current Slide')).toBeInTheDocument(); + expect(screen.getByText('NextSlide')).toBeInTheDocument(); + expect(screen.getByText('Notes')).toBeInTheDocument(); + }); + it('option + p key was pressed twice', async () => { + render(); + await userEvent.keyboard('{alt>}{p}'); + await userEvent.keyboard('{alt>}{p}'); + expect(screen.getByTestId('slide').children).toHaveLength(2); + }); + it('option + p key was pressed when disablePresenterMode option wa true', async () => { + render( + +

{string}

+
+
+
+
+
+
+ , + ); + await userEvent.keyboard('{alt>}{p}'); + expect(screen.getByTestId('slide').children).toHaveLength(2); + }); }); diff --git a/packages/presentify/src/components/NormalLayout.tsx b/packages/presentify/src/components/NormalLayout.tsx new file mode 100644 index 0000000..d46bbe6 --- /dev/null +++ b/packages/presentify/src/components/NormalLayout.tsx @@ -0,0 +1,17 @@ +import React from 'react'; + +import { NotFound } from './NotFound'; +import { Slide } from './Slide'; +import { Layout } from '../styles/NormalLayout.styled'; + +export const NormalLayout = ({ + slides, + currentSlide, +}: { + slides: React.ReactNode[]; + currentSlide: number; +}) => ( + + {slides[currentSlide] ? slides[currentSlide] : } + +); diff --git a/packages/presentify/src/components/PresenterLayout.tsx b/packages/presentify/src/components/PresenterLayout.tsx new file mode 100644 index 0000000..c2cf36f --- /dev/null +++ b/packages/presentify/src/components/PresenterLayout.tsx @@ -0,0 +1,53 @@ +import React from 'react'; + +import { Slide } from './Slide'; +import { getSlideNotes } from '../lib/getSlideNotes'; +import { + Layout, + NextSlideAndNotesWrapper, + NextSlideContainer, + NextSlideWrapper, + NotesContainer, + NotesWrapper, + SlideContainer, + SlideWrapper, +} from '../styles/PresenterLayout.styled'; + +export const PresenterLayout = ({ + slides, + currentSlide, +}: { + slides: React.ReactNode[]; + currentSlide: number; +}) => { + const { notes } = getSlideNotes(slides[currentSlide]); + + return ( + + +

Current Slide

+ + {slides[currentSlide]} + +
+ + +

NextSlide

+ + {slides[currentSlide + 1] ? ( + {slides[currentSlide + 1]} + ) : ( + {slides[0]} + )} + +
+ {notes && ( + +

Notes

+ {notes} +
+ )} +
+
+ ); +}; diff --git a/packages/presentify/src/components/PresentifyProvider.tsx b/packages/presentify/src/components/PresentifyProvider.tsx index 69664af..6440004 100644 --- a/packages/presentify/src/components/PresentifyProvider.tsx +++ b/packages/presentify/src/components/PresentifyProvider.tsx @@ -1,4 +1,5 @@ import { Global } from '@emotion/react'; +import { isNil } from 'lodash'; import React, { createContext, ReactNode, @@ -8,12 +9,12 @@ import React, { } from 'react'; import { Keyboard } from './Keyboard'; -import { NotFound } from './NotFound'; -import { Slide } from './Slide'; +import { NormalLayout } from './NormalLayout'; +import { PresenterLayout } from './PresenterLayout'; import { useQueryParams } from '../hooks/useQueryParams'; +import { useSharedState } from '../hooks/useSharedState'; import { splitSlides } from '../lib/splitSlides'; import { globalStyles } from '../styles/GlobalStyles.styled'; -import { Layout } from '../styles/Layout.styled'; import { Options, PresentifyContextProps } from '../types/types'; const PresentifyContext = createContext(null); @@ -28,9 +29,10 @@ export const PresentifyProvider = ({ const { getParams, setParams } = useQueryParams(); const pageFromParams = parseInt(getParams('page') || '0', 10); - const [currentSlide, setCurrentSlide] = useState( + const [currentSlide, setCurrentSlide] = useSharedState( isNaN(pageFromParams) ? 0 : pageFromParams, ); + const [presenterMode, setPresenterMode] = useState(false); useEffect(() => { setParams('page', currentSlide.toString()); @@ -54,23 +56,34 @@ export const PresentifyProvider = ({ return prevState - 1; }); + const togglePresenterMode = () => { + if (options?.disablePresenterMode !== true) { + setPresenterMode(prevState => !prevState); + if (!presenterMode) { + window.open(window.location.href, '_blank'); + } + } + }; + const contextValue = { slides, options, currentSlide, onGoNextSlide, onGoBackSlide, + presenterMode, + togglePresenterMode, }; return ( - - - {slides[currentSlide] ? slides[currentSlide] : } - - + {presenterMode ? ( + + ) : ( + + )} ); }; diff --git a/packages/presentify/src/components/Slide.tsx b/packages/presentify/src/components/Slide.tsx index d5fe079..6493145 100644 --- a/packages/presentify/src/components/Slide.tsx +++ b/packages/presentify/src/components/Slide.tsx @@ -1,12 +1,14 @@ import React, { ReactNode } from 'react'; import { usePresentifyContext } from './PresentifyProvider'; +import { getSlideNotes } from '../lib/getSlideNotes'; import { getSlideOptions } from '../lib/getSlideOptions'; import { SliderWrapper } from '../styles/Slide.styled'; export const Slide = ({ children }: { children: ReactNode }) => { const { slideWithoutOptions, options: SlideOptions } = getSlideOptions(children); + const { slideWithoutNotes } = getSlideNotes(slideWithoutOptions); const { className, backgroundColor, backgroundImg, layout } = SlideOptions || {}; const presentifyContext = usePresentifyContext(); @@ -24,7 +26,7 @@ export const Slide = ({ children }: { children: ReactNode }) => { backgroundImg={backgroundImg || globalBackgroundImg} layout={layout || globalLayout} > - {slideWithoutOptions} + {slideWithoutNotes} ); }; diff --git a/packages/presentify/src/hooks/useKeyboard.tsx b/packages/presentify/src/hooks/useKeyboard.tsx index bd72a41..7201745 100644 --- a/packages/presentify/src/hooks/useKeyboard.tsx +++ b/packages/presentify/src/hooks/useKeyboard.tsx @@ -5,25 +5,38 @@ import { Keys } from '../types/types'; export const useKeyboard = () => { const context = usePresentifyContext(); - const { onGoNextSlide, onGoBackSlide, currentSlide } = context || {}; + const { onGoNextSlide, onGoBackSlide, currentSlide, togglePresenterMode } = + context || {}; useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { - switch (e.code) { - case Keys.Right: - case Keys.Up: { - e.preventDefault(); - onGoNextSlide && onGoNextSlide(); - break; + if (e.altKey) { + switch (e.code) { + case Keys.P: { + e.preventDefault(); + togglePresenterMode && togglePresenterMode(); + break; + } + default: + break; } - case Keys.Left: - case Keys.Down: { - e.preventDefault(); - onGoBackSlide && onGoBackSlide(); - break; + } else { + switch (e.code) { + case Keys.Right: + case Keys.Up: { + e.preventDefault(); + onGoNextSlide && onGoNextSlide(); + break; + } + case Keys.Left: + case Keys.Down: { + e.preventDefault(); + onGoBackSlide && onGoBackSlide(); + break; + } + default: + break; } - default: - break; } }; @@ -32,5 +45,5 @@ export const useKeyboard = () => { return () => { document.removeEventListener('keydown', handleKeyDown); }; - }, [currentSlide, onGoBackSlide, onGoNextSlide]); + }, [currentSlide, onGoBackSlide, onGoNextSlide, togglePresenterMode]); }; diff --git a/packages/presentify/src/hooks/useSharedState.tsx b/packages/presentify/src/hooks/useSharedState.tsx new file mode 100644 index 0000000..42caff3 --- /dev/null +++ b/packages/presentify/src/hooks/useSharedState.tsx @@ -0,0 +1,55 @@ +import { useEffect, useState } from 'react'; + +type UseSharedReturnType = [ + T, + (newState: T | ((prevState: T) => T)) => void, +]; +export function useSharedState( + initialState: T, +): UseSharedReturnType { + const [state, setState] = useState(initialState); + const broadcastChannelEnabled = window.BroadcastChannel !== undefined; + const broadcastChannel = broadcastChannelEnabled + ? new BroadcastChannel('presentifyChannel') + : undefined; + + const setSharedState = (newState: T | ((prevState: T) => T)): void => { + setState((prevState: T): T => { + const updatedState = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + typeof newState === 'function' ? newState(prevState) : newState; + + broadcastChannelEnabled && + (broadcastChannel as BroadcastChannel).postMessage( + JSON.stringify(updatedState), + ); + + return updatedState; + }); + }; + + const handleMessage = (event: MessageEvent) => { + const newState: T = JSON.parse(event.data); + setState(newState); + }; + + useEffect(() => { + broadcastChannelEnabled && + (broadcastChannel as BroadcastChannel).addEventListener( + 'message', + handleMessage, + ); + + return () => { + broadcastChannelEnabled && + (broadcastChannel as BroadcastChannel).removeEventListener( + 'message', + handleMessage, + ); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return [state, setSharedState]; +} diff --git a/packages/presentify/src/lib/getSlideNotes.ts b/packages/presentify/src/lib/getSlideNotes.ts new file mode 100644 index 0000000..321037b --- /dev/null +++ b/packages/presentify/src/lib/getSlideNotes.ts @@ -0,0 +1,22 @@ +import { Children, ReactElement, ReactNode } from 'react'; + +export const getSlideNotes = (slide: ReactNode) => { + const optionsRegex = /^([/]{2})(.*)$/gs; + const elementArray = Children.toArray(slide); + let notes: string | undefined; + const slideWithoutNotes = elementArray + ?.map(element => { + const { type, props } = element as ReactElement; + if ( + type === 'p' && + typeof props.children === 'string' && + optionsRegex.test(props.children.toString()) + ) { + notes = props.children.toString().replace('//', ''); + return null; + } + return element; + }) + .filter(element => element); + return { slideWithoutNotes, notes }; +}; diff --git a/packages/presentify/src/styles/Layout.styled.ts b/packages/presentify/src/styles/NormalLayout.styled.ts similarity index 100% rename from packages/presentify/src/styles/Layout.styled.ts rename to packages/presentify/src/styles/NormalLayout.styled.ts diff --git a/packages/presentify/src/styles/PresenterLayout.styled.ts b/packages/presentify/src/styles/PresenterLayout.styled.ts new file mode 100644 index 0000000..14d48a7 --- /dev/null +++ b/packages/presentify/src/styles/PresenterLayout.styled.ts @@ -0,0 +1,54 @@ +import styled from '@emotion/styled'; +export const Layout = styled.div` + width: 100vw; + height: 100vh; + background-color: #6c5ce7; + padding: 0 50px 50px 50px; + gap: 50px; + display: flex; +`; + +export const SlideContainer = styled.div` + width: 60%; + height: 100%; +`; + +export const SlideWrapper = styled.div` + width: 100%; + height: calc(100% - 38px); + border: 5px solid #2d3436; + background-color: #fff; +`; + +export const NextSlideAndNotesWrapper = styled.div` + width: 40%; + height: 100%; + gap: 50px; + display: flex; + flex-flow: column; +`; + +export const NextSlideContainer = styled.div` + width: 100%; + height: 50%; +`; + +export const NextSlideWrapper = styled.div` + width: 100%; + height: calc(100% - 38px); + border: 5px solid #2d3436; + background-color: #fff; +`; + +export const NotesContainer = styled.div` + width: 100%; + height: 50%; +`; + +export const NotesWrapper = styled.div` + width: 100%; + height: calc(100% - 38px); + border: 5px solid #2d3436; + background-color: #fff; + padding: 20px; +`; diff --git a/packages/presentify/src/types/types.ts b/packages/presentify/src/types/types.ts index 218046b..75f132f 100644 --- a/packages/presentify/src/types/types.ts +++ b/packages/presentify/src/types/types.ts @@ -5,14 +5,17 @@ export enum Keys { Left = 'ArrowLeft', Up = 'ArrowUp', Down = 'ArrowDown', + P = 'KeyP', } export interface PresentifyContextProps { slides: ReactNode[][]; currentSlide: number; options?: Options; + presenterMode: boolean; onGoNextSlide: () => void; onGoBackSlide: () => void; + togglePresenterMode: () => void; } export interface Options { @@ -22,4 +25,5 @@ export interface Options { backgroundColor?: string; backgroundImg?: string; layout?: string; + disablePresenterMode?: boolean; } diff --git a/website/docs/api-reference/presentify-provider.md b/website/docs/api-reference/presentify-provider.md index 7b6cfa2..c8e394c 100644 --- a/website/docs/api-reference/presentify-provider.md +++ b/website/docs/api-reference/presentify-provider.md @@ -10,20 +10,22 @@ Moreover, it has keyboard shortcuts for manipulating the presentation. It also a ## PresentifyProviders Options -| Option | Type | Optional | Default | Description | -| :---------------: | :-------------: | :----------------: | :-----: | :-----------------------------------------------------------: | -| `theme` | dark / light | :heavy_check_mark: | light | code syntax highlighter theme (dark = vsDark, light = github) | -| `useFiraCode` | boolean | :heavy_check_mark: | false | use firaCode font with ligatures in Code syntax highlighter | -| `className` | string | :heavy_check_mark: | none | set className to slide parent div | -| `backgroundColor` | string | :heavy_check_mark: | white | set background color to slide | -| `backgroundImg` | string | :heavy_check_mark: | none | set background Image to slide | -| `layout` | normal / center | :heavy_check_mark: | center | set layout of slide | +| Option | Type | Optional | Default | Description | +| :--------------------: | :-------------: | :----------------: | :-----: | :-----------------------------------------------------------: | +| `theme` | dark / light | :heavy_check_mark: | light | code syntax highlighter theme (dark = vsDark, light = github) | +| `useFiraCode` | boolean | :heavy_check_mark: | false | use firaCode font with ligatures in Code syntax highlighter | +| `className` | string | :heavy_check_mark: | none | set className to slide parent div | +| `backgroundColor` | string | :heavy_check_mark: | white | set background color to slide | +| `backgroundImg` | string | :heavy_check_mark: | none | set background Image to slide | +| `layout` | normal / center | :heavy_check_mark: | center | set layout of slide | +| `disablePresenterMode` | boolean | :heavy_check_mark: | false | disable option to open presentation in presenter mode | ## Keyboard Shortcuts -| Key | Description | -| :-----------: | :--------------------------: | -| `Arrow up` | change to the next slide | -| `Arrow Down` | change to the previous slide | -| `Arrow left` | change to the next slide | -| `Arrow right` | change to the previous slide | +| Key | Description | +| :--------------: | :---------------------------: | +| `Arrow up` | change to the next slide | +| `Arrow Down` | change to the previous slide | +| `Arrow left` | change to the next slide | +| `Arrow right` | change to the previous slide | +| `option/alt + p` | enable/disable presenter mode | diff --git a/website/docs/getting-started/roadmap.md b/website/docs/getting-started/roadmap.md index 2b3e2cb..a285dc6 100644 --- a/website/docs/getting-started/roadmap.md +++ b/website/docs/getting-started/roadmap.md @@ -7,6 +7,6 @@ title: Roadmap - Add animation to slide and code Highlighter - Add a new theme to code Highlighter -- Add Diagrams component using `mermaid` -- Add Presenter mode +- Add Diagrams component using `mermaid` [v1.1.0](https://github.com/ErnestTeluk/presentify/releases/tag/v1.1.0) +- Add Presenter mode [v1.2.0](https://github.com/ErnestTeluk/presentify/releases/tag/v1.2.0) - Add support to importing files to code highlighter