Skip to content
Snippets Groups Projects
Unverified Commit 94e4e551 authored by Jessie Keck's avatar Jessie Keck Committed by GitHub
Browse files

Merge pull request #1933 from ProjectMirador/1870-book-thumbs

Sets up ThumbnailNavigation for BookView
parents 793fb68c 7f17c6e6
No related branches found
No related tags found
No related merge requests found
...@@ -12,7 +12,7 @@ describe('Thumbnail navigation', () => { ...@@ -12,7 +12,7 @@ describe('Thumbnail navigation', () => {
)); ));
expect(Object.values(windows)[0].canvasIndex).toBe(2); // test harness in index.html starts at 2 expect(Object.values(windows)[0].canvasIndex).toBe(2); // test harness in index.html starts at 2
await page.waitFor(1000); await page.waitFor(1000);
await expect(page).toClick('.mirador-thumbnail-nav-canvas-1'); await expect(page).toClick('.mirador-thumbnail-nav-canvas-1 img');
await expect(page).toMatchElement('.mirador-thumbnail-nav-canvas-1.mirador-current-canvas', { timeout: 1500 }); await expect(page).toMatchElement('.mirador-thumbnail-nav-canvas-1.mirador-current-canvas', { timeout: 1500 });
windows = await page.evaluate(() => ( windows = await page.evaluate(() => (
miradorInstance.store.getState().windows miradorInstance.store.getState().windows
......
...@@ -16,11 +16,11 @@ describe('CanvasThumbnail', () => { ...@@ -16,11 +16,11 @@ describe('CanvasThumbnail', () => {
it('renders properly', () => { it('renders properly', () => {
expect(wrapper.matchesElement( expect(wrapper.matchesElement(
<div> <>
<IntersectionObserver onChange={wrapper.instance().handleIntersection}> <IntersectionObserver onChange={wrapper.instance().handleIntersection}>
<img alt="" /> <img alt="" />
</IntersectionObserver> </IntersectionObserver>
</div>, </>,
)).toBe(true); )).toBe(true);
}); });
......
...@@ -3,8 +3,27 @@ import { shallow } from 'enzyme'; ...@@ -3,8 +3,27 @@ import { shallow } from 'enzyme';
import Grid from 'react-virtualized/dist/commonjs/Grid'; import Grid from 'react-virtualized/dist/commonjs/Grid';
import manifesto from 'manifesto.js'; import manifesto from 'manifesto.js';
import ThumbnailNavigation from '../../../src/components/ThumbnailNavigation'; import ThumbnailNavigation from '../../../src/components/ThumbnailNavigation';
import CanvasGroupings from '../../../src/lib/CanvasGroupings';
import manifestJson from '../../fixtures/version-2/019.json'; import manifestJson from '../../fixtures/version-2/019.json';
/** create wrapper */
function createWrapper(props) {
return shallow(
<ThumbnailNavigation
canvasGroupings={
new CanvasGroupings(manifesto.create(manifestJson).getSequences()[0].getCanvases())
}
window={{
id: 'foobar',
canvasIndex: 1,
thumbnailNavigationPosition: 'bottom',
}}
config={{ thumbnailNavigation: { height: 150 } }}
{...props}
/>,
);
}
describe('ThumbnailNavigation', () => { describe('ThumbnailNavigation', () => {
let wrapper; let wrapper;
let setCanvas; let setCanvas;
...@@ -17,20 +36,7 @@ describe('ThumbnailNavigation', () => { ...@@ -17,20 +36,7 @@ describe('ThumbnailNavigation', () => {
Grid.prototype._scrollingContainer = jest.fn( // eslint-disable-line no-underscore-dangle Grid.prototype._scrollingContainer = jest.fn( // eslint-disable-line no-underscore-dangle
() => ({ scrollLeft: 0 }), () => ({ scrollLeft: 0 }),
); );
wrapper = shallow( wrapper = createWrapper({ setCanvas });
<ThumbnailNavigation
canvases={
manifesto.create(manifestJson).getSequences()[0].getCanvases()
}
window={{
id: 'foobar',
canvasIndex: 1,
thumbnailNavigationPosition: 'bottom',
}}
config={{ thumbnailNavigation: { height: 150 } }}
setCanvas={setCanvas}
/>,
);
grid = wrapper.find('AutoSizer') grid = wrapper.find('AutoSizer')
.dive() .dive()
.find('Grid'); .find('Grid');
...@@ -39,7 +45,7 @@ describe('ThumbnailNavigation', () => { ...@@ -39,7 +45,7 @@ describe('ThumbnailNavigation', () => {
it('renders the component', () => { it('renders the component', () => {
expect(wrapper.find('.mirador-thumb-navigation').length).toBe(1); expect(wrapper.find('.mirador-thumb-navigation').length).toBe(1);
}); });
it('renders li elements based off of number of canvases', () => { it('renders containers based off of number of canvases', () => {
expect(renderedGrid.find('.mirador-thumbnail-nav-canvas').length).toBe(3); expect(renderedGrid.find('.mirador-thumbnail-nav-canvas').length).toBe(3);
}); });
it('sets a mirador-current-canvas class on current canvas', () => { it('sets a mirador-current-canvas class on current canvas', () => {
...@@ -59,6 +65,34 @@ describe('ThumbnailNavigation', () => { ...@@ -59,6 +65,34 @@ describe('ThumbnailNavigation', () => {
it('Grid is set with expected props for scrolling alignment', () => { it('Grid is set with expected props for scrolling alignment', () => {
expect(grid.props().scrollToAlignment).toBe('center'); expect(grid.props().scrollToAlignment).toBe('center');
expect(grid.props().scrollToColumn).toBe(1); expect(grid.props().scrollToColumn).toBe(1);
expect(grid.props().columnIndex).toBe(1); });
it('has a ref set used to reset on view change', () => {
expect(wrapper.instance().gridRef).not.toBe(null);
});
it('renders containers based off of canvas groupings ', () => {
wrapper = createWrapper({
setCanvas,
canvasGroupings: new CanvasGroupings(manifesto.create(manifestJson).getSequences()[0].getCanvases(), 'book'),
});
grid = wrapper.find('AutoSizer')
.dive()
.find('Grid');
renderedGrid = grid.dive();
expect(renderedGrid.find('.mirador-thumbnail-nav-canvas').length).toBe(2);
expect(renderedGrid.find('CanvasThumbnail').length).toBe(3);
expect(wrapper.instance().scrollToColumn()).toBe(1);
});
it('triggers a recomputeGridSize on view change', () => {
const mockRecompute = jest.fn();
wrapper.instance().gridRef = { current: { recomputeGridSize: mockRecompute } };
wrapper.setProps({
window: {
id: 'foobar',
canvasIndex: 1,
thumbnailNavigationPosition: 'bottom',
view: 'book',
},
});
expect(mockRecompute).toHaveBeenCalled();
}); });
}); });
...@@ -111,4 +111,14 @@ describe('WindowViewer', () => { ...@@ -111,4 +111,14 @@ describe('WindowViewer', () => {
}); });
}); });
}); });
it('when view type changes', () => {
expect(wrapper.instance().canvasGroupings.groupings().length).toEqual(3);
wrapper.setProps({
window: {
canvasIndex: 0,
view: 'book',
},
});
expect(wrapper.instance().canvasGroupings.groupings().length).toEqual(2);
});
}); });
import CanvasGroupings from '../../../src/lib/CanvasGroupings';
describe('CanvasGroupings', () => {
describe('constructor', () => {
it('sets canvases and viewType', () => {
const subject = new CanvasGroupings([null, null], 'book');
expect(subject.viewType).toEqual('book');
expect(subject.canvases.length).toEqual(2);
});
it('viewType default is single', () => {
const subject = new CanvasGroupings([null, null]);
expect(subject.viewType).toEqual('single');
});
});
describe('groupings', () => {
describe('single', () => {
it('creates an array of arrays of the canvases', () => {
const subject = new CanvasGroupings([null, null, null, null]);
expect(subject.groupings().length).toEqual(4);
expect(subject.groupings()[0]).toEqual(expect.arrayContaining([null]));
});
});
describe('book', () => {
let subject;
beforeEach(() => {
subject = new CanvasGroupings([0, 1, 2, 3], 'book');
});
it('creates an array of groupings of the canvases', () => {
expect(subject.groupings().length).toEqual(3);
});
it('first grouping has only 1 canvas', () => {
expect(subject.groupings()[0]).toEqual([0]);
});
it('second grouping has 2 canvases', () => {
expect(subject.groupings()[1]).toEqual([1, 2]);
});
});
});
describe('getCanvases', () => {
describe('single', () => {
it('selects by index', () => {
const subject = new CanvasGroupings([0, 1, 2, 3]);
expect(subject.getCanvases(2)).toEqual([2]);
});
});
describe('book', () => {
let subject;
beforeEach(() => {
subject = new CanvasGroupings([0, 1, 2, 3], 'book');
});
it('selects by index / 2', () => {
expect(subject.getCanvases(2)).toEqual([1, 2]);
});
});
});
});
...@@ -40,7 +40,7 @@ export default class CanvasThumbnail extends Component { ...@@ -40,7 +40,7 @@ export default class CanvasThumbnail extends Component {
const { loaded, image } = this.state; const { loaded, image } = this.state;
const imgStyle = { height, width: '100%', ...style }; const imgStyle = { height, width: '100%', ...style };
return ( return (
<div> <>
<IntersectionObserver onChange={this.handleIntersection}> <IntersectionObserver onChange={this.handleIntersection}>
<img <img
alt="" alt=""
...@@ -51,7 +51,7 @@ export default class CanvasThumbnail extends Component { ...@@ -51,7 +51,7 @@ export default class CanvasThumbnail extends Component {
style={imgStyle} style={imgStyle}
/> />
</IntersectionObserver> </IntersectionObserver>
</div> </>
); );
} }
} }
......
...@@ -17,21 +17,33 @@ class ThumbnailNavigation extends Component { ...@@ -17,21 +17,33 @@ class ThumbnailNavigation extends Component {
this.cellRenderer = this.cellRenderer.bind(this); this.cellRenderer = this.cellRenderer.bind(this);
this.calculateScaledWidth = this.calculateScaledWidth.bind(this); this.calculateScaledWidth = this.calculateScaledWidth.bind(this);
this.gridRef = React.createRef();
}
/**
* If the view has changed and the thumbnailNavigation is open, recompute all
* of the grids
*/
componentDidUpdate(prevProps) {
const { window } = this.props;
if (prevProps.window.view !== window.view && window.thumbnailNavigationPosition !== 'off') {
this.gridRef.current.recomputeGridSize();
}
} }
/** /**
* Determines whether the current index is the rendered canvas, providing * Determines whether the current index is the rendered canvas, providing
* a useful class. * a useful class.
*/ */
currentCanvasClass(canvasIndex) { currentCanvasClass(canvasIndices) {
const { window } = this.props; const { window } = this.props;
if (window.canvasIndex === canvasIndex) return 'current-canvas'; if (canvasIndices.includes(window.canvasIndex)) return 'current-canvas';
return ''; return '';
} }
/** /**
* Renders a given "cell" for a react-virtualized Grid. Right now this is a * Renders a given "cell" for a react-virtualized Grid. This is a grouping of
* "canvas" but in the future for paged items, would be connected canvases. * canvases.
* https://github.com/bvaughn/react-virtualized/blob/master/docs/Grid.md * https://github.com/bvaughn/react-virtualized/blob/master/docs/Grid.md
*/ */
cellRenderer(options) { cellRenderer(options) {
...@@ -39,9 +51,9 @@ class ThumbnailNavigation extends Component { ...@@ -39,9 +51,9 @@ class ThumbnailNavigation extends Component {
columnIndex, key, style, columnIndex, key, style,
} = options; } = options;
const { const {
window, setCanvas, config, canvases, window, setCanvas, config, canvasGroupings,
} = this.props; } = this.props;
const canvas = canvases[columnIndex]; const currentGroupings = canvasGroupings.groupings()[columnIndex];
return ( return (
<div <div
key={key} key={key}
...@@ -52,33 +64,57 @@ class ThumbnailNavigation extends Component { ...@@ -52,33 +64,57 @@ class ThumbnailNavigation extends Component {
style={{ style={{
width: style.width - 8, width: style.width - 8,
}} }}
className={ns(['thumbnail-nav-canvas', `thumbnail-nav-canvas-${canvas.index}`, this.currentCanvasClass(canvas.index)])} className={ns(['thumbnail-nav-canvas', `thumbnail-nav-canvas-${columnIndex}`, this.currentCanvasClass(currentGroupings.map(canvas => canvas.index))])}
>
{currentGroupings.map((canvas, i) => (
<div
key={canvas.index}
style={{ position: 'absolute', left: (style.width - 8) * i / 2, top: 2 }}
> >
<CanvasThumbnail <CanvasThumbnail
onClick={() => setCanvas(window.id, canvas.index)} onClick={() => setCanvas(window.id, currentGroupings[0].index)}
imageUrl={new ManifestoCanvas(canvas).thumbnail(config.thumbnailNavigation.height)} imageUrl={new ManifestoCanvas(canvas).thumbnail(config.thumbnailNavigation.height)}
height={config.thumbnailNavigation.height} height={config.thumbnailNavigation.height}
/> />
</div> </div>
))}
</div>
</div> </div>
); );
} }
/** /**
* calculateScaledWidth - calculates the scaled width of a column for a Grid * calculateScaledWidth - calculates the scaled width of a column for a Grid
* in this simple case, a column == canvas. * in a simple case, a column == canvas. In a book view, a group (or two)
* canvases
*/ */
calculateScaledWidth(options) { calculateScaledWidth(options) {
const { config, canvases } = this.props; const { config, canvasGroupings } = this.props;
const canvas = new ManifestoCanvas(canvases[options.index]); return canvasGroupings
return Math.floor(config.thumbnailNavigation.height * canvas.aspectRatio) + 8; .getCanvases(options.index)
.map(canvas => new ManifestoCanvas(canvas).aspectRatio)
.reduce((acc, current) => acc + Math.floor(config.thumbnailNavigation.height * current), 8);
}
/**
* In book view, this is halved to represent the proxy between the "canvasIndex"
* and the columnIndex (in this case the index of grouped canvases)
*/
scrollToColumn() {
const { window } = this.props;
switch (window.view) {
case 'book':
return Math.ceil(window.canvasIndex / 2);
default:
return window.canvasIndex;
}
} }
/** /**
* Renders things * Renders things
*/ */
render() { render() {
const { config, window, canvases } = this.props; const { config, window, canvasGroupings } = this.props;
if (window.thumbnailNavigationPosition === 'off') { if (window.thumbnailNavigationPosition === 'off') {
return <></>; return <></>;
} }
...@@ -94,15 +130,15 @@ class ThumbnailNavigation extends Component { ...@@ -94,15 +130,15 @@ class ThumbnailNavigation extends Component {
{({ height, width }) => ( {({ height, width }) => (
<Grid <Grid
cellRenderer={this.cellRenderer} cellRenderer={this.cellRenderer}
columnCount={canvases.length} columnCount={canvasGroupings.groupings().length}
columnIndex={window.canvasIndex}
columnWidth={this.calculateScaledWidth} columnWidth={this.calculateScaledWidth}
height={config.thumbnailNavigation.height} height={config.thumbnailNavigation.height}
rowCount={1} rowCount={1}
rowHeight={config.thumbnailNavigation.height} rowHeight={config.thumbnailNavigation.height}
scrollToAlignment="center" scrollToAlignment="center"
scrollToColumn={window.canvasIndex} scrollToColumn={this.scrollToColumn()}
width={width} width={width}
ref={this.gridRef}
/> />
)} )}
</AutoSizer> </AutoSizer>
...@@ -113,7 +149,7 @@ class ThumbnailNavigation extends Component { ...@@ -113,7 +149,7 @@ class ThumbnailNavigation extends Component {
ThumbnailNavigation.propTypes = { ThumbnailNavigation.propTypes = {
config: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types config: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
canvases: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types canvasGroupings: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
setCanvas: PropTypes.func.isRequired, setCanvas: PropTypes.func.isRequired,
window: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types window: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
}; };
......
...@@ -44,6 +44,7 @@ class WindowSideBarCanvasPanel extends Component { ...@@ -44,6 +44,7 @@ class WindowSideBarCanvasPanel extends Component {
<ListItem <ListItem
key={canvas.id} key={canvas.id}
> >
<div>
<CanvasThumbnail <CanvasThumbnail
className={classNames(classes.clickable)} className={classNames(classes.clickable)}
isValid={isValid} isValid={isValid}
...@@ -55,6 +56,7 @@ class WindowSideBarCanvasPanel extends Component { ...@@ -55,6 +56,7 @@ class WindowSideBarCanvasPanel extends Component {
width: isValid ? WindowSideBarCanvasPanel.calculateScaledWidth(config.canvasNavigation.height, validationCanvas.aspectRatio) : 'auto', width: isValid ? WindowSideBarCanvasPanel.calculateScaledWidth(config.canvasNavigation.height, validationCanvas.aspectRatio) : 'auto',
}} }}
/> />
</div>
<Typography <Typography
className={classNames(classes.clickable, classes.label)} className={classNames(classes.clickable, classes.label)}
onClick={onClick} onClick={onClick}
......
...@@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; ...@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import OSDViewer from '../containers/OpenSeadragonViewer'; import OSDViewer from '../containers/OpenSeadragonViewer';
import ViewerNavigation from '../containers/ViewerNavigation'; import ViewerNavigation from '../containers/ViewerNavigation';
import ManifestoCanvas from '../lib/ManifestoCanvas'; import ManifestoCanvas from '../lib/ManifestoCanvas';
import CanvasGroupings from '../lib/CanvasGroupings';
/** /**
* Represents a WindowViewer in the mirador workspace. Responsible for mounting * Represents a WindowViewer in the mirador workspace. Responsible for mounting
...@@ -15,8 +16,9 @@ class WindowViewer extends Component { ...@@ -15,8 +16,9 @@ class WindowViewer extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
const { manifest } = this.props; const { manifest, window } = this.props;
this.canvases = manifest.manifestation.getSequences()[0].getCanvases(); this.canvases = manifest.manifestation.getSequences()[0].getCanvases();
this.canvasGroupings = new CanvasGroupings(this.canvases, window.view);
} }
/** /**
...@@ -46,6 +48,10 @@ class WindowViewer extends Component { ...@@ -46,6 +48,10 @@ class WindowViewer extends Component {
fetchInfoResponse(new ManifestoCanvas(canvas).imageInformationUri); fetchInfoResponse(new ManifestoCanvas(canvas).imageInformationUri);
}); });
} }
// If the view changes, create a new instance
if (prevProps.window.view !== window.view) {
this.canvasGroupings = new CanvasGroupings(this.canvases, window.view);
}
} }
/** /**
...@@ -62,28 +68,12 @@ class WindowViewer extends Component { ...@@ -62,28 +68,12 @@ class WindowViewer extends Component {
} }
/** /**
* Figures out how many and what canvases to present to a user based off of * Uses CanvasGroupings to figure out how many and what canvases to present to
* the view, number of canvases, and canvasIndex. * a user based off of the view, number of canvases, and canvasIndex.
*/ */
currentCanvases() { currentCanvases() {
const { window } = this.props; const { window } = this.props;
switch (window.view) { return this.canvasGroupings.getCanvases(window.canvasIndex);
case 'book':
if ( // FIXME: There is probably better logic floating around out there to determine this.
this.canvases.length > 0 // when there are canvases present
&& window.canvasIndex !== 0 // when the first canvas is not selected
&& window.canvasIndex + 1 !== this.canvases.length // when the last canvas is not selected
) {
// For an even canvas
if (window.canvasIndex % 2 === 0) {
return [this.canvases[window.canvasIndex - 1], this.canvases[window.canvasIndex]];
}
return [this.canvases[window.canvasIndex], this.canvases[window.canvasIndex + 1]];
}
return [this.canvases[window.canvasIndex]];
default:
return [this.canvases[window.canvasIndex]];
}
} }
/** /**
......
import { compose } from 'redux'; import { compose } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import miradorWithPlugins from '../lib/miradorWithPlugins'; import miradorWithPlugins from '../lib/miradorWithPlugins';
import CanvasGroupings from '../lib/CanvasGroupings';
import * as actions from '../state/actions'; import * as actions from '../state/actions';
import ThumbnailNavigation from '../components/ThumbnailNavigation'; import ThumbnailNavigation from '../components/ThumbnailNavigation';
import { getManifestCanvases } from '../state/selectors'; import { getManifestCanvases } from '../state/selectors';
...@@ -9,8 +10,8 @@ import { getManifestCanvases } from '../state/selectors'; ...@@ -9,8 +10,8 @@ import { getManifestCanvases } from '../state/selectors';
* @memberof ThumbnailNavigation * @memberof ThumbnailNavigation
* @private * @private
*/ */
const mapStateToProps = ({ config }, { manifest }) => ({ const mapStateToProps = ({ config }, { manifest, window }) => ({
canvases: getManifestCanvases(manifest), canvasGroupings: new CanvasGroupings(getManifestCanvases(manifest), window.view),
config, config,
}); });
......
/**
*
*/
export default class CanvasGroupings {
/**
*/
constructor(canvases, viewType = 'single') {
this.canvases = canvases;
this.viewType = viewType;
this._groupings = null; // eslint-disable-line no-underscore-dangle
}
/**
*/
getCanvases(index) {
switch (this.viewType) {
case 'book':
return this.groupings()[Math.ceil(index / 2)];
default:
return this.groupings()[index];
}
}
/**
* Groups a set of canvases based on the view type. Single, is just an array
* of canvases, while book view creates pairs.
*/
groupings() {
if (this._groupings) { // eslint-disable-line no-underscore-dangle
return this._groupings; // eslint-disable-line no-underscore-dangle
}
if (this.viewType === 'single') {
return this.canvases.map(canvas => [canvas]);
}
const groupings = [];
this.canvases.forEach((canvas, i) => {
if (i === 0) {
groupings.push([canvas]);
return;
}
// Odd page
if (i % 2 !== 0) {
groupings.push([canvas]);
} else {
// Even page
groupings[Math.ceil(i / 2)].push(canvas);
}
});
this._groupings = groupings; // eslint-disable-line no-underscore-dangle
return groupings;
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment