Skip to content
Snippets Groups Projects
Commit 6e19d679 authored by Shaun Ellis's avatar Shaun Ellis Committed by Chris Beer
Browse files

Adds menu item, actions, and reducer for toggling zoom

parent c4684d42
Branches
Tags
No related merge requests found
...@@ -31,4 +31,13 @@ describe('workspace actions', () => { ...@@ -31,4 +31,13 @@ describe('workspace actions', () => {
expect(actions.updateWorkspaceMosaicLayout(options)).toEqual(expectedAction); expect(actions.updateWorkspaceMosaicLayout(options)).toEqual(expectedAction);
}); });
}); });
describe('toggleZoomControls', () => {
it('should set the zoom control visibility', () => {
const expectedAction = {
type: ActionTypes.TOGGLE_ZOOM_CONTROLS,
showZoomControls: true,
};
expect(actions.toggleZoomControls(true)).toEqual(expectedAction);
});
});
}); });
...@@ -6,9 +6,19 @@ import WindowList from '../../../src/containers/WindowList'; ...@@ -6,9 +6,19 @@ import WindowList from '../../../src/containers/WindowList';
describe('WorkspaceMenu', () => { describe('WorkspaceMenu', () => {
let wrapper; let wrapper;
let handleClose; let handleClose;
const showZoomControls = false;
let toggleZoomControls;
beforeEach(() => { beforeEach(() => {
handleClose = jest.fn(); handleClose = jest.fn();
wrapper = shallow(<WorkspaceMenu handleClose={handleClose} />); toggleZoomControls = jest.fn();
wrapper = shallow(
<WorkspaceMenu
handleClose={handleClose}
showZoomControls={showZoomControls}
toggleZoomControls={toggleZoomControls}
/>,
);
}); });
it('renders without an error', () => { it('renders without an error', () => {
...@@ -34,4 +44,11 @@ describe('WorkspaceMenu', () => { ...@@ -34,4 +44,11 @@ describe('WorkspaceMenu', () => {
expect(wrapper.find(WindowList).props().open).toBe(false); expect(wrapper.find(WindowList).props().open).toBe(false);
}); });
}); });
describe('handleZoomToggleClick', () => {
it('resets the anchor state', () => {
wrapper.instance().handleZoomToggleClick();
expect(toggleZoomControls).toBeCalledWith(true);
});
});
}); });
import React from 'react';
import { shallow } from 'enzyme';
import ZoomControls from '../../../src/components/ZoomControls';
describe('ZoomControls', () => {
let wrapper;
const viewer = { x: 100, y: 100, zoom: 1 };
const showZoomControls = false;
let updateViewport;
beforeEach(() => {
updateViewport = jest.fn();
wrapper = shallow(
<ZoomControls
windowId="xyz"
viewer={viewer}
showZoomControls={showZoomControls}
updateViewport={updateViewport}
/>,
).dive();
});
describe('with showZoomControls=false', () => {
it('renders nothing unless asked', () => {
expect(wrapper.find('WithStyles(List)').length).toBe(0);
});
});
describe('with showZoomControls=true', () => {
beforeEach(() => {
updateViewport = jest.fn();
wrapper = shallow(
<ZoomControls
windowId="xyz"
viewer={viewer}
showZoomControls
updateViewport={updateViewport}
/>,
).dive();
});
it('renders a couple buttons', () => {
expect(wrapper.find('WithStyles(List)').length).toBe(1);
});
it('has a zoom-in button', () => {
const button = wrapper.find('WithStyles(IconButton)[aria-label="zoomIn"]');
expect(button.simulate('click'));
expect(updateViewport).toHaveBeenCalledTimes(1);
expect(updateViewport).toHaveBeenCalledWith('xyz', { x: 100, y: 100, zoom: 2 });
});
it('has a zoom-out button', () => {
const button = wrapper.find('WithStyles(IconButton)[aria-label="zoomOut"]');
expect(button.simulate('click'));
expect(updateViewport).toHaveBeenCalledTimes(1);
expect(updateViewport).toHaveBeenCalledWith('xyz', { x: 100, y: 100, zoom: 0.5 });
});
it('has a zoom reseet button', () => {
const button = wrapper.find('WithStyles(IconButton)[aria-label="zoomReset"]');
expect(button.simulate('click'));
expect(updateViewport).toHaveBeenCalledTimes(1);
expect(updateViewport).toHaveBeenCalledWith('xyz', { x: 100, y: 100, zoom: 1 });
});
});
describe('handleZoomInClick', () => {
it('increases the zoom value on Zoom-In', () => {
wrapper.instance().handleZoomInClick();
expect(updateViewport).toHaveBeenCalled();
});
});
});
...@@ -18,6 +18,14 @@ describe('workspace reducer', () => { ...@@ -18,6 +18,14 @@ describe('workspace reducer', () => {
isFullscreenEnabled: true, isFullscreenEnabled: true,
}); });
}); });
it('should handle TOGGLE_ZOOM_CONTROLS', () => {
expect(reducer([], {
type: ActionTypes.TOGGLE_ZOOM_CONTROLS,
showZoomControls: true,
})).toEqual({
showZoomControls: true,
});
});
it('should handle UPDATE_WORKSPACE_MOSAIC_LAYOUT', () => { it('should handle UPDATE_WORKSPACE_MOSAIC_LAYOUT', () => {
expect(reducer([], { expect(reducer([], {
type: ActionTypes.UPDATE_WORKSPACE_MOSAIC_LAYOUT, type: ActionTypes.UPDATE_WORKSPACE_MOSAIC_LAYOUT,
......
...@@ -23,6 +23,9 @@ ...@@ -23,6 +23,9 @@
"theme": "Theme", "theme": "Theme",
"thumbnails": "Thumbnails", "thumbnails": "Thumbnails",
"toggleWindowSideBar": "Toggle window sidebar", "toggleWindowSideBar": "Toggle window sidebar",
"untitled": "[Untitled]" "untitled": "[Untitled]",
"zoomIn": "Zoom in",
"zoomOut": "Zoom out",
"zoomReset": "Reset zoom"
} }
} }
...@@ -2,6 +2,7 @@ import React, { Component } from 'react'; ...@@ -2,6 +2,7 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import OpenSeadragon from 'openseadragon'; import OpenSeadragon from 'openseadragon';
import ns from '../config/css-ns'; import ns from '../config/css-ns';
import ZoomControls from '../containers/ZoomControls';
/** /**
* Represents a OpenSeadragonViewer in the mirador workspace. Responsible for mounting * Represents a OpenSeadragonViewer in the mirador workspace. Responsible for mounting
...@@ -154,6 +155,7 @@ class OpenSeadragonViewer extends Component { ...@@ -154,6 +155,7 @@ class OpenSeadragonViewer extends Component {
> >
{ children } { children }
</div> </div>
<ZoomControls windowId={window.id} />
</> </>
); );
} }
......
...@@ -2,6 +2,7 @@ import React, { Component } from 'react'; ...@@ -2,6 +2,7 @@ import React, { Component } from 'react';
import Menu from '@material-ui/core/Menu'; import Menu from '@material-ui/core/Menu';
import Divider from '@material-ui/core/Divider'; import Divider from '@material-ui/core/Divider';
import ListItemIcon from '@material-ui/core/ListItemIcon'; import ListItemIcon from '@material-ui/core/ListItemIcon';
import LoupeIcon from '@material-ui/icons/Loupe';
import MenuItem from '@material-ui/core/MenuItem'; import MenuItem from '@material-ui/core/MenuItem';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
import SaveAltIcon from '@material-ui/icons/SaveAlt'; import SaveAltIcon from '@material-ui/icons/SaveAlt';
...@@ -22,6 +23,7 @@ class WorkspaceMenu extends Component { ...@@ -22,6 +23,7 @@ class WorkspaceMenu extends Component {
super(props); super(props);
this.state = { this.state = {
windowList: {}, windowList: {},
toggleZoom: {},
settings: {}, settings: {},
exportWorkspace: {}, exportWorkspace: {},
}; };
...@@ -53,13 +55,29 @@ class WorkspaceMenu extends Component { ...@@ -53,13 +55,29 @@ class WorkspaceMenu extends Component {
}; };
} }
/**
* @private
*/
handleZoomToggleClick() {
const { toggleZoomControls, showZoomControls } = this.props;
toggleZoomControls(!showZoomControls);
}
/** /**
* render * render
* @return * @return
*/ */
render() { render() {
const { handleClose, anchorEl, t } = this.props; const {
const { windowList, settings, exportWorkspace } = this.state; handleClose, anchorEl, t, showZoomControls,
} = this.props;
const {
windowList,
toggleZoom,
settings,
exportWorkspace,
} = this.state;
return ( return (
<> <>
...@@ -74,6 +92,16 @@ class WorkspaceMenu extends Component { ...@@ -74,6 +92,16 @@ class WorkspaceMenu extends Component {
</ListItemIcon> </ListItemIcon>
<Typography varient="inherit">{t('listAllOpenWindows')}</Typography> <Typography varient="inherit">{t('listAllOpenWindows')}</Typography>
</MenuItem> </MenuItem>
<MenuItem
aria-haspopup="true"
onClick={(e) => { this.handleZoomToggleClick(e); handleClose(e); }}
aria-owns={toggleZoom.anchorEl ? 'toggle-zoom-menu' : undefined}
>
<ListItemIcon>
<LoupeIcon />
</ListItemIcon>
<Typography varient="inherit">{ showZoomControls ? 'Hide zoom controls' : 'Show Zoom Controls' }</Typography>
</MenuItem>
<Divider /> <Divider />
<MenuItem <MenuItem
aria-haspopup="true" aria-haspopup="true"
...@@ -101,6 +129,10 @@ class WorkspaceMenu extends Component { ...@@ -101,6 +129,10 @@ class WorkspaceMenu extends Component {
open={Boolean(windowList.anchorEl)} open={Boolean(windowList.anchorEl)}
handleClose={this.handleMenuItemClose('windowList')} handleClose={this.handleMenuItemClose('windowList')}
/> />
<WorkspaceSettings
open={Boolean(toggleZoom.open)}
handleClose={this.handleMenuItemClose('toggleZoom')}
/>
<WorkspaceSettings <WorkspaceSettings
open={Boolean(settings.open)} open={Boolean(settings.open)}
handleClose={this.handleMenuItemClose('settings')} handleClose={this.handleMenuItemClose('settings')}
...@@ -116,6 +148,8 @@ class WorkspaceMenu extends Component { ...@@ -116,6 +148,8 @@ class WorkspaceMenu extends Component {
WorkspaceMenu.propTypes = { WorkspaceMenu.propTypes = {
handleClose: PropTypes.func.isRequired, handleClose: PropTypes.func.isRequired,
toggleZoomControls: PropTypes.func,
showZoomControls: PropTypes.bool,
anchorEl: PropTypes.object, // eslint-disable-line react/forbid-prop-types anchorEl: PropTypes.object, // eslint-disable-line react/forbid-prop-types
t: PropTypes.func, t: PropTypes.func,
}; };
...@@ -123,6 +157,8 @@ WorkspaceMenu.propTypes = { ...@@ -123,6 +157,8 @@ WorkspaceMenu.propTypes = {
WorkspaceMenu.defaultProps = { WorkspaceMenu.defaultProps = {
anchorEl: null, anchorEl: null,
t: key => key, t: key => key,
showZoomControls: false,
toggleZoomControls: () => {},
}; };
export default WorkspaceMenu; export default WorkspaceMenu;
import React, { Component } from 'react';
import { withStyles } from '@material-ui/core/styles';
import IconButton from '@material-ui/core/IconButton';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import AddCircleIcon from '@material-ui/icons/AddCircle';
import RemoveCircleIcon from '@material-ui/icons/RemoveCircle';
import RefreshIcon from '@material-ui/icons/Refresh';
import PropTypes from 'prop-types';
/**
*/
class ZoomControls extends Component {
/**
* constructor -
*/
constructor(props) {
super(props);
this.handleZoomInClick = this.handleZoomInClick.bind(this);
this.handleZoomOutClick = this.handleZoomOutClick.bind(this);
this.handleZoomResetClick = this.handleZoomResetClick.bind(this);
}
/**
* @private
*/
handleZoomInClick() {
const { windowId, updateViewport, viewer } = this.props;
updateViewport(windowId, {
x: viewer.x,
y: viewer.y,
zoom: viewer.zoom * 2,
});
}
/**
* @private
*/
handleZoomOutClick() {
const { windowId, updateViewport, viewer } = this.props;
updateViewport(windowId, {
x: viewer.x,
y: viewer.y,
zoom: viewer.zoom / 2,
});
}
/**
* @private
*/
handleZoomResetClick() {
const { windowId, updateViewport, viewer } = this.props;
updateViewport(windowId, {
x: viewer.x,
y: viewer.y,
zoom: 1,
});
}
/**
* render
* @return
*/
render() {
const { showZoomControls, classes, t } = this.props;
if (!showZoomControls) {
return (
<>
</>
);
}
return (
<List className={classes.zoom_controls}>
<ListItem>
<IconButton aria-label={t('zoomIn')} onClick={this.handleZoomInClick}>
<AddCircleIcon />
</IconButton>
</ListItem>
<ListItem>
<IconButton aria-label={t('zoomOut')} onClick={this.handleZoomOutClick}>
<RemoveCircleIcon />
</IconButton>
</ListItem>
<ListItem>
<IconButton aria-label={t('zoomReset')} onClick={this.handleZoomResetClick}>
<RefreshIcon />
</IconButton>
</ListItem>
</List>
);
}
}
ZoomControls.propTypes = {
windowId: PropTypes.string,
showZoomControls: PropTypes.bool,
viewer: PropTypes.shape({
x: PropTypes.number,
y: PropTypes.number,
zoom: PropTypes.number,
}),
updateViewport: PropTypes.func,
classes: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
t: PropTypes.func,
};
ZoomControls.defaultProps = {
windowId: '',
showZoomControls: false,
viewer: {},
updateViewport: () => {},
t: key => key,
};
/**
* @private
*/
const styles = theme => ({
zoom_controls: {
position: 'absolute',
right: 0,
},
ListItem: {
paddingTop: 0,
paddingBottom: 0,
},
});
export default withStyles(styles)(ZoomControls);
import { compose } from 'redux'; import { compose } from 'redux';
import { connect } from 'react-redux';
import { withNamespaces } from 'react-i18next'; import { withNamespaces } from 'react-i18next';
import miradorWithPlugins from '../lib/miradorWithPlugins'; import miradorWithPlugins from '../lib/miradorWithPlugins';
import * as actions from '../state/actions';
import WorkspaceMenu from '../components/WorkspaceMenu'; import WorkspaceMenu from '../components/WorkspaceMenu';
/**
* mapDispatchToProps - used to hook up connect to action creators
* @memberof ManifestListItem
* @private
*/
const mapDispatchToProps = { toggleZoomControls: actions.toggleZoomControls };
/**
* mapStateToProps - to hook up connect
* @memberof WindowViewer
* @private
*/
const mapStateToProps = state => (
{ showZoomControls: state.workspace.showZoomControls }
);
const enhance = compose( const enhance = compose(
connect(mapStateToProps, mapDispatchToProps),
withNamespaces(), withNamespaces(),
miradorWithPlugins, miradorWithPlugins,
// further HOC // further HOC
......
import { compose } from 'redux';
import { connect } from 'react-redux';
import { withNamespaces } from 'react-i18next';
import * as actions from '../state/actions';
import ZoomControls from '../components/ZoomControls';
/**
* mapStateToProps - to hook up connect
* @memberof Workspace
* @private
*/
const mapStateToProps = (state, props) => (
{
showZoomControls: state.workspace.showZoomControls,
viewer: state.windows[props.windowId].viewer,
}
);
/**
* mapDispatchToProps - used to hook up connect to action creators
* @memberof Workspace
* @private
*/
const mapDispatchToProps = { updateViewport: actions.updateViewport };
const enhance = compose(
connect(mapStateToProps, mapDispatchToProps),
withNamespaces(),
// further HOC go here
);
export default enhance(ZoomControls);
...@@ -15,6 +15,7 @@ const ActionTypes = { ...@@ -15,6 +15,7 @@ const ActionTypes = {
SET_WINDOW_THUMBNAIL_POSITION: 'SET_WINDOW_THUMBNAIL_POSITION', SET_WINDOW_THUMBNAIL_POSITION: 'SET_WINDOW_THUMBNAIL_POSITION',
TOGGLE_WINDOW_SIDE_BAR: 'TOGGLE_WINDOW_SIDE_BAR', TOGGLE_WINDOW_SIDE_BAR: 'TOGGLE_WINDOW_SIDE_BAR',
TOGGLE_WINDOW_SIDE_BAR_PANEL: 'TOGGLE_WINDOW_SIDE_BAR_PANEL', TOGGLE_WINDOW_SIDE_BAR_PANEL: 'TOGGLE_WINDOW_SIDE_BAR_PANEL',
TOGGLE_ZOOM_CONTROLS: 'TOGGLE_ZOOM_CONTROLS',
UPDATE_CONFIG: 'UPDATE_CONFIG', UPDATE_CONFIG: 'UPDATE_CONFIG',
REMOVE_MANIFEST: 'REMOVE_MANIFEST', REMOVE_MANIFEST: 'REMOVE_MANIFEST',
REQUEST_INFO_RESPONSE: 'REQUEST_INFO_RESPONSE', REQUEST_INFO_RESPONSE: 'REQUEST_INFO_RESPONSE',
......
...@@ -11,6 +11,15 @@ export function setWorkspaceFullscreen(isFullscreenEnabled) { ...@@ -11,6 +11,15 @@ export function setWorkspaceFullscreen(isFullscreenEnabled) {
return { type: ActionTypes.SET_WORKSPACE_FULLSCREEN, isFullscreenEnabled }; return { type: ActionTypes.SET_WORKSPACE_FULLSCREEN, isFullscreenEnabled };
} }
/**
* toggleZoomControls - action creator
* @param {Boolean} showZoomControls
* @memberof ActionCreators
*/
export function toggleZoomControls(showZoomControls) {
return { type: ActionTypes.TOGGLE_ZOOM_CONTROLS, showZoomControls };
}
/** /**
* updateWorkspaceMosaicLayout - action creator * updateWorkspaceMosaicLayout - action creator
* *
......
...@@ -9,6 +9,8 @@ const workspaceReducer = (state = {}, action) => { ...@@ -9,6 +9,8 @@ const workspaceReducer = (state = {}, action) => {
return { ...state, focusedWindowId: action.windowId }; return { ...state, focusedWindowId: action.windowId };
case ActionTypes.SET_WORKSPACE_FULLSCREEN: case ActionTypes.SET_WORKSPACE_FULLSCREEN:
return { ...state, isFullscreenEnabled: action.isFullscreenEnabled }; return { ...state, isFullscreenEnabled: action.isFullscreenEnabled };
case ActionTypes.TOGGLE_ZOOM_CONTROLS:
return { ...state, showZoomControls: action.showZoomControls };
case ActionTypes.UPDATE_WORKSPACE_MOSAIC_LAYOUT: case ActionTypes.UPDATE_WORKSPACE_MOSAIC_LAYOUT:
return { ...state, layout: action.layout }; return { ...state, layout: action.layout };
default: default:
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment