Skip to content

Commit

Permalink
Feat: Presentation mode (elastic#177)
Browse files Browse the repository at this point in the history
* chore: fullscreen redux boilerplate

* chore: fullscreen api wrapper module

helper module for dealing with the fullscreen api, which exposes methods to setting up the change handlers, creating fullscreen element handlers, and check to see if the fullscreen api is accessible

* chore: initialize the fullscreen module on startup

* chore: add fullscreen logic and control components

* feat: add fullscreen control to workpad header

* fix: bug in firefox with fullscreen api

in the event handler, the target isn't the element in firefox, it looks like the body is...

* feat: use fullscreen component in workpad

when workpad is in fullscreen mode, use css transform to scale it to the display

* fix: correctly position the fullscreen workpad in firefox

* feat: add keymap rules for presentation mode

* fix: don't select elements in fullscreen mode

* feat: add keyboard navigation in presentation mode
  • Loading branch information
w33ble committed Sep 22, 2017
1 parent f49039a commit 5807049
Show file tree
Hide file tree
Showing 18 changed files with 321 additions and 19 deletions.
3 changes: 3 additions & 0 deletions public/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import template from './index.html';
import './state/store_service';
import './directives/react';
import './style/main.less';
import { initialize as initializeFullscreen } from './lib/fullscreen';

import { App } from './components/app';

initializeFullscreen(document);

const app = uiModules.get('apps/canvas', []);

uiRoutes.enable();
Expand Down
14 changes: 10 additions & 4 deletions public/components/element_wrapper/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,38 @@ import { get } from 'lodash';
import { ElementWrapper as Component } from './element_wrapper';
import { removeElement, setPosition } from '../../state/actions/elements';
import { selectElement } from '../../state/actions/transient';
import { getSelectedElementId, getResolvedArgs, getSelectedPage } from '../../state/selectors/workpad';
import { getFullscreen } from '../../state/selectors/app';
import {
getSelectedElementId,
getResolvedArgs,
getSelectedPage,
} from '../../state/selectors/workpad';
import { getState, getValue, getError } from '../../lib/resolved_arg';
import { elements as elementsRegistry } from '../../lib/elements';
import { createHandlers } from './lib/handlers';

const mapStateToProps = (state, { element }) => ({
isFullscreen: getFullscreen(state),
resolvedArg: getResolvedArgs(state, element.id, 'expressionRenderable'),
isSelected: element.id === getSelectedElementId(state),
selectedPage: getSelectedPage(state),
});

const mapDispatchToProps = (dispatch, { element }) => ({
selectElement: () => dispatch(selectElement(element.id)),
selectElement: isFullscreen => () => !isFullscreen && dispatch(selectElement(element.id)),
removeElement: (pageId) => () => dispatch(removeElement(element.id, pageId)),
setPosition: (pageId) => (position) => dispatch(setPosition(element.id, pageId, position)),
handlers: (pageId) => createHandlers(element, pageId, dispatch),
});

const mergeProps = (stateProps, dispatchProps, { element }) => {
const { resolvedArg, selectedPage, isSelected } = stateProps;
const { resolvedArg, selectedPage, isSelected, isFullscreen } = stateProps;
const renderable = getValue(resolvedArg);

return {
position: element.position,
setPosition: dispatchProps.setPosition(selectedPage),
selectElement: dispatchProps.selectElement,
selectElement: dispatchProps.selectElement(isFullscreen),
removeElement: dispatchProps.removeElement(selectedPage),
handlers: dispatchProps.handlers(selectedPage),
isSelected: isSelected,
Expand Down
15 changes: 15 additions & 0 deletions public/components/fullscreen/fullscreen.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';
import PropTypes from 'prop-types';

export const Fullscreen = ({ ident, isFullscreen, windowSize, children }) => (
<div id={ident} allowFullScreen>
{children({ isFullscreen, windowSize })}
</div>
);

Fullscreen.propTypes = {
ident: PropTypes.string.isRequired,
isFullscreen: PropTypes.bool,
windowSize: PropTypes.object,
children: PropTypes.func,
};
36 changes: 36 additions & 0 deletions public/components/fullscreen/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { connect } from 'react-redux';
import { compose, withProps, withState, withHandlers, lifecycle } from 'recompose';
import { debounce } from 'lodash';
import { Fullscreen as Component } from './fullscreen';
import { getFullscreen } from '../../state/selectors/app.js';
import { defaultIdent } from '../../lib/fullscreen.js';

const mapStateToProps = (state) => ({
isFullscreen: getFullscreen(state),
});

const getWindowSize = () => ({
width: window.innerWidth,
height: window.innerHeight,
});

export const Fullscreen = compose(
withProps(({ ident }) => ({
ident: ident || defaultIdent,
})),
withState('windowSize', 'setWindowSize', getWindowSize()),
withHandlers({
windowResizeHandler: ({ setWindowSize }) => debounce(() => {
setWindowSize(getWindowSize());
}, 100),
}),
connect(mapStateToProps),
lifecycle({
componentWillMount() {
window.addEventListener('resize', this.props.windowResizeHandler);
},
componentWillUnmount() {
window.removeEventListener('resize', this.props.windowResizeHandler);
},
}),
)(Component);
50 changes: 50 additions & 0 deletions public/components/fullscreen_control/fullscreen_control.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from 'react';
import PropTypes from 'prop-types';
import { createListener, canFullscreen } from '../../lib/fullscreen.js';

// TODO: this is a class because this has to use ref, and it seemed best to allow
// multile instances of this component... can we use ref with SFCs?
export class FullscreenControl extends React.PureComponent {
componentDidMount() {
// check that the fullscreen api is available, deactivate control if not
const el = this.node;
if (!canFullscreen(el)) {
this.props.setActive(false);
return;
}

// listen for changes to the fullscreen element, update state to match
this.fullscreenListener = createListener(({ fullscreen, element }) => {
// if the app is the fullscreen element, set the app state
if (!element || element.id === this.props.ident) this.props.setFullscreen(fullscreen);
});
}

componentWillUmount() {
// remove the fullscreen event listener
this.fullscreenListener && this.fullscreenListener();
}

render() {
const { isActive, children, isFullscreen, onFullscreen } = this.props;
if (!isActive) return null;
return (
<span ref={node => this.node = node}>
{children({ isFullscreen, onFullscreen })}
</span>
);
}
}

FullscreenControl.propTypes = {
isActive: PropTypes.bool.isRequired,
setActive: PropTypes.func.isRequired,
setFullscreen: PropTypes.func.isRequired,
children: PropTypes.func.isRequired,
ident: PropTypes.string.isRequired,
onFullscreen: PropTypes.oneOfType([
PropTypes.func,
PropTypes.bool,
]),
isFullscreen: PropTypes.bool,
};
32 changes: 32 additions & 0 deletions public/components/fullscreen_control/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { compose, withState, withProps } from 'recompose';
import { FullscreenControl as Component } from './fullscreen_control';
import { defaultIdent, createHandler } from '../../lib/fullscreen.js';
import { setFullscreen } from '../../state/actions/transient.js';
import { getFullscreen } from '../../state/selectors/app.js';

const mapStateToProps = (state) => ({
isFullscreen: getFullscreen(state),
});

const mapDispatchToProps = {
setFullscreen,
};

export const FullscreenControl = compose(
connect(mapStateToProps, mapDispatchToProps),
withState('isActive', 'setActive', true),
withProps(({ ident, isActive }) => ({
ident: ident || defaultIdent,
onFullscreen: (ev) => {
if (!isActive) return;
const fullscreenHandler = createHandler(ident || defaultIdent);
fullscreenHandler(ev);
},
})),
)(Component);

FullscreenControl.propTypes = {
ident: PropTypes.string,
};
5 changes: 4 additions & 1 deletion public/components/workpad/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { connect } from 'react-redux';
import { get } from 'lodash';
import { undoHistory, redoHistory } from '../../state/actions/history.js';
import { undoHistory, redoHistory } from '../../state/actions/history';
import { getElements, getPageById, getSelectedPage, getWorkpad } from '../../state/selectors/workpad';
import { nextPage, previousPage } from '../../state/actions/pages';
import { Workpad as Component } from './workpad';

const mapStateToProps = (state) => {
Expand All @@ -15,6 +16,8 @@ const mapStateToProps = (state) => {
const mapDispatchToProps = {
undoHistory,
redoHistory,
nextPage,
previousPage,
};

export const Workpad = connect(mapStateToProps, mapDispatchToProps)(Component);
48 changes: 40 additions & 8 deletions public/components/workpad/workpad.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,55 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Shortcuts } from 'react-shortcuts';
import { ElementWrapper } from '../element_wrapper';
import { Fullscreen } from '../fullscreen';
import './workpad.less';

export const Workpad = ({ elements, style, workpad, undoHistory, redoHistory }) => {
export const Workpad = (props) => {
const { elements, style, workpad, undoHistory, redoHistory, nextPage, previousPage } = props;
const { height, width } = workpad;
const itsTheNewStyle = Object.assign({}, style, { height, width });

const keyboardHandler = (action) => {
const workpadHandler = (action) => {
if (action === 'UNDO') return undoHistory();
if (action === 'REDO') return redoHistory();
};

const presentationHandler = isFullscreen => (action) => {
if (!isFullscreen) return;
if (action === 'PREV') return nextPage();
if (action === 'NEXT') return previousPage();
};

return (
<Shortcuts name="WORKPAD" handler={keyboardHandler} targetNodeSelector="body" global>
<Shortcuts name="WORKPAD" handler={workpadHandler} targetNodeSelector="body" global>
<div className="canvas__checkered" style={{ height, width }}>
<div className="canvas__workpad" style={itsTheNewStyle}>
{ elements.map(element => (
<ElementWrapper key={element.id} element={element} />
))}
</div>
<Fullscreen>
{({ isFullscreen, windowSize }) => {
const scale = Math.min(windowSize.height / height, windowSize.width / width);
const fsStyle = (!isFullscreen) ? {} : {
WebkitTransform: `scale3d(${scale}, ${scale}, 1)`,
msTransform: `scale3d(${scale}, ${scale}, 1)`,
transform: `scale3d(${scale}, ${scale}, 1)`,
};

return (
<Shortcuts
name="PRESENTATION"
handler={presentationHandler(isFullscreen)}
targetNodeSelector="body"
>
<div
className={`canvas__workpad ${isFullscreen ? 'fullscreen' : ''}`}
style={{ ...itsTheNewStyle, ...fsStyle }}
>
{ elements.map(element => (
<ElementWrapper key={element.id} element={element} />
))}
</div>
</Shortcuts>
);
}}
</Fullscreen>
</div>
</Shortcuts>
);
Expand All @@ -31,5 +61,7 @@ Workpad.propTypes = {
workpad: PropTypes.object.isRequired,
undoHistory: PropTypes.func.isRequired,
redoHistory: PropTypes.func.isRequired,
nextPage: PropTypes.func.isRequired,
previousPage: PropTypes.func.isRequired,
style: PropTypes.object,
};
9 changes: 9 additions & 0 deletions public/components/workpad/workpad.less
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,12 @@
box-shadow: 0px 0px 7px 0px rgba(0,0,0,0.2);
position: relative;
}

// For some bizarre reason, you must repeat the styles in their own blocks or they won’t be applied
// https://www.sitepoint.com/html5-full-screen-api/
// #canvas--fullscreen:-moz-full-screen {
#canvas--fullscreen {
display: flex;
justify-content: center;
align-items: center;
}
14 changes: 11 additions & 3 deletions public/components/workpad_header/workpad_header.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Toggle } from '../toggle';
import { FullscreenControl } from '../fullscreen_control';

import './workpad_header.less';
const btnClass = 'canvas__workpad_header--button';

export const WorkpadHeader = ({ workpadName, editing, inFlight, toggleEditing }) => {

return (
<div className="canvas__workpad_header">
<h2>
{ workpadName }
<span className="canvas__workpad_header--editToggle canvas__workpad_header--button">
<span className={`canvas__workpad_header--editToggle ${btnClass}`}>
<Toggle value={editing} onChange={toggleEditing} />
</span>
<FullscreenControl>
{({ onFullscreen }) => (
<span className={`canvas__workpad_header--fullscreenControl ${btnClass}`}>
<i className="fa fa-play" onClick={onFullscreen} />
</span>
)}
</FullscreenControl>
{ inFlight && (
<span className="canvas__workpad_header--button">
<span className={btnClass}>
<i className="fa fa-spinner fa-pulse" />
</span>
) }
Expand Down
2 changes: 1 addition & 1 deletion public/components/workpad_header/workpad_header.less
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
}

.canvas__workpad_header--button {
margin-left: @spacingS;
margin-left: @spacingL;
cursor: pointer;
}
}
Expand Down
Loading

0 comments on commit 5807049

Please sign in to comment.