Skip to content

Commit 7ed3df9

Browse files
committed
Merge pull request #23 from FormidableLabs/presenter
Presenter mode
2 parents 8fc3359 + 899ce47 commit 7ed3df9

File tree

9 files changed

+302
-34
lines changed

9 files changed

+302
-34
lines changed

presentation/config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
module.exports = {
22
width: 1000,
3+
height: 700,
34
margin: 40,
45
theme: require('../themes/default/index'),
56
print: require('../themes/default/print'),

src/deck.jsx

Lines changed: 81 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,70 +4,120 @@ import cloneWithProps from 'react/lib/cloneWithProps';
44
import Radium from 'radium';
55
import _ from 'lodash';
66

7+
import Presenter from './presenter';
8+
79
React.initializeTouchEvents(true);
810

911
const Style = Radium.Style;
1012

11-
const TransitionGroup = React.addons.TransitionGroup;
13+
const TransitionGroup = Radium(React.addons.TransitionGroup);
1214

1315
@Radium
1416
class Deck extends React.Component {
1517
constructor(props) {
1618
super(props);
1719
this._handleKeyPress = this._handleKeyPress.bind(this);
1820
this._handleClick = this._handleClick.bind(this);
21+
this._goToSlide = this._goToSlide.bind(this);
1922
this.state = {
2023
lastSlide: null
2124
};
2225
}
2326
componentDidMount() {
27+
let slide = 'slide' in this.context.router.state.params ?
28+
parseInt(this.context.router.state.params.slide) : 0;
2429
this.setState({
25-
lastSlide: 'slide' in this.context.router.state.params ?
26-
parseInt(this.context.router.state.params.slide) : 0
30+
lastSlide: slide
2731
});
32+
localStorage.setItem('spectacle-slide',
33+
JSON.stringify({slide: slide, forward: false, time: Date.now()}));
2834
this._attachEvents();
2935
}
3036
componentWillUnmount() {
3137
this._detachEvents();
3238
}
3339
_attachEvents() {
40+
window.addEventListener('storage', this._goToSlide);
3441
window.addEventListener('keydown', this._handleKeyPress);
3542
}
3643
_detachEvents() {
44+
window.removeEventListener('storage', this._goToSlide);
3745
window.removeEventListener('keydown', this._handleKeyPress);
3846
}
3947
_handleKeyPress(e) {
4048
let event = window.event ? window.event : e;
4149
event.keyCode === 37 && this._prevSlide();
4250
event.keyCode === 39 && this._nextSlide();
4351
}
52+
_goToSlide(e) {
53+
if(e.key === 'spectacle-slide') {
54+
let data = JSON.parse(e.newValue);
55+
let presenter = this.context.presenter ? '?presenter' : '';
56+
let slide = 'slide' in this.context.router.state.params ?
57+
parseInt(this.context.router.state.params.slide) : 0;
58+
this.setState({
59+
lastSlide: slide || 0
60+
});
61+
if(this._checkFragments(slide, data.forward)) {
62+
this.context.router.replaceWith('/' + (data.slide) + presenter);
63+
}
64+
}
65+
}
4466
_prevSlide() {
4567
let slide = 'slide' in this.context.router.state.params ?
4668
parseInt(this.context.router.state.params.slide) : 0;
69+
let presenter = this.context.presenter ? '?presenter' : '';
4770
this.setState({
4871
lastSlide: slide
4972
});
5073
if (this._checkFragments(slide, false)) {
5174
if (slide > 0) {
52-
this.context.router.replaceWith('/' + (slide - 1));
75+
this.context.router.replaceWith('/' + (slide - 1) + presenter);
76+
localStorage.setItem('spectacle-slide',
77+
JSON.stringify({slide: slide - 1, forward: false, time: Date.now()}));
78+
}
79+
} else {
80+
if (slide > 0) {
81+
localStorage.setItem('spectacle-slide',
82+
JSON.stringify({slide: slide, forward: false, time: Date.now()}));
5383
}
5484
}
5585
}
5686
_nextSlide() {
5787
let slide = 'slide' in this.context.router.state.params ?
5888
parseInt(this.context.router.state.params.slide) : 0;
89+
let presenter = this.context.presenter ? '?presenter' : '';
5990
this.setState({
6091
lastSlide: slide
6192
});
6293
if(this._checkFragments(slide, true)) {
6394
if (slide < this.props.children.length - 1) {
64-
this.context.router.replaceWith('/' + (slide + 1));
95+
this.context.router.replaceWith('/' + (slide + 1) + presenter);
96+
localStorage.setItem('spectacle-slide',
97+
JSON.stringify({slide: slide + 1, forward: true, time: Date.now()}));
98+
}
99+
} else {
100+
if (slide < this.props.children.length - 1) {
101+
localStorage.setItem('spectacle-slide',
102+
JSON.stringify({slide: slide, forward: true, time: Date.now()}));
65103
}
66104
}
67105
}
68106
_checkFragments(slide, forward) {
69107
let store = this.context.flux.stores.SlideStore;
70108
let fragments = store.getState().fragments;
109+
// Not proud of this at all. 0.14 Parent based contexts will fix this.
110+
if(this.context.presenter) {
111+
let main = document.querySelector('.spectacle-presenter-main');
112+
if (main) {
113+
let fragments = main.querySelectorAll('.appear');
114+
if (!fragments.length) {
115+
return true;
116+
}
117+
} else {
118+
return true;
119+
}
120+
}
71121
if (slide in fragments) {
72122
let count = _.size(fragments[slide]);
73123
let visible = _.filter(fragments[slide], function(s){
@@ -223,6 +273,9 @@ class Deck extends React.Component {
223273
exportMode = true;
224274
}
225275

276+
let slide ='slide' in this.context.router.state.params ?
277+
parseInt(this.context.router.state.params.slide) : 0;
278+
226279
let globals = exportMode ? {
227280
body: {
228281
minWidth: 1100,
@@ -232,24 +285,34 @@ class Deck extends React.Component {
232285
} : {};
233286

234287
let styles = {
235-
position: 'absolute',
236-
top: 0,
237-
left: 0,
238-
width: '100%',
239-
height: '100%',
240-
perspective: 1000,
241-
transformStyle: 'preserve-3d'
288+
deck: {
289+
backgroundColor: this.context.presenter ? 'black' : '',
290+
position: 'absolute',
291+
top: 0,
292+
left: 0,
293+
width: '100%',
294+
height: '100%',
295+
perspective: 1000,
296+
transformStyle: 'preserve-3d'
297+
},
298+
transition: {
299+
height: '100%',
300+
width: '100%'
301+
}
242302
};
243303

244304
return (
245305
<div
246306
className="spectacle-deck"
247-
style={[styles]}
307+
style={[styles.deck]}
248308
onClick={this._handleClick}
249309
{...this._getTouchEvents()}>
250-
<TransitionGroup component="div" style={{height: '100%'}}>
251-
{this._renderSlide()}
252-
</TransitionGroup>
310+
{this.context.presenter ?
311+
<Presenter slides={this.props.children}
312+
slide={slide} lastSlide={this.state.lastSlide}/> :
313+
<TransitionGroup component="div" style={[styles.transition]}>
314+
{this._renderSlide()}
315+
</TransitionGroup>}
253316
<Style rules={assign(this.context.styles.global, globals)} />
254317
</div>
255318
)
@@ -265,7 +328,8 @@ Deck.defaultProps = {
265328
Deck.contextTypes = {
266329
styles: React.PropTypes.object,
267330
router: React.PropTypes.object,
268-
flux: React.PropTypes.object
331+
flux: React.PropTypes.object,
332+
presenter: React.PropTypes.bool
269333
};
270334

271335
export default Deck;

src/presenter.jsx

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import React from 'react/addons';
2+
import assign from 'object-assign';
3+
import cloneWithProps from 'react/lib/cloneWithProps';
4+
import Base from './base';
5+
import Radium from 'radium';
6+
7+
function startTime(date) {
8+
var hours = date.getHours();
9+
var minutes = date.getMinutes();
10+
var seconds = date.getSeconds();
11+
var ampm = hours >= 12 ? 'PM' : 'AM';
12+
hours = hours % 12;
13+
hours = hours ? hours : 12;
14+
minutes = minutes < 10 ? '0'+minutes : minutes;
15+
seconds = seconds < 10 ? '0'+seconds : seconds;
16+
var strTime = hours + ':' + minutes + ':' + seconds + ' ' + ampm;
17+
return strTime;
18+
}
19+
20+
@Radium
21+
class Presenter extends Base {
22+
constructor(props) {
23+
super(props);
24+
this.state = {
25+
time: startTime(new Date())
26+
}
27+
}
28+
_renderMainSlide() {
29+
let child = this.props.slides[this.props.slide];
30+
let presenterStyle = {
31+
position: 'relative'
32+
}
33+
return cloneWithProps(child, {
34+
key: this.props.slide,
35+
slideIndex: this.props.slide,
36+
lastSlide: this.props.lastSlide,
37+
transition: [],
38+
transitionDuration: 0,
39+
presenterStyle: presenterStyle
40+
});
41+
}
42+
componentDidMount() {
43+
setInterval(()=> {
44+
this.setState({
45+
time: startTime(new Date())
46+
});
47+
}, 1000);
48+
}
49+
_renderNextSlide() {
50+
let presenterStyle = {
51+
position: 'relative'
52+
}
53+
let endStyle = {
54+
position: 'absolute',
55+
top: '50%',
56+
left: '50%',
57+
transform: 'translate(-50%, -50%)',
58+
margin: 0
59+
}
60+
let child = this.props.slides[parseInt(this.props.slide) + 1];
61+
return child ? cloneWithProps(child, {
62+
key: this.props.slide + 1,
63+
slideIndex: this.props.slide + 1,
64+
lastSlide: this.props.lastSlide,
65+
transition: [],
66+
transitionDuration: 0,
67+
presenterStyle: presenterStyle,
68+
appearOff: true
69+
}) : <h1 style={[endStyle]}>END</h1>
70+
}
71+
render() {
72+
let styles = {
73+
presenter: {
74+
height: '100%',
75+
width: '100%',
76+
display: 'flex',
77+
flex: 1,
78+
flexDirection: 'column'
79+
},
80+
header: {
81+
position: 'absolute',
82+
display: 'block',
83+
color: 'white',
84+
width: '100%',
85+
height: '20%',
86+
textAlign: 'center',
87+
padding: '20px 50px',
88+
},
89+
slideInfo: {
90+
position: 'relative',
91+
top: '50%',
92+
transform: 'translateY(-50%)',
93+
float: 'left',
94+
margin: 0,
95+
lineHeight: 1,
96+
display: 'inline-block',
97+
fontSize: 28
98+
},
99+
clock: {
100+
position: 'relative',
101+
top: '50%',
102+
transform: 'translateY(-50%)',
103+
float: 'right',
104+
margin: 0,
105+
lineHeight: 1,
106+
display: 'inline-block',
107+
fontSize: 28
108+
},
109+
preview: {
110+
display: 'flex',
111+
width: '100%',
112+
height: '100%',
113+
alignItems: 'center',
114+
justifyContent: 'center',
115+
flex: 1
116+
},
117+
main: {
118+
display: 'inline-block',
119+
width: '50%',
120+
height: '60%',
121+
border: '2px solid white',
122+
padding: 20,
123+
margin: 20,
124+
position: 'relative'
125+
},
126+
next: {
127+
display: 'inline-block',
128+
width: '40%',
129+
height: '50%',
130+
border: '2px solid white',
131+
padding: 20,
132+
margin: 20,
133+
position: 'relative',
134+
color: 'white'
135+
}
136+
};
137+
return (
138+
<div className='spectacle-presenter' style={[styles.presenter]}>
139+
<div style={styles.header}>
140+
<h2 style={styles.slideInfo}>Slide {this.props.slide + 1} of {this.props.slides.length}</h2>
141+
<h2 style={styles.clock}>{this.state.time}</h2>
142+
</div>
143+
<div style={styles.preview}>
144+
<div className="spectacle-presenter-main" style={[styles.main]}>
145+
{this._renderMainSlide()}
146+
</div>
147+
<div className="spectacle-presenter-next" style={[styles.next]}>
148+
{this._renderNextSlide()}
149+
</div>
150+
</div>
151+
</div>
152+
)
153+
}
154+
}
155+
156+
Presenter.propTypes = {
157+
lastSlide: React.PropTypes.number,
158+
slides: React.PropTypes.array,
159+
slide: React.PropTypes.number
160+
}
161+
162+
Presenter.contextTypes = {
163+
styles: React.PropTypes.object,
164+
flux: React.PropTypes.object,
165+
router: React.PropTypes.object
166+
}
167+
168+
export default Presenter;

0 commit comments

Comments
 (0)