Skip to content

Commit

Permalink
Core mirador behaviors to provide a plugin target for text resources
Browse files Browse the repository at this point in the history
- refactor type-based filters into a module
- MiradorCanvas.imagesResources does not assume any service is an image service
- stub TextViewer shows empty div, source elements for text resources, and canvas navigation
- fixes #4085
  • Loading branch information
barmintor committed Jan 29, 2025
1 parent a8daa02 commit 7c63af6
Show file tree
Hide file tree
Showing 14 changed files with 364 additions and 19 deletions.
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"
}
]
}
]
}
]
}
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);
});
});
});
31 changes: 31 additions & 0 deletions __tests__/src/lib/resourceFilters.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Utils } from 'manifesto.js';
import flattenDeep from 'lodash/flattenDeep';
import manifestFixture019 from '../../fixtures/version-2/019.json';
import {
filterByProfiles, filterByTypes,
} from '../../../src/lib/resourceFilters';

describe('resourceFilters', () => {
let canvas;
beforeEach(() => {
[canvas] = Utils.parseManifest(manifestFixture019).getSequences()[0].getCanvases();
});
describe('filterByProfiles', () => {
it('filters resources', () => {
const services = flattenDeep(canvas.resourceAnnotations.map((a) => a.getResource().getServices()));
expect(filterByProfiles(services, 'http://iiif.io/api/image/2/level2.json').map((s) => s.id)).toEqual([
'https://stacks.stanford.edu/image/iiif/hg676jb4964%2F0380_796-44',
]);
expect(filterByProfiles(services, 'http://nonexistent.io/api/service.json').map((s) => s.id)).toEqual([]);
});
});
describe('filterByTypes', () => {
it('filters resources', () => {
const resources = flattenDeep(canvas.resourceAnnotations.map((a) => a.getResource()));
expect(filterByTypes(resources, 'dctypes:Image').map((r) => r.id)).toEqual([
'https://stacks.stanford.edu/image/iiif/hg676jb4964%2F0380_796-44/full/full/0/default.jpg',
]);
expect(filterByTypes(resources, 'Nonexistent').map((r) => r.id)).toEqual([]);
});
});
});
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}
/>

Check warning on line 65 in src/components/PrimaryWindow.js

View check run for this annotation

Codecov / codecov/patch

src/components/PrimaryWindow.js#L62-L65

Added lines #L62 - L65 were not covered by tests
);
}
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
19 changes: 19 additions & 0 deletions src/containers/TextViewer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { compose } from 'redux';
import { withPlugins } from '../extend/withPlugins';
import { TextViewer } from '../components/TextViewer';

Check warning on line 3 in src/containers/TextViewer.js

View check run for this annotation

Codecov / codecov/patch

src/containers/TextViewer.js#L2-L3

Added lines #L2 - L3 were not covered by tests

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

Check warning on line 10 in src/containers/TextViewer.js

View check run for this annotation

Codecov / codecov/patch

src/containers/TextViewer.js#L6-L10

Added lines #L6 - L10 were not covered by tests
);

const enhance = compose(
connect(mapStateToProps, null),
withPlugins('TextViewer'),

Check warning on line 15 in src/containers/TextViewer.js

View check run for this annotation

Codecov / codecov/patch

src/containers/TextViewer.js#L13-L15

Added lines #L13 - L15 were not covered by tests
// further HOC go here
);

Check warning on line 17 in src/containers/TextViewer.js

View check run for this annotation

Codecov / codecov/patch

src/containers/TextViewer.js#L17

Added line #L17 was not covered by tests

export default enhance(TextViewer);

Check warning on line 19 in src/containers/TextViewer.js

View check run for this annotation

Codecov / codecov/patch

src/containers/TextViewer.js#L19

Added line #L19 was not covered by tests
Loading

0 comments on commit 7c63af6

Please sign in to comment.