Skip to content
Snippets Groups Projects
Commit 71a21909 authored by Andrew Winget (Standard)'s avatar Andrew Winget (Standard) Committed by aeschylus
Browse files

adds overlapping, panning windowing system using react-Rnd

parent df616ce5
No related branches found
No related tags found
No related merge requests found
Showing
with 571 additions and 12 deletions
...@@ -19,7 +19,10 @@ describe('window actions', () => { ...@@ -19,7 +19,10 @@ describe('window actions', () => {
manifestId: null, manifestId: null,
rangeId: null, rangeId: null,
thumbnailNavigationPosition: 'bottom', thumbnailNavigationPosition: 'bottom',
xywh: [0, 0, 400, 400], x: 2700,
y: 2700,
width: 400,
height: 400,
rotation: null, rotation: null,
view: 'single', view: 'single',
}, },
...@@ -141,4 +144,48 @@ describe('window actions', () => { ...@@ -141,4 +144,48 @@ describe('window actions', () => {
}); });
}); });
}); });
describe('setWindowSize', () => {
it('returns the appropriate action type', () => {
const id = 'abc123';
const expectedAction = {
type: ActionTypes.SET_WINDOW_SIZE,
payload: {
windowId: id,
size: {
x: 20,
y: 20,
width: 200,
height: 200,
},
},
};
expect(actions.setWindowSize(id, {
x: 20,
y: 20,
width: 200,
height: 200,
})).toEqual(expectedAction);
});
});
describe('updateWindowPosition', () => {
it('returns the appropriate action type', () => {
const id = 'abc123';
const expectedAction = {
type: ActionTypes.UPDATE_WINDOW_POSITION,
payload: {
windowId: id,
position: {
x: 20,
y: 20,
},
},
};
expect(actions.updateWindowPosition(id, {
x: 20,
y: 20,
})).toEqual(expectedAction);
});
});
}); });
...@@ -49,4 +49,29 @@ describe('workspace actions', () => { ...@@ -49,4 +49,29 @@ describe('workspace actions', () => {
expect(actions.setWorkspaceAddVisibility(true)).toEqual(expectedAction); expect(actions.setWorkspaceAddVisibility(true)).toEqual(expectedAction);
}); });
}); });
describe('setWorkspaceViewportPosition', () => {
it('should set the workspace add visibility', () => {
const expectedAction = {
type: ActionTypes.SET_WORKSPACE_VIEWPORT_POSITION,
payload: {
position: {
x: 20,
y: 20,
},
},
};
expect(actions.setWorkspaceViewportPosition({
x: 20,
y: 20,
})).toEqual(expectedAction);
});
});
describe('toggleWorkspaceExposeMode', () => {
it('should set the exposeMode to true', () => {
const expectedAction = {
type: ActionTypes.TOGGLE_WORKSPACE_EXPOSE_MODE,
};
expect(actions.toggleWorkspaceExposeMode()).toEqual(expectedAction);
});
});
}); });
...@@ -28,7 +28,7 @@ function createWrapper(props) { ...@@ -28,7 +28,7 @@ function createWrapper(props) {
describe('App', () => { describe('App', () => {
it('should render outer element correctly', () => { it('should render outer element correctly', () => {
const wrapper = createWrapper(); const wrapper = createWrapper();
expect(wrapper.find('div.mirador-app').length).toBe(1); expect(wrapper.find('div.mirador-viewer').length).toBe(1);
}); });
it('should render all needed elements ', () => { it('should render all needed elements ', () => {
......
...@@ -17,7 +17,13 @@ function createWrapper(props, context) { ...@@ -17,7 +17,13 @@ function createWrapper(props, context) {
describe('Window', () => { describe('Window', () => {
let wrapper; let wrapper;
const window = { id: 123, xywh: [0, 0, 400, 500] }; const window = {
id: 123,
x: 2700,
y: 2700,
width: 400,
height: 400,
};
it('should render nothing, if provided with no window data', () => { it('should render nothing, if provided with no window data', () => {
wrapper = shallow(<Window />); wrapper = shallow(<Window />);
expect(wrapper.find('.mirador-window')).toHaveLength(0); expect(wrapper.find('.mirador-window')).toHaveLength(0);
......
import React from 'react'; import React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import WorkspaceMosaic from '../../../src/containers/WorkspaceMosaic'; import WorkspaceMosaic from '../../../src/containers/WorkspaceMosaic';
import WorkspaceElastic from '../../../src/containers/WorkspaceElastic';
import Window from '../../../src/containers/Window'; import Window from '../../../src/containers/Window';
import { Workspace } from '../../../src/components/Workspace'; import { Workspace } from '../../../src/components/Workspace';
...@@ -22,12 +23,23 @@ function createWrapper(props) { ...@@ -22,12 +23,23 @@ function createWrapper(props) {
} }
describe('Workspace', () => { describe('Workspace', () => {
describe('if workspace type is elastic', () => {
it('should render <WorkspaceElastic/> properly', () => {
const wrapper = createWrapper({ workspaceType: 'elastic' });
expect(wrapper.matchesElement(
<div className="mirador-workspace-viewport mirador-workspace-with-control-panel">
<WorkspaceElastic />
</div>,
)).toBe(true);
});
});
describe('if workspace type is mosaic', () => { describe('if workspace type is mosaic', () => {
it('should render <WorkspaceMosaic/> properly', () => { it('should render <WorkspaceMosaic/> properly', () => {
const wrapper = createWrapper(); const wrapper = createWrapper();
expect(wrapper.matchesElement( expect(wrapper.matchesElement(
<div className="mirador-workspace mirador-workspace-with-control-panel"> <div className="mirador-workspace-viewport mirador-workspace-with-control-panel">
<WorkspaceMosaic windows={windows} /> <WorkspaceMosaic windows={windows} />
</div>, </div>,
)).toBe(true); )).toBe(true);
...@@ -38,7 +50,7 @@ describe('Workspace', () => { ...@@ -38,7 +50,7 @@ describe('Workspace', () => {
const wrapper = createWrapper({ workspaceType: 'bubu' }); const wrapper = createWrapper({ workspaceType: 'bubu' });
expect(wrapper.matchesElement( expect(wrapper.matchesElement(
<div className="mirador-workspace mirador-workspace-with-control-panel"> <div className="mirador-workspace-viewport mirador-workspace-with-control-panel">
<Window window={{ id: 1 }} /> <Window window={{ id: 1 }} />
<Window window={{ id: 2 }} /> <Window window={{ id: 2 }} />
</div>, </div>,
......
import React from 'react';
import { shallow } from 'enzyme';
import { Rnd } from 'react-rnd';
import WorkspaceElastic from '../../../src/components/WorkspaceElastic';
/** create wrapper */
function createWrapper(props) {
return shallow(
<WorkspaceElastic
windows={{}}
workspace={{
viewportPosition: {
x: 20,
y: 20,
},
}}
setWorkspaceViewportPosition={() => {}}
setWindowSize={() => {}}
updateWindowPosition={() => {}}
{...props}
/>,
);
}
describe('WorkspaceElastic', () => {
const windows = {
1: {
id: 1,
x: 20,
y: 20,
width: 200,
height: 200,
},
2: {
id: 2,
x: 25,
y: 25,
width: 300,
height: 400,
},
};
let wrapper;
beforeEach(() => {
wrapper = createWrapper({ windows });
});
it('should render properly with an initialValue', () => {
expect(wrapper.find(Rnd).length).toBe(3);
expect(wrapper
.find(Rnd)
.at(1)
.props().size)
.toEqual({
width: 200,
height: 200,
});
expect(wrapper
.find(Rnd)
.at(2)
.props().position)
.toEqual({
x: 25,
y: 25,
});
expect(wrapper
.find(Rnd)
.at(2)
.props().size)
.toEqual({
width: 300,
height: 400,
});
});
describe('window behaviour', () => {
it('when windows are dragged', () => {
const mockDragStop = jest.fn();
wrapper = createWrapper({
windows,
updateWindowPosition: mockDragStop,
});
wrapper
.find(Rnd)
.at(1)
.props()
.onDragStop('myevent', {
x: 200,
y: 200,
});
expect(mockDragStop).toHaveBeenCalledWith(1, {
x: 200,
y: 200,
});
});
it('when windows are resized', () => {
const mockOnResize = jest.fn();
wrapper = createWrapper({
windows,
setWindowSize: mockOnResize,
});
wrapper
.find(Rnd)
.at(1)
.props()
.onResize('myevent', 'direction', {
style: {
width: 400,
height: 200,
},
});
expect(mockOnResize).toHaveBeenCalledWith(1, {
width: 400,
height: 200,
});
});
});
describe('workspace behaviour', () => {
it('when workspace itself is dragged', () => {
const mockDragStop = jest.fn();
wrapper = createWrapper({
windows,
setWorkspaceViewportPosition: mockDragStop,
});
wrapper
.find(Rnd)
.at(0)
.props()
.onDragStop('myevent', {
x: 200,
y: 200,
});
expect(mockDragStop).toHaveBeenCalledWith({
x: 200,
y: 200,
});
});
});
});
...@@ -170,4 +170,66 @@ describe('windows reducer', () => { ...@@ -170,4 +170,66 @@ describe('windows reducer', () => {
expect(windowsReducer(beforeState, action)).toEqual(expectedState); expect(windowsReducer(beforeState, action)).toEqual(expectedState);
}); });
}); });
it('should handle SET_WINDOW_SIZE', () => {
expect(windowsReducer({
abc123: {
id: 'abc123',
},
def456: {
id: 'def456',
},
}, {
type: ActionTypes.SET_WINDOW_SIZE,
payload: {
windowId: 'abc123',
size: {
x: 20,
y: 20,
width: 200,
height: 200,
},
},
})).toEqual({
abc123: {
id: 'abc123',
x: 20,
y: 20,
width: 200,
height: 200,
},
def456: {
id: 'def456',
},
});
});
it('should handle UPDATE_WINDOW_POSITION', () => {
expect(windowsReducer({
abc123: {
id: 'abc123',
},
def456: {
id: 'def456',
},
}, {
type: ActionTypes.UPDATE_WINDOW_POSITION,
payload: {
windowId: 'abc123',
position: {
x: 20,
y: 20,
},
},
})).toEqual({
abc123: {
id: 'abc123',
x: 20,
y: 20,
},
def456: {
id: 'def456',
},
});
});
}); });
...@@ -42,4 +42,27 @@ describe('workspace reducer', () => { ...@@ -42,4 +42,27 @@ describe('workspace reducer', () => {
isWorkspaceAddVisible: true, isWorkspaceAddVisible: true,
}); });
}); });
it('should handle SET_WORKSPACE_VIEWPORT_POSITION', () => {
expect(workspaceReducer([], {
type: ActionTypes.SET_WORKSPACE_VIEWPORT_POSITION,
payload: {
position: {
x: 50,
y: 50,
},
},
})).toEqual({
viewportPosition: {
x: 50,
y: 50,
},
});
});
it('should handle TOGGLE_WORKSPACE_EXPOSE_MODE', () => {
expect(workspaceReducer([], {
type: ActionTypes.TOGGLE_WORKSPACE_EXPOSE_MODE,
})).toEqual({
exposeModeOn: true,
});
});
}); });
...@@ -57,7 +57,7 @@ export class App extends Component { ...@@ -57,7 +57,7 @@ export class App extends Component {
}); });
return ( return (
<div className={classNames(classes.background, ns('app'))}> <div className={classNames(classes.background, ns('viewer'))}>
<I18nextProvider i18n={this.i18n}> <I18nextProvider i18n={this.i18n}>
<MuiThemeProvider theme={createMuiTheme(theme)}> <MuiThemeProvider theme={createMuiTheme(theme)}>
<Fullscreen <Fullscreen
......
...@@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; ...@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import Window from '../containers/Window'; import Window from '../containers/Window';
import WorkspaceMosaic from '../containers/WorkspaceMosaic'; import WorkspaceMosaic from '../containers/WorkspaceMosaic';
import WorkspaceElastic from '../containers/WorkspaceElastic';
import ns from '../config/css-ns'; import ns from '../config/css-ns';
/** /**
...@@ -17,6 +18,8 @@ export class Workspace extends React.Component { ...@@ -17,6 +18,8 @@ export class Workspace extends React.Component {
workspaceByType() { workspaceByType() {
const { workspaceType, windows } = this.props; const { workspaceType, windows } = this.props;
switch (workspaceType) { switch (workspaceType) {
case 'elastic':
return <WorkspaceElastic />;
case 'mosaic': case 'mosaic':
return <WorkspaceMosaic windows={windows} />; return <WorkspaceMosaic windows={windows} />;
default: default:
...@@ -39,7 +42,7 @@ export class Workspace extends React.Component { ...@@ -39,7 +42,7 @@ export class Workspace extends React.Component {
<div <div
className={ className={
classNames( classNames(
ns('workspace'), ns('workspace-viewport'),
(isWorkspaceControlPanelVisible && ns('workspace-with-control-panel')), (isWorkspaceControlPanelVisible && ns('workspace-with-control-panel')),
) )
} }
......
import React from 'react';
import PropTypes from 'prop-types';
import { Rnd } from 'react-rnd';
import Window from '../containers/Window';
import ns from '../config/css-ns';
/**
* Represents a work area that contains any number of windows
* @memberof Workspace
* @private
*/
class WorkspaceElastic extends React.Component {
/**
*/
render() {
const {
workspace,
windows,
setWorkspaceViewportPosition,
updateWindowPosition,
setWindowSize,
} = this.props;
return (
<Rnd
default={{
width: 5000,
height: 5000,
}}
position={{ x: workspace.viewportPosition.x, y: workspace.viewportPosition.y }}
enableResizing={{
top: false,
right: false,
bottom: false,
left: false,
topRight: false,
bottomRight: false,
bottomLeft: false,
topLeft: false,
}}
onDragStop={(e, d) => {
setWorkspaceViewportPosition({ x: d.x, y: d.y });
}}
cancel={`.${ns('window')}`}
className={ns('workspace')}
>
{
Object.values(windows).map(window => (
<Rnd
key={window.id}
size={{ width: window.width, height: window.height }}
position={{ x: window.x, y: window.y }}
bounds="parent"
onDragStop={(e, d) => {
updateWindowPosition(window.id, { x: d.x, y: d.y });
}}
onResize={(e, direction, ref, delta, position) => {
setWindowSize(window.id, {
width: ref.style.width,
height: ref.style.height,
...position,
});
}}
dragHandleClassName={ns('window-top-bar')}
>
<Window
window={window}
/>
</Rnd>
))
}
</Rnd>
);
}
}
WorkspaceElastic.propTypes = {
setWorkspaceViewportPosition: PropTypes.func.isRequired,
windows: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
workspace: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
updateWindowPosition: PropTypes.func.isRequired,
setWindowSize: PropTypes.func.isRequired,
};
export default WorkspaceElastic;
...@@ -44,5 +44,5 @@ export default { ...@@ -44,5 +44,5 @@ export default {
}, },
workspaceControlPanel: { workspaceControlPanel: {
enabled: true, enabled: true,
} },
}; };
...@@ -2,6 +2,7 @@ import { compose } from 'redux'; ...@@ -2,6 +2,7 @@ import { compose } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Window } from '../components/Window'; import { Window } from '../components/Window';
/** /**
* mapStateToProps - used to hook up connect to action creators * mapStateToProps - used to hook up connect to action creators
* @memberof Window * @memberof Window
......
import { compose } from 'redux';
import { connect } from 'react-redux';
import * as actions from '../state/actions';
import WorkspaceElastic from '../components/WorkspaceElastic';
/**
* mapStateToProps - to hook up connect
* @memberof Workspace
* @private
*/
const mapStateToProps = state => (
{
workspace: state.workspace,
windows: state.windows,
}
);
/**
* mapDispatchToProps - used to hook up connect to action creators
* @memberof Workspace
* @private
*/
const mapDispatchToProps = (dispatch, props) => ({
setWorkspaceViewportPosition: (position) => {
dispatch(
actions.setWorkspaceViewportPosition(position),
);
},
toggleWorkspaceExposeMode: size => dispatch(
actions.toggleWorkspaceExposeMode(),
),
updateWindowPosition: (windowId, position) => {
dispatch(
actions.updateWindowPosition(windowId, position),
);
},
setWindowSize: (windowId, size) => {
dispatch(
actions.setWindowSize(windowId, size),
);
},
});
const enhance = compose(
connect(mapStateToProps, mapDispatchToProps),
// further HOC go here
);
export default enhance(WorkspaceElastic);
...@@ -6,9 +6,13 @@ const ActionTypes = { ...@@ -6,9 +6,13 @@ const ActionTypes = {
FOCUS_WINDOW: 'FOCUS_WINDOW', FOCUS_WINDOW: 'FOCUS_WINDOW',
SET_WORKSPACE_FULLSCREEN: 'SET_WORKSPACE_FULLSCREEN', SET_WORKSPACE_FULLSCREEN: 'SET_WORKSPACE_FULLSCREEN',
SET_WORKSPACE_VIEWPORT_POSITION: 'SET_WORKSPACE_VIEWPORT_POSITION',
TOGGLE_WORKSPACE_EXPOSE_MODE: 'TOGGLE_WORKSPACE_EXPOSE_MODE',
ADD_MANIFEST: 'ADD_MANIFEST', ADD_MANIFEST: 'ADD_MANIFEST',
ADD_WINDOW: 'ADD_WINDOW', ADD_WINDOW: 'ADD_WINDOW',
SET_CANVAS: 'SET_CANVAS', SET_CANVAS: 'SET_CANVAS',
UPDATE_WINDOW_POSITION: 'UPDATE_WINDOW_POSITION',
SET_WINDOW_SIZE: 'SET_WINDOW_SIZE',
REMOVE_WINDOW: 'REMOVE_WINDOW', REMOVE_WINDOW: 'REMOVE_WINDOW',
PICK_WINDOWING_SYSTEM: 'PICK_WINDOWING_SYSTEM', PICK_WINDOWING_SYSTEM: 'PICK_WINDOWING_SYSTEM',
REQUEST_MANIFEST: 'REQUEST_MANIFEST', REQUEST_MANIFEST: 'REQUEST_MANIFEST',
......
...@@ -26,7 +26,10 @@ export function addWindow(options) { ...@@ -26,7 +26,10 @@ export function addWindow(options) {
manifestId: null, manifestId: null,
rangeId: null, rangeId: null,
thumbnailNavigationPosition: 'bottom', // bottom by default in settings.js thumbnailNavigationPosition: 'bottom', // bottom by default in settings.js
xywh: [0, 0, 400, 400], width: 400,
height: 400,
x: 2700,
y: 2700,
companionWindowIds: [], companionWindowIds: [],
rotation: null, rotation: null,
view: 'single', view: 'single',
...@@ -138,3 +141,37 @@ export function setWindowThumbnailPosition(windowId, position) { ...@@ -138,3 +141,37 @@ export function setWindowThumbnailPosition(windowId, position) {
export function setWindowViewType(windowId, viewType) { export function setWindowViewType(windowId, viewType) {
return { type: ActionTypes.SET_WINDOW_VIEW_TYPE, windowId, viewType }; return { type: ActionTypes.SET_WINDOW_VIEW_TYPE, windowId, viewType };
} }
/**
* updateWindowPosition - action creator
*
* @param {String} windowId
* @param {Array} position
* @memberof ActionCreators
*/
export function updateWindowPosition(windowId, position) {
return {
type: ActionTypes.UPDATE_WINDOW_POSITION,
payload: {
windowId,
position,
},
};
}
/**
* setWindowSize - action creator
*
* @param {String} windowId
* @param {Object} size
* @memberof ActionCreators
*/
export function setWindowSize(windowId, size) {
return {
type: ActionTypes.SET_WINDOW_SIZE,
payload: {
windowId,
size,
},
};
}
import ActionTypes from './action-types'; import ActionTypes from './action-types';
/* eslint-disable import/prefer-default-export */
/** /**
* setWorkspaceFullscreen - action creator * setWorkspaceFullscreen - action creator
* *
...@@ -39,3 +38,33 @@ export function updateWorkspaceMosaicLayout(layout) { ...@@ -39,3 +38,33 @@ export function updateWorkspaceMosaicLayout(layout) {
export function setWorkspaceAddVisibility(isWorkspaceAddVisible) { export function setWorkspaceAddVisibility(isWorkspaceAddVisible) {
return { type: ActionTypes.SET_WORKSPACE_ADD_VISIBILITY, isWorkspaceAddVisible }; return { type: ActionTypes.SET_WORKSPACE_ADD_VISIBILITY, isWorkspaceAddVisible };
} }
/**
* setWorkspaceViewportPosition - action creator
*
* @param {Object} position
* @memberof ActionCreators
*/
export function setWorkspaceViewportPosition(position) {
return {
type: ActionTypes.SET_WORKSPACE_VIEWPORT_POSITION,
payload: {
position: {
x: position.x,
y: position.y,
},
},
};
}
/**
* toggleWorkspaceExposeMode - action creator
*
* @param {Object} position
* @memberof ActionCreators
*/
export function toggleWorkspaceExposeMode() {
return {
type: ActionTypes.TOGGLE_WORKSPACE_EXPOSE_MODE,
};
}
...@@ -55,6 +55,30 @@ export const windowsReducer = (state = {}, action) => { ...@@ -55,6 +55,30 @@ export const windowsReducer = (state = {}, action) => {
), ),
}, },
}; };
case ActionTypes.UPDATE_WINDOW_POSITION:
return {
...state,
[action.payload.windowId]: {
...state[action.payload.windowId],
x: action.payload.position.x,
y: action.payload.position.y,
},
};
case ActionTypes.SET_WINDOW_SIZE:
return {
...state,
[action.payload.windowId]: {
...state[action.payload.windowId],
width: action.payload.size.width,
height: action.payload.size.height,
x: action.payload.size.x,
y: action.payload.size.y,
},
};
case ActionTypes.NEXT_CANVAS:
return setCanvasIndex(state, action.windowId, currentIndex => currentIndex + 1);
case ActionTypes.PREVIOUS_CANVAS:
return setCanvasIndex(state, action.windowId, currentIndex => currentIndex - 1);
case ActionTypes.SET_CANVAS: case ActionTypes.SET_CANVAS:
return setCanvasIndex(state, action.windowId, currentIndex => action.canvasIndex); return setCanvasIndex(state, action.windowId, currentIndex => action.canvasIndex);
default: default:
......
...@@ -3,7 +3,16 @@ import ActionTypes from '../actions/action-types'; ...@@ -3,7 +3,16 @@ import ActionTypes from '../actions/action-types';
/** /**
* workspaceReducer * workspaceReducer
*/ */
export const workspaceReducer = (state = {}, action) => { export const workspaceReducer = (
state = { // we'll need to abstract this more, methinks.
viewportPosition: {
x: -2500,
y: -2500,
},
exposeModeOn: false,
},
action,
) => {
switch (action.type) { switch (action.type) {
case ActionTypes.FOCUS_WINDOW: case ActionTypes.FOCUS_WINDOW:
return { ...state, focusedWindowId: action.windowId }; return { ...state, focusedWindowId: action.windowId };
...@@ -15,6 +24,10 @@ export const workspaceReducer = (state = {}, action) => { ...@@ -15,6 +24,10 @@ export const workspaceReducer = (state = {}, action) => {
return { ...state, layout: action.layout }; return { ...state, layout: action.layout };
case ActionTypes.SET_WORKSPACE_ADD_VISIBILITY: case ActionTypes.SET_WORKSPACE_ADD_VISIBILITY:
return { ...state, isWorkspaceAddVisible: action.isWorkspaceAddVisible }; return { ...state, isWorkspaceAddVisible: action.isWorkspaceAddVisible };
case ActionTypes.SET_WORKSPACE_VIEWPORT_POSITION:
return { ...state, viewportPosition: action.payload.position };
case ActionTypes.TOGGLE_WORKSPACE_EXPOSE_MODE:
return { ...state, exposeModeOn: !state.exposeModeOn };
default: default:
return state; return state;
} }
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment