Skip to content
Snippets Groups Projects
Unverified Commit acecd915 authored by Chris Beer's avatar Chris Beer Committed by GitHub
Browse files

Merge pull request #1741 from ProjectMirador/1689-thumb-lazy-load

Implements ThumbnailNavigation image requesting with lazy loading
parents 2116584b bc3d4df6
Branches
Tags
No related merge requests found
import React from 'react';
import { shallow } from 'enzyme';
import IntersectionObserver from '@researchgate/react-intersection-observer';
import CanvasThumbnail from '../../../src/components/CanvasThumbnail';
describe('CanvasThumbnail', () => {
let wrapper;
beforeEach(() => {
wrapper = shallow(
<CanvasThumbnail
imageUrl="https://stacks.stanford.edu/image/iiif/sn904cj3429%2F12027000/full/193,/0/default.jpg"
/>,
);
});
it('renders properly', () => {
expect(wrapper.matchesElement(
<div>
<IntersectionObserver onChange={wrapper.instance().handleIntersection}>
<img alt="" height={150} />
</IntersectionObserver>
</div>,
)).toBe(true);
});
it('defaults using the placeholder image', () => {
expect(wrapper.find('img').props().src).toMatch(/data:image\/png;base64/);
});
it('when handleIntersection is called, loads the image', () => {
wrapper.instance().handleIntersection({ isIntersecting: true });
expect(wrapper.find('img').props().src).toMatch(/stacks/);
});
});
......@@ -55,6 +55,9 @@ describe('ThumbnailNavigation', () => {
expect(renderedGrid.find('.mirador-thumbnail-nav-container').first().prop('style').width).toEqual(108);
expect(renderedGrid.find('.mirador-thumbnail-nav-canvas').first().prop('style').width).toEqual(100);
});
it('renders canvas thumbnails', () => {
expect(renderedGrid.find('CanvasThumbnail').length).toBe(2);
});
it('Grid is set with expected props for scrolling alignment', () => {
expect(grid.props().scrollToAlignment).toBe('center');
expect(grid.props().scrollToColumn).toBe(1);
......
import manifesto from 'manifesto.js';
import ManifestoCanvas from '../../../src/lib/ManifestoCanvas';
import fixture from '../../fixtures/version-2/019.json';
describe('ManifestoCanvas', () => {
let instance;
beforeAll(() => {
instance = new ManifestoCanvas(
manifesto.create(fixture).getSequences()[0].getCanvases()[0],
);
});
describe('canonicalImageUri', () => {
it('calls manifestos method to return a canonical imageUri', () => {
expect(instance.canonicalImageUri).toEqual(
'https://stacks.stanford.edu/image/iiif/hg676jb4964%2F0380_796-44/full/5426,/0/default.jpg',
);
});
});
describe('aspectRatio', () => {
it('calculates a width / height aspectRatio', () => {
expect(instance.aspectRatio).toBeCloseTo(0.667);
});
});
describe('thumbnail', () => {
it('calculates a thumbnail image API request based off of height', () => {
expect(instance.thumbnail(100)).toEqual(
'https://stacks.stanford.edu/image/iiif/hg676jb4964%2F0380_796-44/full/66,/0/default.jpg',
);
});
it('defaults to using 150 as a height', () => {
expect(instance.thumbnail()).toEqual(
'https://stacks.stanford.edu/image/iiif/hg676jb4964%2F0380_796-44/full/100,/0/default.jpg',
);
});
});
});
......@@ -30,6 +30,7 @@
"dependencies": {
"@material-ui/core": "^3.9.0",
"@material-ui/icons": "^3.0.2",
"@researchgate/react-intersection-observer": "^0.7.4",
"classnames": "^2.2.6",
"css-ns": "^1.2.2",
"deepmerge": "^3.1.0",
......
......@@ -18,4 +18,17 @@ global.navigator = {
userAgent: 'node.js',
};
/* eslint-disable require-jsdoc, class-methods-use-this */
class IntersectionObserverPolyfill {
observe() {
}
disconnect() {
}
}
/* eslint-enable require-jsdoc, class-methods-use-this */
global.IntersectionObserver = IntersectionObserverPolyfill;
global.Image = window.Image;
Enzyme.configure({ adapter: new Adapter() });
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import IntersectionObserver from '@researchgate/react-intersection-observer';
/**
* Uses InteractionObserver to "lazy" load canvas thumbnails that are in view.
*/
export default class CanvasThumbnail extends Component {
/**
*/
constructor(props) {
super(props);
this.state = { loaded: false, image: null };
this.handleIntersection = this.handleIntersection.bind(this);
}
/**
* Handles the intersection (visibility) of a given thumbnail, by requesting
* the image and then updating the state.
*/
handleIntersection(event) {
const { imageUrl } = this.props;
const { loaded } = this.state;
if (loaded) return;
const image = new Image();
image.src = imageUrl;
this.setState({
loaded: true,
image,
});
}
/**
*/
render() {
const { height } = this.props;
const { loaded, image } = this.state;
return (
<div>
<IntersectionObserver onChange={this.handleIntersection}>
<img
alt=""
src={loaded ? image.src : CanvasThumbnail.defaultImgPlaceholder}
height={height}
width="100%"
/>
</IntersectionObserver>
</div>
);
}
}
// Transparent "gray"
CanvasThumbnail.defaultImgPlaceholder = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mMMDQmtBwADgwF/Op8FmAAAAABJRU5ErkJggg==';
CanvasThumbnail.propTypes = {
imageUrl: PropTypes.string,
height: PropTypes.number,
};
CanvasThumbnail.defaultProps = {
imageUrl: null,
height: 150,
};
......@@ -4,6 +4,8 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import AutoSizer from 'react-virtualized/dist/commonjs/AutoSizer';
import Grid from 'react-virtualized/dist/commonjs/Grid';
import CanvasThumbnail from './CanvasThumbnail';
import ManifestoCanvas from '../lib/ManifestoCanvas';
import miradorWithPlugins from '../lib/miradorWithPlugins';
import * as actions from '../state/actions';
import ns from '../config/css-ns';
......@@ -59,7 +61,7 @@ export class ThumbnailNavigation extends Component {
columnIndex, key, style,
} = options;
const {
window, setCanvas,
window, setCanvas, config,
} = this.props;
const { canvases } = this.state;
const canvas = canvases[columnIndex];
......@@ -77,8 +79,13 @@ export class ThumbnailNavigation extends Component {
width: style.width - 8,
}}
className={ns(['thumbnail-nav-canvas', `thumbnail-nav-canvas-${canvas.index}`, this.currentCanvasClass(canvas.index)])}
>
<CanvasThumbnail
imageUrl={new ManifestoCanvas(canvas).thumbnail(config.thumbnailNavigation.height)}
height={config.thumbnailNavigation.height}
/>
</div>
</div>
);
}
......@@ -89,9 +96,8 @@ export class ThumbnailNavigation extends Component {
calculateScaledWidth(options) {
const { config } = this.props;
const { canvases } = this.state;
const canvas = canvases[options.index];
const aspectRatio = canvas.getHeight() / canvas.getWidth();
return Math.floor(config.thumbnailNavigation.height / aspectRatio) + 8;
const canvas = new ManifestoCanvas(canvases[options.index]);
return Math.floor(config.thumbnailNavigation.height * canvas.aspectRatio) + 8;
}
/**
......
/**
* ManifestoCanvas - adds additional, testable logic around Manifesto's Canvas
* https://iiif-commons.github.io/manifesto/classes/_canvas_.manifesto.canvas.html
*/
export default class ManifestoCanvas {
/**
* @param {ManifestoCanvas} canvas
*/
constructor(canvas) {
this.canvas = canvas;
}
/**
*/
get canonicalImageUri() {
return this.canvas.getCanonicalImageUri();
}
/**
*/
get aspectRatio() {
return this.canvas.getWidth() / this.canvas.getHeight();
}
/**
* Creates a canonical image request for a thumb
* @param {Number} height
*/
thumbnail(height = 150) {
const width = Math.floor(height * this.aspectRatio);
return this.canonicalImageUri.replace(/\/full\/.*\/0\//, `/full/${width},/0/`);
}
}
......@@ -80,7 +80,6 @@ body {
&-thumb-navigation {
.mirador-thumbnail-nav-canvas {
background-color: $gray;
border: 1px solid $black;
color: $white;
cursor: pointer;
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment