Skip to content

Commit eaefcb5

Browse files
author
Carlos Paelinck
authored
Added out-of-order slide navigation. (#390)
1 parent 3e66139 commit eaefcb5

File tree

5 files changed

+151
-12
lines changed

5 files changed

+151
-12
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,7 @@ The slide tag represents each slide in the presentation. Giving a slide tag an `
342342
|Name|PropType|Description|
343343
|---|---|---|
344344
|align| PropTypes.string | Accepts a space delimited value for positioning interior content. The first value can be `flex-start` (left), `center` (middle), or `flex-end` (right). The second value can be `flex-start` (top) , `center` (middle), or `flex-end` (bottom). You would provide this prop like `align="center center"`, which is its default.
345+
|goTo| PropTypes.number | Used to navigate to a slide for out-of-order presenting. Slide numbers start at `1`. This can also be used to skip slides as well.
345346
|id| PropTypes.string | Used to create a string based hash.
346347
|maxHeight| PropTypes.number | Used to set max dimensions of the Slide.
347348
|maxWidth| PropTypes.number | Used to set max dimensions of the Slide.

example/src/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export default class Presentation extends React.Component {
5959
</Slide>
6060
<Slide
6161
id="wait-what"
62+
goTo={4}
6263
transition={[
6364
'fade',
6465
(transitioning, forward) => {
@@ -93,7 +94,7 @@ export default class Presentation extends React.Component {
9394
overflow = "overflow"
9495
/>
9596
</Slide>
96-
<Slide>
97+
<Slide goTo={3}>
9798
<ComponentPlayground
9899
theme="dark"
99100
/>

src/components/__snapshots__/manager.test.js.snap

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ exports[`<Manager /> should render correctly. 1`] = `
1717
contentWidth={1000}
1818
controls={true}
1919
dispatch={[Function]}
20+
fragment={
21+
Object {
22+
"fragments": Array [],
23+
}
24+
}
2025
globalStyles={true}
2126
progress="pacman"
2227
route={
@@ -101,7 +106,11 @@ exports[`<Manager /> should render correctly. 1`] = `
101106
<MockSlide
102107
dispatch={[Function]}
103108
export={false}
104-
fragments={undefined}
109+
fragments={
110+
Object {
111+
"fragments": Array [],
112+
}
113+
}
105114
hash={0}
106115
key=".$0"
107116
lastSlideIndex={0}
@@ -273,6 +282,11 @@ exports[`<Manager /> should render the export configuration when specified. 1`]
273282
contentWidth={1000}
274283
controls={true}
275284
dispatch={[Function]}
285+
fragment={
286+
Object {
287+
"fragments": Array [],
288+
}
289+
}
276290
globalStyles={true}
277291
progress="pacman"
278292
route={
@@ -396,6 +410,11 @@ exports[`<Manager /> should render the overview configuration when specified. 1`
396410
contentWidth={1000}
397411
controls={true}
398412
dispatch={[Function]}
413+
fragment={
414+
Object {
415+
"fragments": Array [],
416+
}
417+
}
399418
globalStyles={true}
400419
progress="pacman"
401420
route={
@@ -567,6 +586,11 @@ exports[`<Manager /> should render with slideset slides 1`] = `
567586
contentWidth={1000}
568587
controls={true}
569588
dispatch={[Function]}
589+
fragment={
590+
Object {
591+
"fragments": Array [],
592+
}
593+
}
570594
globalStyles={true}
571595
progress="pacman"
572596
route={
@@ -664,7 +688,11 @@ exports[`<Manager /> should render with slideset slides 1`] = `
664688
<MockSlide
665689
dispatch={[Function]}
666690
export={false}
667-
fragments={undefined}
691+
fragments={
692+
Object {
693+
"fragments": Array [],
694+
}
695+
}
668696
hash={1}
669697
key=".$1"
670698
lastSlideIndex={1}

src/components/manager.js

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ export class Manager extends Component {
111111
slideReference: [],
112112
fullscreen: window.innerHeight === screen.height,
113113
mobile: window.innerWidth < props.contentWidth,
114-
autoplaying: props.autoplay,
114+
autoplaying: props.autoplay
115115
};
116116
}
117117

@@ -148,6 +148,9 @@ export class Manager extends Component {
148148
componentWillUnmount() {
149149
this._detachEvents();
150150
}
151+
152+
viewedIndexes = new Set();
153+
151154
_attachEvents() {
152155
window.addEventListener('storage', this._goToSlide);
153156
window.addEventListener('keydown', this._handleKeyPress);
@@ -286,6 +289,7 @@ export class Manager extends Component {
286289
this.setState({
287290
lastSlideIndex: slideIndex,
288291
});
292+
this.viewedIndexes.delete(slideIndex);
289293
if (
290294
this._checkFragments(this.props.route.slide, false) ||
291295
this.props.route.params.indexOf('overview') !== -1
@@ -314,6 +318,27 @@ export class Manager extends Component {
314318
);
315319
}
316320
}
321+
_nextUnviewedIndex() {
322+
const sortedIndexes = Array.from(this.viewedIndexes).sort((a, b) => a - b);
323+
return Math.min(
324+
(sortedIndexes[sortedIndexes.length - 1] || 0) + 1,
325+
this.state.slideReference.length - 1
326+
);
327+
}
328+
_getOffset(slideIndex) {
329+
const { goTo } = this.state.slideReference[slideIndex];
330+
const nextUnviewedIndex = this._nextUnviewedIndex();
331+
if (goTo && !isNaN(parseInt(goTo))) {
332+
const goToIndex = () => {
333+
if (this.viewedIndexes.has(goTo - 1)) {
334+
return this._nextUnviewedIndex();
335+
}
336+
return goTo - 1;
337+
};
338+
return goToIndex() - slideIndex;
339+
}
340+
return nextUnviewedIndex - slideIndex;
341+
}
317342
_nextSlide() {
318343
const slideIndex = this._getSlideIndex();
319344
this.setState({
@@ -331,13 +356,15 @@ export class Manager extends Component {
331356
this._goToSlide({ key: 'spectacle-slide', newValue: slideData });
332357
}
333358
} else if (slideIndex < slideReference.length - 1) {
359+
this.viewedIndexes.add(slideIndex);
360+
const offset = this._getOffset(slideIndex);
334361
this.context.history.replace(
335-
`/${this._getHash(slideIndex + 1) + this._getSuffix()}`
362+
`/${this._getHash(slideIndex + offset) + this._getSuffix()}`
336363
);
337364
localStorage.setItem(
338365
'spectacle-slide',
339366
JSON.stringify({
340-
slide: this._getHash(slideIndex + 1),
367+
slide: this._getHash(slideIndex + offset),
341368
forward: true,
342369
time: Date.now(),
343370
})
@@ -492,17 +519,25 @@ export class Manager extends Component {
492519
const slideReference = [];
493520
Children.toArray(this.props.children).forEach((child, rootIndex) => {
494521
if (!child.props.hasSlideChildren) {
495-
slideReference.push({
522+
const reference = {
496523
id: child.props.id || slideReference.length,
497-
rootIndex,
498-
});
524+
rootIndex
525+
};
526+
if (child.props.goTo) {
527+
reference.goTo = child.props.goTo;
528+
}
529+
slideReference.push(reference);
499530
} else {
500531
child.props.children.forEach((setSlide, setIndex) => {
501-
slideReference.push({
532+
const reference = {
502533
id: setSlide.props.id || slideReference.length,
503534
setIndex,
504535
rootIndex,
505-
});
536+
};
537+
if (child.props.goTo) {
538+
reference.goTo = child.props.goTo;
539+
}
540+
slideReference.push(reference);
506541
});
507542
}
508543
});
@@ -671,4 +706,4 @@ export class Manager extends Component {
671706
}
672707
}
673708

674-
export default connect(state => state)(Manager);
709+
export default connect(state => state, null, null, { withRef: true })(Manager);

src/components/manager.test.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React, { Component } from 'react';
22
import PropTypes from 'prop-types';
33
import { mount } from 'enzyme';
44
import Manager from './manager';
5+
import range from 'lodash/range';
56

67
const _mockContext = function(slide, routeParams) {
78
return {
@@ -23,6 +24,9 @@ const _mockContext = function(slide, routeParams) {
2324
style: {
2425
globalStyleSet: [],
2526
},
27+
fragment: {
28+
fragments: []
29+
}
2630
}),
2731
dispatch: () => {},
2832
subscribe: () => {},
@@ -54,6 +58,12 @@ const _mockChildContext = function() {
5458
};
5559

5660
describe('<Manager />', () => {
61+
beforeEach(() => {
62+
window.localStorage = { setItem: () => {} };
63+
});
64+
afterEach(() => {
65+
window.localStorage = undefined;
66+
});
5767
test('should render correctly.', () => {
5868
const wrapper = mount(
5969
<Manager transition={['zoom', 'slide']} transitionDuration={500}>
@@ -106,4 +116,68 @@ describe('<Manager />', () => {
106116
);
107117
expect(wrapper).toMatchSnapshot();
108118
});
119+
120+
test('should get the next index when using out-of-order viewing', () => {
121+
const wrapper = mount(
122+
<Manager>
123+
{range(0, 10).map(value => <MockSlide key={value} />)}
124+
</Manager>,
125+
{ context: _mockContext(5, []), childContextTypes: _mockChildContext() }
126+
);
127+
const managerInstance = wrapper.instance().getWrappedInstance();
128+
managerInstance.viewedIndexes = new Set([0, 1, 2, 5, 4, 3]);
129+
// The next unviwed index should sort the set and figure out the next
130+
// best slide to go to, since 0 through 5 have been visited, 6 is the best.
131+
expect(managerInstance._nextUnviewedIndex()).toEqual(6);
132+
});
133+
134+
test('should not exceed the maximum number of slides for next index', () => {
135+
const wrapper = mount(
136+
<Manager>
137+
{range(0, 11).map(value => <MockSlide key={value} />)}
138+
</Manager>,
139+
{ context: _mockContext(10, []), childContextTypes: _mockChildContext() }
140+
);
141+
const managerInstance = wrapper.instance().getWrappedInstance();
142+
managerInstance.viewedIndexes = new Set([0, 1, 2, 5, 4, 3, 6, 9, 10, 7, 8]);
143+
// Even though we are on index 10, index 10 is still the next best index
144+
// because there are no more slides in the deck.
145+
expect(managerInstance._nextUnviewedIndex()).toEqual(10);
146+
});
147+
148+
test('should calc a negative offset when routing from a higher index slide to lower', () => {
149+
const wrapper = mount(
150+
<Manager>
151+
<MockSlide />
152+
<MockSlide goTo={4} />
153+
<MockSlide />
154+
<MockSlide goTo={3} />
155+
<MockSlide />
156+
</Manager>,
157+
{ context: _mockContext(3, []), childContextTypes: _mockChildContext() }
158+
);
159+
const managerInstance = wrapper.instance().getWrappedInstance();
160+
managerInstance.viewedIndexes = new Set([0, 1, 3]);
161+
// We are at slide 4 (index 3) which directs us to go to
162+
// slide 3 (index 2) the delta should be 2 - 3, thus -1.
163+
expect(managerInstance._getOffset(3)).toEqual(-1);
164+
});
165+
166+
test('should calc a positive offset when routing from a lower index slide to higher', () => {
167+
const wrapper = mount(
168+
<Manager>
169+
<MockSlide />
170+
<MockSlide goTo={4} />
171+
<MockSlide />
172+
<MockSlide goTo={3} />
173+
<MockSlide />
174+
</Manager>,
175+
{ context: _mockContext(1, []), childContextTypes: _mockChildContext() }
176+
);
177+
const managerInstance = wrapper.instance().getWrappedInstance();
178+
managerInstance.viewedIndexes = new Set([0, 1]);
179+
// We are at slide 2 (index 1) which directs us to go to
180+
// slide 4 (index 3) the delta should be 3 - 1, thus 2.
181+
expect(managerInstance._getOffset(1)).toEqual(2);
182+
});
109183
});

0 commit comments

Comments
 (0)