Skip to content

Commit 067bb9f

Browse files
URL-based navigation for slides. (#711)
* URL-based navigation for slides. * Added support for presenterMode * Add back in support for looping. * Fixed missing hooks dep
1 parent 2066806 commit 067bb9f

File tree

5 files changed

+187
-59
lines changed

5 files changed

+187
-59
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,10 @@
6969
"@mdx-js/react": "^1.1.5",
7070
"@mdx-js/tag": "^0.20.3",
7171
"gray-matter": "^4.0.2",
72+
"history": "^4.9.0",
7273
"loader-utils": "^1.2.3",
7374
"normalize-newline": "^3.0.0",
75+
"query-string": "^6.8.2",
7476
"react-spring": "^8.0.25",
7577
"styled-components": "^4.3.1",
7678
"wonka": "^3.2.0"

src/components/deck.js

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
TransitionPipeContext,
99
TransitionPipeProvider
1010
} from '../hooks/use-transition-pipe';
11+
import useUrlRouting from '../hooks/use-url-routing';
1112

1213
/**
1314
* Provides top level state/context provider with useDeck hook
@@ -27,7 +28,8 @@ const initialState = {
2728
immediate: false,
2829
immediateElement: false,
2930
currentSlideElement: 0,
30-
reverseDirection: false
31+
reverseDirection: false,
32+
presenterMode: false
3133
};
3234

3335
const defaultSlideEffect = {
@@ -80,10 +82,33 @@ const Deck = ({ children, loop, keyboardControls, ...rest }) => {
8082
rest.animationsWhenGoingBack,
8183
slideElementMap
8284
);
85+
86+
const {
87+
navigateToNextSlide,
88+
navigateToPreviousSlide,
89+
navigateToCurrentUrl
90+
} = useUrlRouting({
91+
dispatch,
92+
currentSlide: state.currentSlide,
93+
currentSlideElement: state.currentSlideElement,
94+
presenterMode: state.presenterMode,
95+
slideElementMap,
96+
loop
97+
});
98+
8399
const userTransitionEffect =
84100
filteredChildren[state.currentSlide].props.transitionEffect || {};
85101
const transitionRef = React.useRef(null);
86102

103+
React.useEffect(() => {
104+
/***
105+
* This will look at the current query string and navigate to whatever
106+
* slide is specified, otherwise start at 0. This only runs once per mount
107+
* of Deck, which should be the entire lifecyle of the slideshow.
108+
*/
109+
navigateToCurrentUrl();
110+
}, [navigateToCurrentUrl]);
111+
87112
React.useEffect(() => {
88113
if (!transitionRef.current) {
89114
return;
@@ -125,7 +150,9 @@ const Deck = ({ children, loop, keyboardControls, ...rest }) => {
125150
numberOfSlides: slides.length,
126151
keyboardControls,
127152
animationsWhenGoingBack: rest.animationsWhenGoingBack,
128-
slideElementMap
153+
slideElementMap,
154+
navigateToNextSlide,
155+
navigateToPreviousSlide
129156
}}
130157
>
131158
{slides}

src/hooks/use-deck.js

Lines changed: 36 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -17,53 +17,36 @@ function useDeck(
1717
function reducer(state, action) {
1818
switch (action.type) {
1919
case 'NEXT_SLIDE':
20+
return {
21+
...state,
22+
currentSlide: state.currentSlide + 1,
23+
currentSlideElement: 0,
24+
immediate: false,
25+
immediateElement: false,
26+
reverseDirection: false
27+
};
2028
case 'NEXT_SLIDE_IMMEDIATE':
21-
// If next slide is going to be greater than number
22-
// of slides, if it is looping then go to initial state
23-
// if not then stop
24-
if (state.currentSlide + 1 === numSlides) {
25-
if (looping) {
26-
return action.type === 'NEXT_SLIDE_IMMEDIATE'
27-
? {
28-
...initialState,
29-
immediate: true,
30-
reverseDirection: false,
31-
immediateElement: false
32-
}
33-
: initialState;
34-
}
35-
return { ...state };
36-
}
37-
return action.type === 'NEXT_SLIDE_IMMEDIATE'
38-
? {
39-
currentSlide: state.currentSlide + 1,
40-
immediate: true,
41-
currentSlideElement: 0,
42-
immediateElement: false,
43-
reverseDirection: false
44-
}
45-
: {
46-
currentSlide: state.currentSlide + 1,
47-
currentSlideElement: 0,
48-
immediate: false,
49-
immediateElement: false,
50-
reverseDirection: false
51-
};
29+
return {
30+
...state,
31+
currentSlide: state.currentSlide + 1,
32+
immediate: true,
33+
currentSlideElement: 0,
34+
immediateElement: false,
35+
reverseDirection: false
36+
};
37+
case 'GO_TO_SLIDE': {
38+
return {
39+
...state,
40+
currentSlideElement: 0,
41+
currentSlide: action.payload.slideNumber,
42+
immediate: action.payload.immediate,
43+
immediateElement: false,
44+
reverseDirection: action.payload.reverseDirection
45+
};
46+
}
5247
case 'PREV_SLIDE':
53-
// If current slide is initial slide then if looping go
54-
// to last slide else stop
55-
if (state.currentSlide === initialState.currentSlide) {
56-
if (looping) {
57-
return {
58-
currentSlide: numSlides - 1,
59-
currentSlideElement: Math.max(slideElementMap[numSlides - 1], 0),
60-
immediate: !!animationsWhenGoingBack,
61-
reverseDirection: true
62-
};
63-
}
64-
return { ...state };
65-
}
6648
return {
49+
...state,
6750
currentSlide: state.currentSlide - 1,
6851
currentSlideElement: Math.max(
6952
slideElementMap[state.currentSlide - 1],
@@ -73,37 +56,39 @@ function useDeck(
7356
immediateElement: true,
7457
reverseDirection: true
7558
};
76-
case 'NEXT_SLIDE_ELEMENT': {
59+
case 'NEXT_SLIDE_ELEMENT':
7760
return {
7861
...state,
7962
currentSlideElement: state.currentSlideElement + 1,
8063
immediateElement: false,
8164
reverseDirection: false
8265
};
83-
}
84-
case 'NEXT_SLIDE_ELEMENT_IMMEDIATE': {
66+
case 'NEXT_SLIDE_ELEMENT_IMMEDIATE':
8567
return {
8668
...state,
8769
currentSlideElement: state.currentSlideElement + 1,
8870
immediateElement: true,
8971
reverseDirection: false
9072
};
91-
}
92-
case 'PREV_SLIDE_ELEMENT': {
73+
case 'PREV_SLIDE_ELEMENT':
9374
return {
9475
...state,
9576
currentSlideElement: Math.max(state.currentSlideElement - 1, 0),
9677
immediateElement: false,
9778
reverseDirection: true
9879
};
99-
}
100-
case 'PREV_SLIDE_ELEMENT_IMMEDIATE': {
80+
case 'PREV_SLIDE_ELEMENT_IMMEDIATE':
10181
return {
10282
...state,
10383
currentSlideElement: Math.max(state.currentSlideElement - 1, 0),
10484
immediateElement: true,
10585
reverseDirection: true
10686
};
87+
case 'SET_PRESENTER_MODE': {
88+
return {
89+
...state,
90+
presenterMode: action.payload.presenterMode
91+
};
10792
}
10893
default:
10994
return { ...state };

src/hooks/use-slide.js

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ function useSlide(
2727
const {
2828
state: deckContextState,
2929
dispatch: deckContextDispatch,
30-
animationsWhenGoingBack
30+
animationsWhenGoingBack,
31+
navigateToNextSlide,
32+
navigateToPreviousSlide
3133
} = React.useContext(DeckContext);
3234

3335
const isActiveSlide = deckContextState.currentSlide === slideNum;
@@ -37,13 +39,14 @@ function useSlide(
3739
slideElementsLength === 0 ||
3840
deckContextState.currentSlideElement === slideElementsLength
3941
) {
40-
deckContextDispatch({ type: 'NEXT_SLIDE' });
42+
navigateToNextSlide();
4143
} else {
4244
deckContextDispatch({ type: 'NEXT_SLIDE_ELEMENT' });
4345
}
4446
}, [
4547
deckContextDispatch,
4648
deckContextState.currentSlideElement,
49+
navigateToNextSlide,
4750
slideElementsLength
4851
]);
4952

@@ -52,23 +55,33 @@ function useSlide(
5255
slideElementsLength === 0 ||
5356
deckContextState.currentSlideElement === slideElementsLength
5457
) {
55-
deckContextDispatch({ type: 'NEXT_SLIDE_IMMEDIATE' });
58+
navigateToNextSlide({ immediate: true });
5659
} else {
5760
deckContextDispatch({ type: 'NEXT_SLIDE_ELEMENT_IMMEDIATE' });
5861
}
59-
}, [deckContextDispatch, deckContextState, slideElementsLength]);
62+
}, [
63+
deckContextDispatch,
64+
deckContextState.currentSlideElement,
65+
navigateToNextSlide,
66+
slideElementsLength
67+
]);
6068

6169
const goToPreviousSlideElement = React.useCallback(() => {
6270
if (deckContextState.currentSlideElement === 0) {
63-
deckContextDispatch({ type: 'PREV_SLIDE' });
71+
navigateToPreviousSlide();
6472
} else {
6573
if (!animationsWhenGoingBack) {
6674
deckContextDispatch({ type: 'PREV_SLIDE_ELEMENT_IMMEDIATE' });
6775
return;
6876
}
6977
deckContextDispatch({ type: 'PREV_SLIDE_ELEMENT' });
7078
}
71-
}, [animationsWhenGoingBack, deckContextDispatch, deckContextState]);
79+
}, [
80+
animationsWhenGoingBack,
81+
deckContextDispatch,
82+
deckContextState.currentSlideElement,
83+
navigateToPreviousSlide
84+
]);
7285

7386
const keyPressCount = React.useRef(0);
7487

src/hooks/use-url-routing.js

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import * as React from 'react';
2+
import { createBrowserHistory } from 'history';
3+
import * as queryString from 'query-string';
4+
5+
export default function useUrlRouting(options) {
6+
const {
7+
dispatch,
8+
slideElementMap,
9+
currentSlide,
10+
presenterMode,
11+
loop
12+
} = options;
13+
const history = React.useRef(createBrowserHistory());
14+
const numberOfSlides = React.useMemo(
15+
() => Object.getOwnPropertyNames(slideElementMap).length,
16+
[slideElementMap]
17+
);
18+
19+
const onHistoryChange = React.useCallback(() => {
20+
const query = queryString.parse(window.location.search);
21+
const proposedSlideNumber = parseInt(query.slide, 10);
22+
const presenterMode = Boolean(query.presenterMode);
23+
24+
/**
25+
* If the proposed URL slide index is out-of-bounds or is not a valid
26+
* integer, navigate to the first slide. Do nothing if the proposed slide
27+
* number is the same as the current slide.
28+
*/
29+
if (
30+
isNaN(proposedSlideNumber) ||
31+
numberOfSlides - 1 < proposedSlideNumber
32+
) {
33+
const qs = queryString.stringify({ slide: 0 });
34+
history.current.replace(`?${qs}`);
35+
return;
36+
}
37+
if (proposedSlideNumber === currentSlide) {
38+
dispatch({ type: 'SET_PRESENTER_MODE', payload: { presenterMode } });
39+
return;
40+
}
41+
const reverseDirection = proposedSlideNumber < currentSlide;
42+
dispatch({
43+
type: 'GO_TO_SLIDE',
44+
payload: {
45+
slideNumber: proposedSlideNumber,
46+
reverseDirection,
47+
immediate: Boolean(query.immediate),
48+
presenterMode
49+
}
50+
});
51+
}, [dispatch, numberOfSlides, currentSlide]);
52+
53+
const navigateToNextSlide = React.useCallback(
54+
({ immediate } = {}) => {
55+
let nextSafeSlideIndex;
56+
if (currentSlide + 1 > numberOfSlides - 1 && loop) {
57+
nextSafeSlideIndex = 0;
58+
} else {
59+
nextSafeSlideIndex = Math.min(currentSlide + 1, numberOfSlides - 1);
60+
}
61+
const qs = queryString.stringify({
62+
slide: nextSafeSlideIndex,
63+
immediate: immediate || undefined,
64+
presenterMode: presenterMode || undefined
65+
});
66+
history.current.push(`?${qs}`);
67+
},
68+
[currentSlide, loop, numberOfSlides, presenterMode]
69+
);
70+
71+
const navigateToPreviousSlide = React.useCallback(
72+
({ immediate } = {}) => {
73+
let previousSafeSlideIndex;
74+
if (currentSlide - 1 < 0 && loop) {
75+
previousSafeSlideIndex = numberOfSlides - 1;
76+
} else {
77+
previousSafeSlideIndex = Math.max(0, currentSlide - 1);
78+
}
79+
const qs = queryString.stringify({
80+
slide: previousSafeSlideIndex,
81+
immediate: immediate || undefined,
82+
presenterMode: presenterMode || undefined
83+
});
84+
history.current.push(`?${qs}`);
85+
},
86+
[currentSlide, loop, numberOfSlides, presenterMode]
87+
);
88+
89+
React.useEffect(() => {
90+
const removeHistoryListener = history.current.listen(onHistoryChange);
91+
return () => {
92+
removeHistoryListener();
93+
};
94+
}, [onHistoryChange]);
95+
96+
return {
97+
navigateToNextSlide,
98+
navigateToPreviousSlide,
99+
navigateToCurrentUrl: onHistoryChange
100+
};
101+
}

0 commit comments

Comments
 (0)