Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions __tests__/fixtures/version-3/text-pdf.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"@context": "http://iiif.io/api/presentation/3/context.json",
"id": "https://iiif.io/api/cookbook/recipe/0001-text-pdf/manifest.json",
"type": "Manifest",
"label": {
"en": [
"Simplest Text Example 1"
]
},
"items": [
{
"id": "https://iiif.io/api/cookbook/recipe/0001-text-pdf/canvas",
"type": "Canvas",
"items": [
{
"id": "https://iiif.io/api/cookbook/recipe/0001-text-pdf/canvas/page",
"type": "AnnotationPage",
"items": [
{
"id": "https://iiif.io/api/cookbook/recipe/0001-text-pdf/canvas/page/annotation",
"type": "Annotation",
"motivation": "painting",
"body": {
"id": "https://fixtures.iiif.io/other/UCLA/kabuki_ezukushi_rtl.pdf",
"type": "Text",
"format": "application/pdf"
},
"target": "https://iiif.io/api/cookbook/recipe/0001-text-pdf/canvas/page"
}
]
}
]
}
]
}
29 changes: 29 additions & 0 deletions __tests__/src/components/PrimaryWindow.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { render, screen, waitFor } from '@tests/utils/test-utils';
import { Resource } from 'manifesto.js';
import textManifest from '../../fixtures/version-3/text-pdf.json';
import { PrimaryWindow } from '../../../src/components/PrimaryWindow';

/** create wrapper */
Expand Down Expand Up @@ -48,4 +50,31 @@ describe('PrimaryWindow', () => {
);
await screen.findByRole('button', { accessibleName: 'show collection' });
});
it('should render a TextViewer when window has textResources but not audio, video or image', async () => {
render(<div id="xyz" />);
const manifests = {};
const manifestId = 'https://iiif.io/api/cookbook/recipe/0001-text-pdf/manifest.json';
manifests[manifestId] = {
id: manifestId,
json: textManifest,
};
const textResources = [true]; // this is a flag; the actual value will be given by a state selector against the preloaded state

const xyz = {
manifestId: 'https://iiif.io/api/cookbook/recipe/0001-text-pdf/manifest.json',
visibleCanvases: ['https://iiif.io/api/cookbook/recipe/0001-text-pdf/canvas'],
};

render(
<PrimaryWindow
classes={{}}
textResources={textResources}
windowId="xyz"
/>,
{ preloadedState: { manifests, windows: { xyz } } },
);
await waitFor(() => {
expect(document.querySelector('source:nth-of-type(1)')).toHaveAttribute('type', 'application/pdf'); // eslint-disable-line testing-library/no-node-access
});
});
});
47 changes: 47 additions & 0 deletions __tests__/src/components/TextViewer.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { render, screen } from '@tests/utils/test-utils';
import { TextViewer } from '../../../src/components/TextViewer';

/** create wrapper */
function createWrapper(props, suspenseFallback) {
return render(
<TextViewer
classes={{}}
textOptions={{ crossOrigin: 'anonymous', 'data-testid': 'text' }}
{...props}
/>,
);
}

describe('TextViewer', () => {
describe('render', () => {
it('textResources as source elements', () => {
createWrapper({
textResources: [
{ getFormat: () => 'application/pdf', getType: () => 'Text', id: 1 },
],
windowId: 'a',
}, true);
const text = screen.getByTestId('text');
expect(text.querySelector('source:nth-of-type(1)')).toHaveAttribute('type', 'application/pdf'); // eslint-disable-line testing-library/no-node-access
});
it('passes through configurable options', () => {
createWrapper({
textResources: [
{ getFormat: () => 'application/pdf', getType: () => 'Text', id: 1 },
],
windowId: 'a',
}, true);
expect(screen.getByTestId('text')).toHaveAttribute('crossOrigin', 'anonymous');
});
it('canvas navigation', () => {
createWrapper({
textResources: [
{ getFormat: () => 'application/pdf', getType: () => 'Text', id: 1 },
],
windowId: 'a',
}, true);
const text = screen.getByTestId('text');
expect(text.querySelector('.mirador-canvas-nav')).toBeDefined(); // eslint-disable-line testing-library/no-node-access
});
});
});
9 changes: 9 additions & 0 deletions __tests__/src/lib/MiradorCanvas.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import otherContentStringsFixture from '../../fixtures/version-2/BibliographicRe
import fragmentFixture from '../../fixtures/version-2/hamilton.json';
import fragmentFixtureV3 from '../../fixtures/version-3/hamilton.json';
import audioFixture from '../../fixtures/version-3/0002-mvm-audio.json';
import textFixture from '../../fixtures/version-3/text-pdf.json';
import videoFixture from '../../fixtures/version-3/0015-start.json';
import videoWithAnnoCaptions from '../../fixtures/version-3/video_with_annotation_captions.json';

Expand Down Expand Up @@ -133,4 +134,12 @@ describe('MiradorCanvas', () => {
expect(instance.v3VttContent.length).toEqual(1);
});
});
describe('textResources', () => {
it('returns text', () => {
instance = new MiradorCanvas(
Utils.parseManifest(textFixture).getSequences()[0].getCanvases()[0],
);
expect(instance.textResources.length).toEqual(1);
});
});
});
24 changes: 24 additions & 0 deletions __tests__/src/selectors/canvases.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import manifestFixture019 from '../../fixtures/version-2/019.json';
import minimumRequired from '../../fixtures/version-2/minimumRequired.json';
import minimumRequired3 from '../../fixtures/version-3/minimumRequired.json';
import audioFixture from '../../fixtures/version-3/0002-mvm-audio.json';
import textFixture from '../../fixtures/version-3/text-pdf.json';
import videoFixture from '../../fixtures/version-3/0015-start.json';
import videoWithAnnoCaptions from '../../fixtures/version-3/video_with_annotation_captions.json';
import settings from '../../../src/config/settings';
Expand All @@ -15,6 +16,7 @@ import {
getCanvasLabel,
selectInfoResponse,
getVisibleCanvasNonTiledResources,
getVisibleCanvasTextResources,
getVisibleCanvasVideoResources,
getVisibleCanvasAudioResources,
getVisibleCanvasCaptions,
Expand Down Expand Up @@ -462,4 +464,26 @@ describe('getVisibleCanvasNonTiledResources', () => {
expect(getVisibleCanvasAudioResources(state, { windowId: 'a' })[0].id).toBe('https://fixtures.iiif.io/audio/indiana/mahler-symphony-3/CD1/medium/128Kbps.mp4');
});
});

describe('getVisibleCanvasTextResources', () => {
it('returns canvases resources', () => {
const state = {
manifests: {
'https://iiif.io/api/cookbook/recipe/0001-text-pdf/manifest.json': {
id: 'https://iiif.io/api/cookbook/recipe/0001-text-pdf/manifest.json',
json: textFixture,
},
},
windows: {
a: {
manifestId: 'https://iiif.io/api/cookbook/recipe/0001-text-pdf/manifest.json',
visibleCanvases: [
'https://iiif.io/api/cookbook/recipe/0001-text-pdf/canvas',
],
},
},
};
expect(getVisibleCanvasTextResources(state, { windowId: 'a' })[0].id).toBe('https://fixtures.iiif.io/other/UCLA/kabuki_ezukushi_rtl.pdf');
});
});
});
20 changes: 16 additions & 4 deletions src/components/PrimaryWindow.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const GalleryView = lazy(() => import('../containers/GalleryView'));
const SelectCollection = lazy(() => import('../containers/SelectCollection'));
const WindowViewer = lazy(() => import('../containers/WindowViewer'));
const VideoViewer = lazy(() => import('../containers/VideoViewer'));
const TextViewer = lazy(() => import('../containers/TextViewer'));

GalleryView.displayName = 'GalleryView';
SelectCollection.displayName = 'SelectCollection';
Expand All @@ -25,8 +26,8 @@ const Root = styled('div', { name: 'PrimaryWindow', slot: 'root' })(() => ({

/** */
const TypeSpecificViewer = ({
audioResources = [], isCollection = false,
isFetching = false, videoResources = [], view = undefined, windowId,
audioResources = [], isCollection = false, isFetching = false, textResources = [],
videoResources = [], view = undefined, windowId,
}) => {
if (isCollection) {
return (
Expand Down Expand Up @@ -57,6 +58,13 @@ const TypeSpecificViewer = ({
/>
);
}
if (textResources.length > 0) {
return (
<TextViewer
windowId={windowId}
/>
);
}
return (
<WindowViewer
windowId={windowId}
Expand All @@ -70,6 +78,7 @@ TypeSpecificViewer.propTypes = {
audioResources: PropTypes.arrayOf(PropTypes.object), // eslint-disable-line react/forbid-prop-types
isCollection: PropTypes.bool,
isFetching: PropTypes.bool,
textResources: PropTypes.arrayOf(PropTypes.object), // eslint-disable-line react/forbid-prop-types
videoResources: PropTypes.arrayOf(PropTypes.object), // eslint-disable-line react/forbid-prop-types
view: PropTypes.string,
windowId: PropTypes.string.isRequired,
Expand All @@ -80,13 +89,15 @@ TypeSpecificViewer.propTypes = {
* window. Right now this differentiates between a Image, Video, or Audio viewer.
*/
export function PrimaryWindow({
audioResources = undefined, isCollection = false, isFetching = false, videoResources = undefined,
view = undefined, windowId, isCollectionDialogVisible = false, children = null, className = undefined,
audioResources = undefined, children = null, className = undefined, isCollection = false,
isCollectionDialogVisible = false, isFetching = false, textResources = undefined, videoResources = undefined,
view = undefined, windowId,
}) {
const viewerProps = {
audioResources,
isCollection,
isFetching,
textResources,
videoResources,
view,
windowId,
Expand All @@ -111,6 +122,7 @@ PrimaryWindow.propTypes = {
isCollection: PropTypes.bool,
isCollectionDialogVisible: PropTypes.bool,
isFetching: PropTypes.bool,
textResources: PropTypes.arrayOf(PropTypes.object), // eslint-disable-line react/forbid-prop-types
videoResources: PropTypes.arrayOf(PropTypes.object), // eslint-disable-line react/forbid-prop-types
view: PropTypes.string,
windowId: PropTypes.string.isRequired,
Expand Down
37 changes: 37 additions & 0 deletions src/components/TextViewer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { styled } from '@mui/material/styles';
import PropTypes from 'prop-types';
import WindowCanvasNavigationControls from '../containers/WindowCanvasNavigationControls';

const StyledContainer = styled('div')(() => ({
alignItems: 'center',
display: 'flex',
width: '100%',
}));

const StyledText = styled('div')(() => ({
maxHeight: '100%',
width: '100%',
}));

/**
* Simple divs with canvas navigation, which should mimic v3 fallthrough to WindowViewer
* with non-image resources and provide a target for plugin overrides with minimal disruption.
*/
export function TextViewer({ textOptions = {}, textResources = [], windowId }) {
return (
<StyledContainer>
<StyledText {...textOptions}>
{textResources.map(text => (
<source key={text.id} src={text.id} type={text.getFormat()} />
))}
<WindowCanvasNavigationControls windowId={windowId} />
</StyledText>
</StyledContainer>
);
}

TextViewer.propTypes = {
textOptions: PropTypes.object, // eslint-disable-line react/forbid-prop-types
textResources: PropTypes.arrayOf(PropTypes.object), // eslint-disable-line react/forbid-prop-types
windowId: PropTypes.string.isRequired,
};
4 changes: 3 additions & 1 deletion src/containers/PrimaryWindow.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { compose } from 'redux';
import { connect } from 'react-redux';
import { withPlugins } from '../extend/withPlugins';
import {
getManifestoInstance, getVisibleCanvasAudioResources, getVisibleCanvasVideoResources, getWindow,
getManifestoInstance, getVisibleCanvasAudioResources, getVisibleCanvasTextResources,
getVisibleCanvasVideoResources, getWindow,
} from '../state/selectors';
import { PrimaryWindow } from '../components/PrimaryWindow';

Expand All @@ -13,6 +14,7 @@ const mapStateToProps = (state, { windowId }) => {
audioResources: getVisibleCanvasAudioResources(state, { windowId }) || [],
isCollection: manifestoInstance && manifestoInstance.isCollection(),
isCollectionDialogVisible: getWindow(state, { windowId }).collectionDialogOn,
textResources: getVisibleCanvasTextResources(state, { windowId }) || [],
videoResources: getVisibleCanvasVideoResources(state, { windowId }) || [],
};
};
Expand Down
21 changes: 21 additions & 0 deletions src/containers/TextViewer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { compose } from 'redux';
import { connect } from 'react-redux';
import { withPlugins } from '../extend/withPlugins';
import { getConfig, getVisibleCanvasTextResources } from '../state/selectors';
import { TextViewer } from '../components/TextViewer';

/** */
const mapStateToProps = (state, { windowId }) => (
{
textOptions: getConfig(state).textOptions,
textResources: getVisibleCanvasTextResources(state, { windowId }) || [],
}
);

const enhance = compose(
connect(mapStateToProps, null),
withPlugins('TextViewer'),
// further HOC go here
);

export default enhance(TextViewer);
8 changes: 8 additions & 0 deletions src/lib/MiradorCanvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ export default class MiradorCanvas {
}));
}

/** */
get textResources() {
const resources = flattenDeep([
this.canvas.getContent().map(i => i.getBody()),
]);
return flatten(resources.filter((resource) => resource.getProperty('type') === 'Text'));
}

/** */
get videoResources() {
const resources = flattenDeep([
Expand Down
14 changes: 14 additions & 0 deletions src/state/selectors/canvases.js
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,20 @@ export const getVisibleCanvasNonTiledResources = createSelector(
.filter(resource => resource.getServices().length < 1),
);

/**
* Returns visible canvas text resources.
* @param {object} state
* @param {string} windowId
* @return {Array}
*/
export const getVisibleCanvasTextResources = createSelector(
[
getVisibleCanvases,
],
canvases => flatten(canvases
.map(canvas => new MiradorCanvas(canvas).textResources)),
);

/**
* Returns visible canvas video resources.
* @param {object} state
Expand Down