diff --git a/__tests__/integration/mirador/index.html b/__tests__/integration/mirador/index.html index 5c097b29396425ee4ea3f7b69922b2546a291d6a..0d1d65e24fa85abd8f7eb23399e921b323f83fa8 100644 --- a/__tests__/integration/mirador/index.html +++ b/__tests__/integration/mirador/index.html @@ -17,7 +17,7 @@ canvasIndex: 2, }, { - loadedManifest: 'https://media.nga.gov/public/manifests/nga_highlights.json', + loadedManifest: 'https://iiif.bodleian.ox.ac.uk/iiif/manifest/e32a277e-91e2-4a6d-8ba6-cc4bad230410.json', thumbnailNavigationPosition: 'off', }] }); diff --git a/__tests__/src/components/App.test.js b/__tests__/src/components/App.test.js index 145241c5e9499b3ffb0a7aac8a51a78e22fd59a0..258bc47b49d9e4de45e91dbd991d4db1a21ae34b 100644 --- a/__tests__/src/components/App.test.js +++ b/__tests__/src/components/App.test.js @@ -1,29 +1,52 @@ import React from 'react'; import { shallow } from 'enzyme'; +import { MuiThemeProvider } from '@material-ui/core/styles'; +import Fullscreen from 'react-fullscreen-crossbrowser'; +import WorkspaceControlPanel from '../../../src/components/WorkspaceControlPanel'; +import Workspace from '../../../src/containers/Workspace'; import App from '../../../src/components/App'; +/** */ +function createWrapper(props) { + return shallow( + <App + isFullscreenEnabled={false} + setWorkspaceFullscreen={() => {}} + theme="light" + classes={{}} + {...props} + />, + ).dive(); // to unwrapp HOC created by withStyle() +} + describe('App', () => { - it('renders without an error', () => { - const wrapper = shallow( - <App - manifests={[]} - workspace={{}} - config={{ theme: 'light' }} - />, - ); - expect(wrapper.dive().find('div.mirador-app').length).toBe(1); + it('should render outer element correctly', () => { + const wrapper = createWrapper(); + expect(wrapper.find('div.mirador-app').length).toBe(1); + }); + + it('should render all needed elements ', () => { + const wrapper = createWrapper(); + expect(wrapper.find(MuiThemeProvider).length).toBe(1); + expect(wrapper.find(Fullscreen).length).toBe(1); + expect(wrapper.find(Workspace).length).toBe(1); + expect(wrapper.find(WorkspaceControlPanel).length).toBe(1); }); - describe('FullScreen', () => { - it('is enabled by the workspace.fullscreen state', () => { - const wrapper = shallow( - <App - manifests={[]} - workspace={{ isFullscreenEnabled: true }} - config={{ theme: 'light' }} - />, - ); - expect(wrapper.dive().find('FullScreen').first().prop('enabled')).toEqual(true); - }); + it('should pass setWorkspaceFullscreen to Fullscreen.onChange', () => { + const mockFn = jest.fn(); + const wrapper = createWrapper({ setWorkspaceFullscreen: mockFn }); + expect(wrapper.find(Fullscreen).first().prop('onChange')) + .toBe(mockFn); + }); + + it('should pass isFullscreenEnabled to Fullscreen.enabled', () => { + let wrapper = createWrapper({ isFullscreenEnabled: false }); + expect(wrapper.find(Fullscreen).first().prop('enabled')) + .toEqual(false); + + wrapper = createWrapper({ isFullscreenEnabled: true }); + expect(wrapper.find(Fullscreen).first().prop('enabled')) + .toEqual(true); }); }); diff --git a/__tests__/src/components/ThumbnailNavigation.test.js b/__tests__/src/components/ThumbnailNavigation.test.js index a702eeafa84f20f8e960c14aaca18f19df982897..16266e59003bab8f59620f905564bdd6a59695df 100644 --- a/__tests__/src/components/ThumbnailNavigation.test.js +++ b/__tests__/src/components/ThumbnailNavigation.test.js @@ -19,11 +19,9 @@ describe('ThumbnailNavigation', () => { ); wrapper = shallow( <ThumbnailNavigation - manifest={{ - id: 'http://foo', - manifestation: manifesto.create(manifestJson), - isFetching: false, - }} + canvases={ + manifesto.create(manifestJson).getSequences()[0].getCanvases() + } window={{ id: 'foobar', canvasIndex: 1, diff --git a/__tests__/src/components/WindowIcon.test.js b/__tests__/src/components/WindowIcon.test.js index d5a209d73edf42cffb9d4729b23ced59b1230883..93a26f5f58a47466e0f039739874992d914e24df 100644 --- a/__tests__/src/components/WindowIcon.test.js +++ b/__tests__/src/components/WindowIcon.test.js @@ -2,40 +2,27 @@ import React from 'react'; import { shallow } from 'enzyme'; import WindowIcon from '../../../src/components/WindowIcon'; -describe('WindowIcon', () => { - let wrapper; - let manifestation; - - describe('without a manifestation', () => { - beforeEach(() => { - wrapper = shallow(<WindowIcon />).dive(); - }); - - it('renders without an error', () => { - expect(wrapper.find('img').length).toBe(0); - }); - }); +/** createWrapper */ +function createWrapper(props) { + return shallow( + <WindowIcon + manifestLogo="" + classses={{}} + {...props} + />, + ).dive(); // to unwrap HOC created by withStyles(); +} - - describe('with a manifestation without a logo', () => { - beforeEach(() => { - manifestation = { getLogo: () => null }; - wrapper = shallow(<WindowIcon manifestation={manifestation} />).dive(); - }); - - it('renders without an error', () => { - expect(wrapper.find('img').length).toBe(0); - }); +describe('WindowIcon', () => { + it('should render nothing if no manifest logo given', () => { + const wrapper = createWrapper(); + expect(wrapper.find('img').length).toBe(0); }); - describe('with a manifestation with a logo', () => { - beforeEach(() => { - manifestation = { getLogo: () => 'http://example.com/thumbnail.jpg' }; - wrapper = shallow(<WindowIcon manifestation={manifestation} classes={{ logo: 'logo-class' }} />).dive(); - }); - - it('renders without an error', () => { - expect(wrapper.find('img.logo-class[src="http://example.com/thumbnail.jpg"]').length).toBe(1); - }); + it('should render logo if manifest logo is given', () => { + const manifestLogo = 'http://foo.bar'; + const wrapper = createWrapper({ manifestLogo }); + expect(wrapper.find('img').first().prop('src')) + .toEqual(manifestLogo); }); }); diff --git a/__tests__/src/components/WindowList.test.js b/__tests__/src/components/WindowList.test.js index 896dd2aa64e23c0f18d7ff4f1c5169536dff97af..0bc7a7e143d131c70a935633b51c503bba976f89 100644 --- a/__tests__/src/components/WindowList.test.js +++ b/__tests__/src/components/WindowList.test.js @@ -49,7 +49,7 @@ describe('WindowList', () => { expect(wrapper.find('WithStyles(MenuItem)').length).toBe(1); expect(wrapper.find('WithStyles(MenuItem)').key()).toBe('xyz'); expect( - wrapper.find('WithStyles(MenuItem)').matchesElement(<MenuItem>[Untitled]</MenuItem>), + wrapper.find('WithStyles(MenuItem)').matchesElement(<MenuItem>untitled</MenuItem>), ).toBe(true); wrapper.find('WithStyles(MenuItem)').simulate('click', {}); expect(handleClose).toBeCalled(); diff --git a/__tests__/src/components/WindowSideBarButtons.test.js b/__tests__/src/components/WindowSideBarButtons.test.js index 0ffde2da597f94e00f98d9e6751c72681307bb5f..b57978af86b66edb99bb61010868908957f6e9ad 100644 --- a/__tests__/src/components/WindowSideBarButtons.test.js +++ b/__tests__/src/components/WindowSideBarButtons.test.js @@ -18,7 +18,7 @@ describe('WindowSideBarButtons', () => { <WindowSideBarButtons toggleWindowSideBarPanel={toggleWindowSideBarPanel} />, ); - const iconButton = wrapper.find('WithStyles(IconButton)[aria-label="Open information companion window"]'); + const iconButton = wrapper.find('WithStyles(IconButton)[aria-label="openInfoCompanionWindow"]'); expect(iconButton.simulate('click')); expect(toggleWindowSideBarPanel).toHaveBeenCalledTimes(1); expect(toggleWindowSideBarPanel).toHaveBeenCalledWith('info'); diff --git a/__tests__/src/components/WindowSideBarInfoPanel.test.js b/__tests__/src/components/WindowSideBarInfoPanel.test.js index e74a3e79929e997a818a56538cf1402fbf66c676..21c6fabc32f12d3076bea7c6d4862de33376c7fa 100644 --- a/__tests__/src/components/WindowSideBarInfoPanel.test.js +++ b/__tests__/src/components/WindowSideBarInfoPanel.test.js @@ -1,5 +1,6 @@ import React from 'react'; import { shallow } from 'enzyme'; +import Typography from '@material-ui/core/Typography'; import createStore from '../../../src/state/createStore'; import * as actions from '../../../src/state/actions'; import WindowSideBarInfoPanel from '../../../src/components/WindowSideBarInfoPanel'; @@ -13,12 +14,28 @@ describe('WindowSideBarInfoPanel', () => { beforeEach(() => { store.dispatch(actions.receiveManifest('foo', fixture)); manifest = store.getState().manifests.foo; - wrapper = shallow(<WindowSideBarInfoPanel manifest={manifest} />); + wrapper = shallow( + <WindowSideBarInfoPanel manifest={manifest} />, + ).dive(); }); it('renders without an error', () => { - expect(wrapper.find('h2').text()).toBe('About this item'); - expect(wrapper.find('h3').text()).toBe('Bodleian Library Human Freaks 2 (33)'); - expect(wrapper.find('.mirador-window-sidebar-info-panel div').text()).toBe('[Handbill of Mr. Becket, [1787] ]'); + expect( + wrapper.find('WithStyles(Typography)[variant="h2"]').first().matchesElement( + <Typography>aboutThisItem</Typography>, + ), + ).toBe(true); + + expect( + wrapper.find('WithStyles(Typography)[variant="h3"]').first().matchesElement( + <Typography>Bodleian Library Human Freaks 2 (33)</Typography>, + ), + ).toBe(true); + + expect( + wrapper.find('WithStyles(Typography)[variant="body2"]').first().matchesElement( + <Typography>[Handbill of Mr. Becket, [1787] ]</Typography>, + ), + ).toBe(true); }); }); diff --git a/__tests__/src/components/WindowSideBarPanel.test.js b/__tests__/src/components/WindowSideBarPanel.test.js index 1478f900e36add28440a0f2a47a87ea14f07c015..f31c05019910c90a36cb66ede06d229e6d24a5f7 100644 --- a/__tests__/src/components/WindowSideBarPanel.test.js +++ b/__tests__/src/components/WindowSideBarPanel.test.js @@ -3,6 +3,7 @@ import { shallow } from 'enzyme'; import createStore from '../../../src/state/createStore'; import * as actions from '../../../src/state/actions'; import WindowSideBarPanel from '../../../src/components/WindowSideBarPanel'; +import WindowSideBarInfoPanel from '../../../src/containers/WindowSideBarInfoPanel'; import fixture from '../../fixtures/version-2/001.json'; describe('WindowSideBarPanel', () => { @@ -21,7 +22,7 @@ describe('WindowSideBarPanel', () => { }); it('renders the WindowSideBarInfoPanel', () => { - expect(wrapper.find('WindowSideBarInfoPanel').length).toBe(1); + expect(wrapper.find(WindowSideBarInfoPanel).length).toBe(1); }); }); @@ -31,7 +32,7 @@ describe('WindowSideBarPanel', () => { }); it('does not render any panel component', () => { - expect(wrapper.find('WindowSideBarInfoPanel').length).toBe(0); + expect(wrapper.find(WindowSideBarInfoPanel).length).toBe(0); }); }); }); diff --git a/__tests__/src/components/WindowThumbnailSettings.test.js b/__tests__/src/components/WindowThumbnailSettings.test.js index 6453d313cf205b5511c9bb6c95ad00ecb8da5379..7b835555a7e0c9c24a1b2bf37e0aa743c27030d7 100644 --- a/__tests__/src/components/WindowThumbnailSettings.test.js +++ b/__tests__/src/components/WindowThumbnailSettings.test.js @@ -1,27 +1,47 @@ import React from 'react'; import { shallow } from 'enzyme'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import RadioGroup from '@material-ui/core/RadioGroup'; +import Typography from '@material-ui/core/Typography'; import WindowThumbnailSettings from '../../../src/components/WindowThumbnailSettings'; +/** create wrapper */ +function createWrapper(props) { + return shallow( + <WindowThumbnailSettings + windowId="xyz" + setWindowThumbnailPosition={() => {}} + thumbnailNavigationPosition="off" + {...props} + />, + ); +} + describe('WindowThumbnailSettings', () => { - let wrapper; - const setWindowThumbnailPosition = jest.fn(); - beforeEach(() => { - wrapper = shallow( - <WindowThumbnailSettings - windowId="xyz" - setWindowThumbnailPosition={setWindowThumbnailPosition} - thumbnailNavigationPosition="bottom" - />, - ); + it('renders all elements correctly', () => { + const wrapper = createWrapper(); + expect(wrapper.find(Typography).length).toBe(1); + expect(wrapper.find(RadioGroup).length).toBe(1); + const labels = wrapper.find(FormControlLabel); + expect(labels.length).toBe(3); + expect(labels.at(0).props().value).toBe('off'); + expect(labels.at(1).props().value).toBe('bottom'); + expect(labels.at(2).props().value).toBe('right'); }); - it('renders without an error', () => { - expect(wrapper.find('WithStyles(Typography)').dive().dive().text()).toBe('Thumbnails'); - expect(wrapper.find('RadioGroup').props().value).toBe('bottom'); + it('should set the correct label active', () => { + let wrapper = createWrapper({ thumbnailNavigationPosition: 'bottom' }); + expect(wrapper.find(RadioGroup).props().value).toBe('bottom'); + wrapper = createWrapper({ thumbnailNavigationPosition: 'right' }); + expect(wrapper.find(RadioGroup).props().value).toBe('right'); }); it('updates state when the thumbnail config selection changes', () => { - wrapper.find('RadioGroup').first().simulate('change', { target: { value: 'off' } }); + const setWindowThumbnailPosition = jest.fn(); + const wrapper = createWrapper({ setWindowThumbnailPosition }); + wrapper.find(RadioGroup).first().simulate('change', { target: { value: 'off' } }); expect(setWindowThumbnailPosition).toHaveBeenCalledWith('xyz', 'off'); + wrapper.find(RadioGroup).first().simulate('change', { target: { value: 'right' } }); + expect(setWindowThumbnailPosition).toHaveBeenCalledWith('xyz', 'right'); }); }); diff --git a/__tests__/src/components/WindowTopBar.test.js b/__tests__/src/components/WindowTopBar.test.js index 250249f37af3c547cf540201c7d912e525662eda..86cc567291eeb31e5d1136fd1663c6109df3e5d8 100644 --- a/__tests__/src/components/WindowTopBar.test.js +++ b/__tests__/src/components/WindowTopBar.test.js @@ -1,6 +1,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import WindowTopBar from '../../../src/components/WindowTopBar'; +import WindowIcon from '../../../src/containers/WindowIcon'; const manifestFixture = { manifestation: { @@ -23,6 +24,7 @@ describe('WindowTopBar', () => { removeWindow={mockRemoveWindow} toggleWindowSideBar={mockToggleWindowSideBar} classes={{}} + t={key => key} />, ).dive(); }); @@ -37,7 +39,7 @@ describe('WindowTopBar', () => { }); it('renders a window icon', () => { - expect(topBar.find('WithStyles(WindowIcon)').length).toBe(1); + expect(topBar.find(WindowIcon).length).toBe(1); }); it('calls the toggleWindowSideBar prop when the menu IconButton is clicked', () => { diff --git a/__tests__/src/components/WindowTopMenu.test.js b/__tests__/src/components/WindowTopMenu.test.js index 47851603c78bd7d8081aaa8c8192344d094aae13..d2b260854166e6c87d7790af4e1b1efba1dbf29f 100644 --- a/__tests__/src/components/WindowTopMenu.test.js +++ b/__tests__/src/components/WindowTopMenu.test.js @@ -1,17 +1,52 @@ import React from 'react'; import { shallow } from 'enzyme'; +import ListItem from '@material-ui/core/ListItem'; +import Menu from '@material-ui/core/Menu'; +import Divider from '@material-ui/core/Divider'; +import WindowThumbnailSettings from '../../../src/containers/WindowThumbnailSettings'; import WindowTopMenu from '../../../src/components/WindowTopMenu'; +/** create wrapper */ +function createWrapper(props) { + return shallow( + <WindowTopMenu + windowId="xyz" + handleClose={() => {}} + anchorEl={null} + {...props} + />, + ); +} + describe('WindowTopMenu', () => { - let wrapper; - let handleClose; - beforeEach(() => { - handleClose = jest.fn(); - wrapper = shallow(<WindowTopMenu windowId="xyz" handleClose={handleClose} />).dive(); + it('renders all needed elements', () => { + const wrapper = createWrapper(); + expect(wrapper.find(Menu).length).toBe(1); + expect(wrapper.find(ListItem).length).toBe(1); + expect(wrapper.find(WindowThumbnailSettings).length).toBe(1); + expect(wrapper.find(Divider).length).toBe(1); + }); + + it('passes windowId to <WindowThumbnailSettings/>', () => { + const wrapper = createWrapper(); + expect(wrapper.find(WindowThumbnailSettings) + .first().props().windowId).toBe('xyz'); + }); + + it('passses correct props to <Menu/> when no achor element given', () => { + const handleClose = jest.fn(); + const wrapper = createWrapper({ handleClose }); + expect(wrapper.find(Menu).first().props().anchorEl).toBe(null); + expect(wrapper.find(Menu).first().props().open).toBe(false); + expect(wrapper.find(Menu).first().props().onClose).toBe(handleClose); }); - it('renders without an error', () => { - expect(wrapper.find('WithStyles(Menu)').length).toBe(1); - expect(wrapper.find('Connect(WindowThumbnailSettings)').length).toBe(1); + it('passses correct props to <Menu/> when no achor element given', () => { + const handleClose = jest.fn(); + const anchorEl = {}; + const wrapper = createWrapper({ anchorEl, handleClose }); + expect(wrapper.find(Menu).first().props().anchorEl).toBe(anchorEl); + expect(wrapper.find(Menu).first().props().open).toBe(true); + expect(wrapper.find(Menu).first().props().onClose).toBe(handleClose); }); }); diff --git a/__tests__/src/components/WindowTopMenuButton.test.js b/__tests__/src/components/WindowTopMenuButton.test.js index c016c58da0146cf137c8b53a7c0b93e8f472f78a..e36db1c72fe205f6fdfebfb259892bcf4fa212f7 100644 --- a/__tests__/src/components/WindowTopMenuButton.test.js +++ b/__tests__/src/components/WindowTopMenuButton.test.js @@ -1,6 +1,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import WindowTopMenuButton from '../../../src/components/WindowTopMenuButton'; +import WindowTopMenu from '../../../src/containers/WindowTopMenu'; describe('WindowTopMenuButton', () => { let wrapper; @@ -15,6 +16,6 @@ describe('WindowTopMenuButton', () => { }); it('when clicked, updates the state', () => { wrapper.find('WithStyles(IconButton)').simulate('click', { currentTarget: 'x' }); - expect(wrapper.find('Connect(miradorWithPlugins(WithStyles(WindowTopMenu)))').props().anchorEl).toBe('x'); + expect(wrapper.find(WindowTopMenu).props().anchorEl).toBe('x'); }); }); diff --git a/__tests__/src/components/WorkspaceControlPanelButtons.test.js b/__tests__/src/components/WorkspaceControlPanelButtons.test.js index c4c28fbf164d354c9fcbbbab76c607a446f1b66e..a66c8dcb5c7b0d7b37fdc4d746d0421d700435d0 100644 --- a/__tests__/src/components/WorkspaceControlPanelButtons.test.js +++ b/__tests__/src/components/WorkspaceControlPanelButtons.test.js @@ -1,5 +1,6 @@ import React from 'react'; import { shallow } from 'enzyme'; +import WorkspaceFullScreenButton from '../../../src/containers/WorkspaceFullScreenButton'; import WorkspaceControlPanelButtons from '../../../src/components/WorkspaceControlPanelButtons'; @@ -11,6 +12,6 @@ describe('WorkspaceControlPanelButtons', () => { it('renders without an error', () => { expect(wrapper.find('WithStyles(List)').length).toBe(1); - expect(wrapper.find('Connect(WithStyles(WorkspaceFullScreenButton))').length).toBe(1); + expect(wrapper.find(WorkspaceFullScreenButton).length).toBe(1); }); }); diff --git a/__tests__/src/components/WorkspaceFullScreenButton.test.js b/__tests__/src/components/WorkspaceFullScreenButton.test.js index 97e1612c65a8109334c5b49bee49b3ae71f58dc8..4fcb48a6bf5d0ecf5ecf3a4b68b2f836ba067fe0 100644 --- a/__tests__/src/components/WorkspaceFullScreenButton.test.js +++ b/__tests__/src/components/WorkspaceFullScreenButton.test.js @@ -8,7 +8,10 @@ describe('WorkspaceFullScreenButton', () => { beforeEach(() => { setWorkspaceFullscreen = jest.fn(); wrapper = shallow( - <WorkspaceFullScreenButton classes={{}} setWorkspaceFullscreen={setWorkspaceFullscreen} />, + <WorkspaceFullScreenButton + classes={{}} + setWorkspaceFullscreen={setWorkspaceFullscreen} + />, ).dive(); }); diff --git a/__tests__/src/components/WorkspaceMenu.test.js b/__tests__/src/components/WorkspaceMenu.test.js index bc9117a59a87c798963677c9225ec31d8c9574fc..27c11729c9568368cfb7881ae3c51cbfebe2745c 100644 --- a/__tests__/src/components/WorkspaceMenu.test.js +++ b/__tests__/src/components/WorkspaceMenu.test.js @@ -1,6 +1,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import WorkspaceMenu from '../../../src/components/WorkspaceMenu'; +import WindowList from '../../../src/containers/WindowList'; describe('WorkspaceMenu', () => { let wrapper; @@ -23,15 +24,14 @@ describe('WorkspaceMenu', () => { it('sets the anchor state', () => { wrapper.instance().handleMenuItemClick('windowList', { currentTarget: true }); - expect(wrapper.find('Connect(WindowList)').props().open).toBe(true); + expect(wrapper.find(WindowList).props().open).toBe(true); }); }); describe('handleMenuItemClose', () => { it('resets the anchor state', () => { wrapper.instance().handleMenuItemClose('windowList')(); - - expect(wrapper.find('Connect(WindowList)').props().open).toBe(false); + expect(wrapper.find(WindowList).props().open).toBe(false); }); }); }); diff --git a/__tests__/src/selectors/index.test.js b/__tests__/src/selectors/index.test.js new file mode 100644 index 0000000000000000000000000000000000000000..f3b3d48e832b087e0014e6e361accd2a5b10f9d4 --- /dev/null +++ b/__tests__/src/selectors/index.test.js @@ -0,0 +1,96 @@ +import manifesto from 'manifesto.js'; +import manifestFixture from '../../fixtures/version-2/001.json'; +import { + getWindowManifest, + getManifestLogo, + getManifestCanvases, + getThumbnailNavigationPosition, +} from '../../../src/state/selectors'; + + +describe('getWindowManifest()', () => { + const state = { + windows: { + a: { id: 'a', manifestId: 'x' }, + b: { id: 'b', manifestId: 'y' }, + c: { id: 'c' }, + }, + manifests: { + x: { id: 'x' }, + }, + }; + + it('should return the manifest of a certain window', () => { + const received = getWindowManifest(state, 'a'); + const expected = { id: 'x' }; + expect(received).toEqual(expected); + }); + + it('should return undefined if window doesnt exist', () => { + const received = getWindowManifest(state, 'unknown'); + expect(received).toBeUndefined(); + }); + + it('should return undefined if window has no manifest id', () => { + const received = getWindowManifest(state, 'c'); + expect(received).toBeUndefined(); + }); + + it('should return undefined if manifest does not exist', () => { + const received = getWindowManifest(state, 'b'); + expect(received).toBeUndefined(); + }); +}); + +describe('getManifestLogo()', () => { + it('should return manifest logo id', () => { + const manifest = { manifestation: manifesto.create(manifestFixture) }; + const received = getManifestLogo(manifest); + expect(received).toEqual(manifestFixture.logo['@id']); + }); + + it('should return null if manifest has no logo', () => { + const manifest = { manifestation: manifesto.create({}) }; + const received = getManifestLogo(manifest); + expect(received).toBeNull(); + }); +}); + +describe('getManifestCanvases', () => { + it('returns an empty array if the manifestation is not loaded', () => { + const manifest = {}; + const received = getManifestCanvases(manifest); + expect(received).toEqual([]); + }); + + it('returns canvases from the manifest', () => { + const manifest = { manifestation: manifesto.create(manifestFixture) }; + const received = getManifestCanvases(manifest); + expect(received.length).toBe(1); + expect(received[0].id).toBe('https://iiif.bodleian.ox.ac.uk/iiif/canvas/9cca8fdd-4a61-4429-8ac1-f648764b4d6d.json'); + }); +}); + +describe('getThumbnailNavigationPosition', () => { + const state = { + windows: { + a: { id: 'a', thumbnailNavigationPosition: 'bottom' }, + b: { id: 'b' }, + }, + }; + + it('should return thumbnail navigation position if window exists', () => { + const received = getThumbnailNavigationPosition(state, 'a'); + expect(received).toBe('bottom'); + }); + + it('should return undefined if position does not exist in window', () => { + const received = getThumbnailNavigationPosition(state, 'b'); + expect(received).toBeUndefined(); + }); + + it('should return undefined if window does not exists', () => { + const received = getThumbnailNavigationPosition(state, 'c'); + expect(received).toBeUndefined(); + }); +}); diff --git a/locales/en/translation.json b/locales/en/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..3ca92e21328345b5afbeff4ef4d8c37ddc80a007 --- /dev/null +++ b/locales/en/translation.json @@ -0,0 +1,28 @@ +{ + "translation": { + "aboutThisItem": "About this item", + "add": "Add", + "bottom": "Bottom", + "closeInfoCompanionWindow": "Close information companion window", + "closeMenu": "Close Menu", + "closeWindow": "Close window", + "dark": "Dark", + "downloadExport": "Download/Export", + "downloadExportWorkspace": "Download/export workspace", + "fetchManifest": "Fetch Manifest", + "fullScreen": "Full Screen", + "light": "Light", + "listAllOpenWindows": "List all open windows", + "menu": "Menu", + "off": "Off", + "openInfoCompanionWindow": "Open information companion window", + "openWindows": "Open windows", + "position": "Position", + "right": "Right", + "settings": "Settings", + "theme": "Theme", + "thumbnails": "Thumbnails", + "toggleWindowSideBar": "Toggle window sidebar", + "untitled": "[Untitled]" + } +} diff --git a/package.json b/package.json index d147f892767a6bc609542207aa5e4a093cc8f04b..5e0c9cbdd54832473e5e6c0110ba08746cecc621 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "classnames": "^2.2.6", "css-ns": "^1.2.2", "deepmerge": "^3.1.0", + "i18next": "^14.0.1", "intersection-observer": "^0.5.1", "manifesto.js": "^3.0.9", "node-fetch": "^2.3.0", @@ -43,6 +44,7 @@ "react": "^16.7.0", "react-dom": "^16.4.0", "react-fullscreen-crossbrowser": "^1.0.9", + "react-i18next": "^9.0.10", "react-mosaic-component": "^2.0.2", "react-redux": "^6.0.0", "react-virtualized": "^9.21.0", diff --git a/src/components/App.js b/src/components/App.js index 05b03c507cdb06412824ea44fb36eab0c445973e..7b653149cc2404d786f75f25d26139d536ce7aec 100644 --- a/src/components/App.js +++ b/src/components/App.js @@ -13,28 +13,34 @@ import ns from '../config/css-ns'; */ class App extends Component { /** - * render - * @return {String} - HTML markup for the component - */ - render() { - const { - workspace, setWorkspaceFullscreen, config, classes, - } = this.props; - const theme = createMuiTheme({ + */ + makeMuiTheme() { + const { theme } = this.props; + return createMuiTheme({ palette: { - type: config.theme, + type: theme, }, typography: { useNextVariants: true, }, }); + } + + /** + * render + * @return {String} - HTML markup for the component + */ + render() { + const { + isFullscreenEnabled, setWorkspaceFullscreen, classes, + } = this.props; return ( <div className={classNames(classes.background, ns('app'))}> - <MuiThemeProvider theme={theme}> + <MuiThemeProvider theme={this.makeMuiTheme()}> <Fullscreen - enabled={workspace.isFullscreenEnabled} - onChange={isFullscreenEnabled => setWorkspaceFullscreen(isFullscreenEnabled)} + enabled={isFullscreenEnabled} + onChange={setWorkspaceFullscreen} > <Workspace /> </Fullscreen> @@ -46,17 +52,15 @@ class App extends Component { } App.propTypes = { - config: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types - workspace: PropTypes.object, // eslint-disable-line react/forbid-prop-types + theme: PropTypes.string.isRequired, // eslint-disable-line react/forbid-prop-types + isFullscreenEnabled: PropTypes.bool, // eslint-disable-line react/forbid-prop-types classes: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types, - setWorkspaceFullscreen: PropTypes.func, + setWorkspaceFullscreen: PropTypes.func.isRequired, }; App.defaultProps = { - workspace: {}, - setWorkspaceFullscreen: () => {}, + isFullscreenEnabled: false, }; - /** Material UI style overrides @private diff --git a/src/components/ManifestForm.js b/src/components/ManifestForm.js index 4ea3eeee329f7ab307ba8da95b67e31c56547f5c..bc9e3251389cd5a71680fceedfbe1822ad0141e9 100644 --- a/src/components/ManifestForm.js +++ b/src/components/ManifestForm.js @@ -52,6 +52,7 @@ class ManifestForm extends Component { */ render() { const { formValue } = this.state; + const { t } = this.props; return ( <form onSubmit={this.formSubmit}> <input @@ -60,7 +61,7 @@ class ManifestForm extends Component { type="text" onChange={this.handleInputChange} /> - <button id="fetchBtn" type="submit">FetchManifest</button> + <button id="fetchBtn" type="submit">{t('fetchManifest')}</button> </form> ); } @@ -69,6 +70,11 @@ class ManifestForm extends Component { ManifestForm.propTypes = { fetchManifest: PropTypes.func.isRequired, setLastRequested: PropTypes.func.isRequired, + t: PropTypes.func, +}; + +ManifestForm.defaultProps = { + t: key => key, }; export default ManifestForm; diff --git a/src/components/ThumbnailNavigation.js b/src/components/ThumbnailNavigation.js index edf65753bb3dbbb2cd800af11e63a49aefeabbee..e7de786093b5c7dda810350049406ffb3dda4dda 100644 --- a/src/components/ThumbnailNavigation.js +++ b/src/components/ThumbnailNavigation.js @@ -15,28 +15,10 @@ class ThumbnailNavigation extends Component { constructor(props) { super(props); - const canvases = (props.manifest.manifestation) - ? props.manifest.manifestation.getSequences()[0].getCanvases() : []; - this.state = { canvases, manifest: props.manifest }; - this.cellRenderer = this.cellRenderer.bind(this); this.calculateScaledWidth = this.calculateScaledWidth.bind(this); } - /** - */ - static getDerivedStateFromProps(props, state) { - // Any time the manifest changes, - // Reset any parts of state that are tied to that manifest (canvases). - if (props.manifest !== state.manifest) { - return { - canvases: props.manifest.manifestation.getSequences()[0].getCanvases(), - manifest: props.manifest, - }; - } - return null; - } - /** * Determines whether the current index is the rendered canvas, providing * a useful class. @@ -57,9 +39,8 @@ class ThumbnailNavigation extends Component { columnIndex, key, style, } = options; const { - window, setCanvas, config, + window, setCanvas, config, canvases, } = this.props; - const { canvases } = this.state; const canvas = canvases[columnIndex]; return ( <div @@ -90,8 +71,7 @@ class ThumbnailNavigation extends Component { * in this simple case, a column == canvas. */ calculateScaledWidth(options) { - const { config } = this.props; - const { canvases } = this.state; + const { config, canvases } = this.props; const canvas = new ManifestoCanvas(canvases[options.index]); return Math.floor(config.thumbnailNavigation.height * canvas.aspectRatio) + 8; } @@ -100,8 +80,7 @@ class ThumbnailNavigation extends Component { * Renders things */ render() { - const { config, window } = this.props; - const { canvases } = this.state; + const { config, window, canvases } = this.props; if (window.thumbnailNavigationPosition === 'off') { return <></>; } @@ -136,7 +115,7 @@ class ThumbnailNavigation extends Component { ThumbnailNavigation.propTypes = { config: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types - manifest: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types + canvases: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types setCanvas: PropTypes.func.isRequired, window: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types }; diff --git a/src/components/WindowIcon.js b/src/components/WindowIcon.js index 28a4edf9579a1ba771553f8e4d024f06acd9eb95..63894236feb9c737ba040e706ceb0c301e58d656 100644 --- a/src/components/WindowIcon.js +++ b/src/components/WindowIcon.js @@ -10,30 +10,32 @@ class WindowIcon extends Component { * @return */ render() { - const { - manifestation, classes, - } = this.props; + const { manifestLogo, classes } = this.props; - if (manifestation && manifestation.getLogo()) { - return (<img src={manifestation.getLogo()} alt="" role="presentation" className={classes.logo} />); - } + const img = manifestLogo && ( + <img + src={manifestLogo} + alt="" + role="presentation" + className={classes.logo} + /> + ); return ( - <></> + <> + {img} + </> ); } } WindowIcon.propTypes = { - manifestation: PropTypes.shape({ - getLogo: PropTypes.func, - }), - classes: PropTypes.shape({ logo: PropTypes.string }), + manifestLogo: PropTypes.string, + classes: PropTypes.shape({ logo: PropTypes.string }).isRequired, }; WindowIcon.defaultProps = { - manifestation: null, - classes: {}, + manifestLogo: null, }; const styles = { diff --git a/src/components/WindowList.js b/src/components/WindowList.js index df0ffdf703b9fbfac363afa2957818d4b583ba80..99d5c8fc7421105972a2b5b49f8ae36c897503b1 100644 --- a/src/components/WindowList.js +++ b/src/components/WindowList.js @@ -13,14 +13,14 @@ class WindowList extends Component { * @private */ titleContent(window) { - const { manifests } = this.props; + const { manifests, t } = this.props; if (window.manifestId && manifests[window.manifestId] && manifests[window.manifestId].manifestation) { return manifests[window.manifestId].manifestation.getLabel().map(label => label.value)[0]; } - return '[Untitled]'; + return t('untitled'); } /** @@ -29,13 +29,13 @@ class WindowList extends Component { */ render() { const { - handleClose, anchorEl, windows, focusWindow, + handleClose, anchorEl, windows, focusWindow, t, } = this.props; return ( <Menu id="window-list-menu" anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleClose}> <ListSubheader> - <Button color="inherit" aria-label="Close Menu" onClick={handleClose} align="right" style={{ float: 'right' }}>×</Button> - Open windows + <Button color="inherit" aria-label={t('closeMenu')} onClick={handleClose} align="right" style={{ float: 'right' }}>×</Button> + {t('openWindows')} </ListSubheader> { Object.values(windows).map(window => ( @@ -60,10 +60,12 @@ WindowList.propTypes = { anchorEl: PropTypes.object, // eslint-disable-line react/forbid-prop-types windows: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types manifests: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types + t: PropTypes.func, }; WindowList.defaultProps = { anchorEl: null, + t: key => key, }; export default WindowList; diff --git a/src/components/WindowSideBarButtons.js b/src/components/WindowSideBarButtons.js index 037349f635a591b5326c3a68edd4f5a82f4eaf01..122c22dc61a6ea6bb57f13a3e63c1b05e915d1d2 100644 --- a/src/components/WindowSideBarButtons.js +++ b/src/components/WindowSideBarButtons.js @@ -23,16 +23,19 @@ class WindowSideBarButtons extends Component { * @return {type} description */ render() { - const { toggleWindowSideBarPanel } = this.props; + const { toggleWindowSideBarPanel, t } = this.props; return ( <> <IconButton - aria-label="Open information companion window" - color="inherit" + aria-label={ + this.sideBarPanelCurrentlySelected('info') + ? t('closeInfoCompanionWindow') + : t('openInfoCompanionWindow') + } onClick={() => (toggleWindowSideBarPanel('info'))} > <InfoIcon - color={this.sideBarPanelCurrentlySelected('info') ? 'action' : 'inherit'} + color={this.sideBarPanelCurrentlySelected('info') ? 'primary' : 'inherit'} /> </IconButton> </> @@ -43,11 +46,13 @@ class WindowSideBarButtons extends Component { WindowSideBarButtons.propTypes = { toggleWindowSideBarPanel: PropTypes.func, sideBarPanel: PropTypes.string, + t: PropTypes.func, }; WindowSideBarButtons.defaultProps = { toggleWindowSideBarPanel: () => {}, sideBarPanel: 'closed', + t: key => key, }; export default WindowSideBarButtons; diff --git a/src/components/WindowSideBarInfoPanel.js b/src/components/WindowSideBarInfoPanel.js index 83de8502b1559a935a636c4e4dc5bd816e61ef73..aa646786848c2b78dd24c9efa59b7790c86a93d9 100644 --- a/src/components/WindowSideBarInfoPanel.js +++ b/src/components/WindowSideBarInfoPanel.js @@ -1,11 +1,13 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; +import Typography from '@material-ui/core/Typography'; +import { withStyles } from '@material-ui/core/styles'; import ns from '../config/css-ns'; /** * WindowSideBarInfoPanel */ -export default class WindowSideBarInfoPanel extends Component { +class WindowSideBarInfoPanel extends Component { /** * manifestLabel - get the label from the manifesto manifestation * @return String @@ -37,21 +39,36 @@ export default class WindowSideBarInfoPanel extends Component { * @return */ render() { + const { classes, t } = this.props; return ( <div className={ns('window-sidebar-info-panel')}> - <h2>About this item</h2> - <h3>{this.manifestLabel()}</h3> - <div>{this.manifestDescription()}</div> + <Typography variant="h2" className={classes.windowSideBarH2}>{t('aboutThisItem')}</Typography> + <Typography variant="h3" className={classes.windowSideBarH3}>{this.manifestLabel()}</Typography> + <Typography variant="body2">{this.manifestDescription()}</Typography> </div> ); } } WindowSideBarInfoPanel.propTypes = { + classes: PropTypes.object, // eslint-disable-line react/forbid-prop-types manifest: PropTypes.object, // eslint-disable-line react/forbid-prop-types + t: PropTypes.func, }; WindowSideBarInfoPanel.defaultProps = { + classes: {}, manifest: {}, + t: key => key, }; + +/** + * @private + */ +const styles = theme => ({ + windowSideBarH2: theme.typography.h5, + windowSideBarH3: theme.typography.h6, +}); + +export default withStyles(styles)(WindowSideBarInfoPanel); diff --git a/src/components/WindowSideBarPanel.js b/src/components/WindowSideBarPanel.js index a34d1622aaf65cd00ec322c677caeb5e1e2f67e5..2d647b70178e3c29f8e028f6d16f5f0dd77c1f2f 100644 --- a/src/components/WindowSideBarPanel.js +++ b/src/components/WindowSideBarPanel.js @@ -1,6 +1,6 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import WindowSideBarInfoPanel from './WindowSideBarInfoPanel'; +import WindowSideBarInfoPanel from '../containers/WindowSideBarInfoPanel'; /** * WindowSideBarPanel - the panel that pops out from the sidebar diff --git a/src/components/WindowThumbnailSettings.js b/src/components/WindowThumbnailSettings.js index ced6f4958653b9ab31e071868e424bd207faff92..518a74cd2884f1a5ac42dab26ae0ec4ddcaff2fc 100644 --- a/src/components/WindowThumbnailSettings.js +++ b/src/components/WindowThumbnailSettings.js @@ -16,8 +16,6 @@ export default class WindowThumbnailSettings extends Component { */ constructor(props) { super(props); - this.state = { - }; this.handleChange = this.handleChange.bind(this); } @@ -36,28 +34,28 @@ export default class WindowThumbnailSettings extends Component { * @return {type} description */ render() { - const { thumbnailNavigationPosition } = this.props; + const { thumbnailNavigationPosition, t } = this.props; return ( <> - <Typography>Thumbnails</Typography> - <RadioGroup aria-label="position" name="position" value={thumbnailNavigationPosition} onChange={this.handleChange} row> + <Typography>{t('thumbnails')}</Typography> + <RadioGroup aria-label={t('position')} name="position" value={thumbnailNavigationPosition} onChange={this.handleChange} row> <FormControlLabel value="off" control={<Radio color="primary" icon={<CancelPresentationIcon />} checkedIcon={<CancelPresentationIcon />} />} - label="Off" + label={t('off')} labelPlacement="bottom" /> <FormControlLabel value="bottom" control={<Radio color="primary" icon={<ThumbnailNavigationBottomIcon />} checkedIcon={<ThumbnailNavigationBottomIcon />} />} - label="Bottom" + label={t('bottom')} labelPlacement="bottom" /> <FormControlLabel value="right" control={<Radio color="primary" icon={<ThumbnailNavigationRightIcon />} checkedIcon={<ThumbnailNavigationRightIcon />} />} - label="Right" + label={t('right')} labelPlacement="bottom" /> </RadioGroup> @@ -88,4 +86,8 @@ WindowThumbnailSettings.propTypes = { windowId: PropTypes.string.isRequired, setWindowThumbnailPosition: PropTypes.func.isRequired, thumbnailNavigationPosition: PropTypes.string.isRequired, + t: PropTypes.func, +}; +WindowThumbnailSettings.defaultProps = { + t: key => key, }; diff --git a/src/components/WindowTopBar.js b/src/components/WindowTopBar.js index 23c9b07ddc3e5c06de8cdeb24966d58d8463a0c3..46a52d0aab63a07c41a398ef4b49c0a895eb2600 100644 --- a/src/components/WindowTopBar.js +++ b/src/components/WindowTopBar.js @@ -7,11 +7,12 @@ import IconButton from '@material-ui/core/IconButton'; import MenuIcon from '@material-ui/icons/Menu'; import Toolbar from '@material-ui/core/Toolbar'; import classNames from 'classnames'; -import WindowIcon from './WindowIcon'; -import WindowTopMenuButton from './WindowTopMenuButton'; +import WindowIcon from '../containers/WindowIcon'; +import WindowTopMenuButton from '../containers/WindowTopMenuButton'; import WindowTopBarButtons from '../containers/WindowTopBarButtons'; import ns from '../config/css-ns'; + /** * WindowTopBar */ @@ -35,24 +36,24 @@ class WindowTopBar extends Component { */ render() { const { - removeWindow, windowId, classes, toggleWindowSideBar, manifest, + removeWindow, windowId, classes, toggleWindowSideBar, t, } = this.props; return ( <Toolbar disableGutters className={classNames(classes.reallyDense, ns('window-top-bar'))} variant="dense"> <IconButton - aria-label="Open window side bar" + aria-label={t('toggleWindowSideBar')} color="inherit" onClick={() => toggleWindowSideBar(windowId)} > <MenuIcon /> </IconButton> - <WindowIcon manifestation={manifest.manifestation} /> + <WindowIcon windowId={windowId} /> <Typography variant="h3" noWrap color="inherit" className={classes.typographyBody}> {this.titleContent()} </Typography> <WindowTopBarButtons windowId={windowId} /> <WindowTopMenuButton className={ns('window-menu-btn')} windowId={windowId} /> - <Button color="inherit" className={ns('window-close')} aria-label="Close Window" onClick={removeWindow}>×</Button> + <Button color="inherit" className={ns('window-close')} aria-label={t('closeWindow')} onClick={removeWindow}>×</Button> </Toolbar> ); } @@ -64,11 +65,13 @@ WindowTopBar.propTypes = { windowId: PropTypes.string.isRequired, classes: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types toggleWindowSideBar: PropTypes.func, + t: PropTypes.func, }; WindowTopBar.defaultProps = { manifest: null, toggleWindowSideBar: () => {}, + t: key => key, }; const styles = { diff --git a/src/components/WindowTopMenu.js b/src/components/WindowTopMenu.js index a7e41a2f84ee04866fef1eee2fec2ad5bf2d5e44..b8d799abb772164ba0f2650933ee81c837c4004a 100644 --- a/src/components/WindowTopMenu.js +++ b/src/components/WindowTopMenu.js @@ -1,24 +1,13 @@ import React, { Component } from 'react'; -import { compose } from 'redux'; import ListItem from '@material-ui/core/ListItem'; import Menu from '@material-ui/core/Menu'; import Divider from '@material-ui/core/Divider'; -import { withStyles } from '@material-ui/core/styles'; import PropTypes from 'prop-types'; import WindowThumbnailSettings from '../containers/WindowThumbnailSettings'; /** */ class WindowTopMenu extends Component { - /** - * constructor - - */ - constructor(props) { - super(props); - this.state = { - }; - } - /** * render * @return @@ -50,15 +39,4 @@ WindowTopMenu.defaultProps = { anchorEl: null, }; -/** - * @private - */ -const styles = theme => ({ -}); - -const enhance = compose( - withStyles(styles), - // further HOC go here -); - -export default enhance(WindowTopMenu); +export default WindowTopMenu; diff --git a/src/components/WindowTopMenuButton.js b/src/components/WindowTopMenuButton.js index a8b66b249e84f772ca3e55f0f7670382e37851a7..a70bbb66850040fab080ded3669b0a650381b521 100644 --- a/src/components/WindowTopMenuButton.js +++ b/src/components/WindowTopMenuButton.js @@ -1,5 +1,4 @@ import React, { Component } from 'react'; -import { compose } from 'redux'; import IconButton from '@material-ui/core/IconButton'; import MoreVertIcon from '@material-ui/icons/MoreVert'; import { withStyles } from '@material-ui/core/styles'; @@ -44,14 +43,14 @@ class WindowTopMenuButton extends Component { * @return */ render() { - const { classes, windowId } = this.props; + const { classes, t, windowId } = this.props; const { anchorEl } = this.state; return ( <> <IconButton - color="primary" - aria-label="Menu" + color="inherit" + aria-label={t('menu')} className={classes.ctrlBtn} aria-haspopup="true" onClick={this.handleMenuClick} @@ -72,6 +71,11 @@ class WindowTopMenuButton extends Component { WindowTopMenuButton.propTypes = { windowId: PropTypes.string.isRequired, classes: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types + t: PropTypes.func, +}; + +WindowTopMenuButton.defaultProps = { + t: key => key, }; /** @@ -83,10 +87,4 @@ const styles = theme => ({ }, }); - -const enhance = compose( - withStyles(styles), - // further HOC go here -); - -export default enhance(WindowTopMenuButton); +export default withStyles(styles)(WindowTopMenuButton); diff --git a/src/components/WorkspaceAddButton.js b/src/components/WorkspaceAddButton.js index 83936f5c003266e3995e8a0331ad51bf5ff0f5b8..7e37ef16667e28f073280ebb0268d473cff181c7 100644 --- a/src/components/WorkspaceAddButton.js +++ b/src/components/WorkspaceAddButton.js @@ -60,7 +60,7 @@ class WorkspaceAddButton extends Component { * @return */ render() { - const { classes, manifests } = this.props; + const { classes, t, manifests } = this.props; const { lastRequested, anchorEl } = this.state; const manifestList = Object.keys(manifests).map(manifest => ( @@ -76,7 +76,7 @@ class WorkspaceAddButton extends Component { <Fab color="primary" id="addBtn" - aria-label="Add" + aria-label={t('add')} className={classes.fab} aria-owns={anchorEl ? 'add-form' : undefined} aria-haspopup="true" @@ -105,6 +105,11 @@ class WorkspaceAddButton extends Component { WorkspaceAddButton.propTypes = { manifests: PropTypes.instanceOf(Object).isRequired, classes: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types + t: PropTypes.func, +}; + +WorkspaceAddButton.defaultProps = { + t: key => key, }; /** diff --git a/src/components/WorkspaceControlPanelButtons.js b/src/components/WorkspaceControlPanelButtons.js index 0122894c567d9845be21f27a7d944c8ff1a2c34d..a9f75a68f0864bab7aa1df954c3483037010a902 100644 --- a/src/components/WorkspaceControlPanelButtons.js +++ b/src/components/WorkspaceControlPanelButtons.js @@ -3,7 +3,8 @@ import PropTypes from 'prop-types'; import List from '@material-ui/core/List'; import WorkspaceFullScreenButton from '../containers/WorkspaceFullScreenButton'; import WorkspaceAddButton from '../containers/WorkspaceAddButton'; -import WorkspaceMenuButton from './WorkspaceMenuButton'; +import WorkspaceMenuButton from '../containers/WorkspaceMenuButton'; + /** * */ diff --git a/src/components/WorkspaceExport.js b/src/components/WorkspaceExport.js index 8bbbb6e50d81673d155a65185b5f002609ae9221..9407576c5ca1259923c6089c231f28251a8345be 100644 --- a/src/components/WorkspaceExport.js +++ b/src/components/WorkspaceExport.js @@ -26,11 +26,11 @@ class WorkspaceExport extends Component { */ render() { const { - handleClose, open, children, + handleClose, open, children, t, } = this.props; return ( <Dialog id="workspace-settings" open={open} onClose={handleClose}> - <DialogTitle id="form-dialog-title">Download/Export</DialogTitle> + <DialogTitle id="form-dialog-title">{t('downloadExport')}</DialogTitle> <DialogContent> {children} <pre> @@ -47,11 +47,13 @@ WorkspaceExport.propTypes = { open: PropTypes.bool, // eslint-disable-line react/forbid-prop-types children: PropTypes.node, state: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types + t: PropTypes.func, }; WorkspaceExport.defaultProps = { open: false, children: null, + t: key => key, }; export default WorkspaceExport; diff --git a/src/components/WorkspaceFullScreenButton.js b/src/components/WorkspaceFullScreenButton.js index 2c6a3685bb5a3aafb442a1699877736a250b0e17..96bac8ab1b0f6ea5fbd744b51227a213939677b6 100644 --- a/src/components/WorkspaceFullScreenButton.js +++ b/src/components/WorkspaceFullScreenButton.js @@ -13,10 +13,10 @@ class WorkspaceFullScreenButton extends Component { * @return */ render() { - const { classes, setWorkspaceFullscreen } = this.props; + const { classes, setWorkspaceFullscreen, t } = this.props; return ( <ListItem> - <IconButton className={classes.ctrlBtn} aria-label="Full Screen" onClick={() => setWorkspaceFullscreen(true)}> + <IconButton className={classes.ctrlBtn} aria-label={t('fullScreen')} onClick={() => setWorkspaceFullscreen(true)}> <FullscreenIcon /> </IconButton> </ListItem> @@ -27,6 +27,11 @@ class WorkspaceFullScreenButton extends Component { WorkspaceFullScreenButton.propTypes = { setWorkspaceFullscreen: PropTypes.func.isRequired, classes: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types + t: PropTypes.func, +}; + +WorkspaceFullScreenButton.defaultProps = { + t: key => key, }; /** diff --git a/src/components/WorkspaceMenu.js b/src/components/WorkspaceMenu.js index bcb016dab8b2d5372b2d0925a270edb1daf4dd57..9f37eff027313e9c5279c2fa5ea040a1cf5fc2d9 100644 --- a/src/components/WorkspaceMenu.js +++ b/src/components/WorkspaceMenu.js @@ -58,7 +58,7 @@ class WorkspaceMenu extends Component { * @return */ render() { - const { handleClose, anchorEl } = this.props; + const { handleClose, anchorEl, t } = this.props; const { windowList, settings, exportWorkspace } = this.state; return ( @@ -72,7 +72,7 @@ class WorkspaceMenu extends Component { <ListItemIcon> <ViewHeadlineIcon /> </ListItemIcon> - <Typography varient="inherit">List all open windows</Typography> + <Typography varient="inherit">{t('listAllOpenWindows')}</Typography> </MenuItem> <Divider /> <MenuItem @@ -83,7 +83,7 @@ class WorkspaceMenu extends Component { <ListItemIcon> <SettingsIcon /> </ListItemIcon> - <Typography varient="inherit">Settings</Typography> + <Typography varient="inherit">{t('settings')}</Typography> </MenuItem> <MenuItem aria-haspopup="true" @@ -93,7 +93,7 @@ class WorkspaceMenu extends Component { <ListItemIcon> <SaveAltIcon /> </ListItemIcon> - <Typography varient="inherit">Download/export workspace</Typography> + <Typography varient="inherit">{t('downloadExportWorkspace')}</Typography> </MenuItem> </Menu> <WindowList @@ -117,10 +117,12 @@ class WorkspaceMenu extends Component { WorkspaceMenu.propTypes = { handleClose: PropTypes.func.isRequired, anchorEl: PropTypes.object, // eslint-disable-line react/forbid-prop-types + t: PropTypes.func, }; WorkspaceMenu.defaultProps = { anchorEl: null, + t: key => key, }; export default WorkspaceMenu; diff --git a/src/components/WorkspaceMenuButton.js b/src/components/WorkspaceMenuButton.js index 3f2a60cb99ec38933f20d5d939924d5836ba287e..913acac4a1efd36c79a16501db09bd744021be54 100644 --- a/src/components/WorkspaceMenuButton.js +++ b/src/components/WorkspaceMenuButton.js @@ -4,7 +4,7 @@ import MenuIcon from '@material-ui/icons/Menu'; import ListItem from '@material-ui/core/ListItem'; import { withStyles } from '@material-ui/core/styles'; import PropTypes from 'prop-types'; -import WorkspaceMenu from './WorkspaceMenu'; +import WorkspaceMenu from '../containers/WorkspaceMenu'; /** */ @@ -44,16 +44,16 @@ class WorkspaceMenuButton extends Component { * @return */ render() { - const { classes } = this.props; + const { classes, t } = this.props; const { anchorEl } = this.state; return ( <> <ListItem> <IconButton - color="primary" + color="default" id="menuBtn" - aria-label="Menu" + aria-label={t('menu')} className={classes.ctrlBtn} aria-haspopup="true" onClick={this.handleMenuClick} @@ -73,6 +73,11 @@ class WorkspaceMenuButton extends Component { WorkspaceMenuButton.propTypes = { classes: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types + t: PropTypes.func, +}; + +WorkspaceMenuButton.defaultProps = { + t: key => key, }; /** diff --git a/src/components/WorkspaceMosaic.js b/src/components/WorkspaceMosaic.js index 8a7b0b0eb6326d0a3c581bfbf5f10244a280fe9b..a4ba5bd7a5cec1210927d5e7e17562617a9bcab1 100644 --- a/src/components/WorkspaceMosaic.js +++ b/src/components/WorkspaceMosaic.js @@ -52,7 +52,7 @@ class WorkspaceMosaic extends React.Component { */ determineWorkspaceLayout() { const { windows, workspace } = this.props; - const windowKeys = Object.keys(windows); + const windowKeys = Object.keys(windows).sort(); const leaveKeys = getLeaves(workspace.layout); // Check every window is in the layout, and all layout windows are present // in store diff --git a/src/components/WorkspaceSettings.js b/src/components/WorkspaceSettings.js index fbd7c3f2c8f3c35a2c3f2eac1451571b84ca31a2..bf84a8f552ac544d1242850b977792a410f41323 100644 --- a/src/components/WorkspaceSettings.js +++ b/src/components/WorkspaceSettings.js @@ -34,15 +34,15 @@ class WorkspaceSettings extends Component { */ render() { const { - handleClose, open, children, theme, + handleClose, open, children, theme, t, } = this.props; return ( <Dialog id="workspace-settings" open={open} onClose={handleClose}> - <DialogTitle id="form-dialog-title">Settings</DialogTitle> + <DialogTitle id="form-dialog-title">{t('settings')}</DialogTitle> <DialogContent> {children} <FormControl> - <InputLabel htmlFor="theme-simple">Theme</InputLabel> + <InputLabel htmlFor="theme-simple">{t('theme')}</InputLabel> <Select value={theme} onChange={this.handleThemeChange} @@ -51,8 +51,8 @@ class WorkspaceSettings extends Component { id: 'theme-simple', }} > - <MenuItem value="light">Light</MenuItem> - <MenuItem value="dark">Dark</MenuItem> + <MenuItem value="light">{t('light')}</MenuItem> + <MenuItem value="dark">{t('dark')}</MenuItem> </Select> </FormControl> </DialogContent> @@ -67,11 +67,13 @@ WorkspaceSettings.propTypes = { children: PropTypes.node, updateConfig: PropTypes.func.isRequired, theme: PropTypes.string.isRequired, + t: PropTypes.func, }; WorkspaceSettings.defaultProps = { open: false, children: null, + t: key => key, }; export default WorkspaceSettings; diff --git a/src/containers/App.js b/src/containers/App.js index aa351221c3c86f47cff6e27be3c63c9b170aacd9..1806662aebf92f736f8fe3038c6d733ca73d54e3 100644 --- a/src/containers/App.js +++ b/src/containers/App.js @@ -10,9 +10,8 @@ import App from '../components/App'; */ const mapStateToProps = state => ( { - config: state.config, - workspace: state.workspace, - manifests: state.manifests, + theme: state.config.theme, + isFullscreenEnabled: state.workspace.isFullscreenEnabled, } ); @@ -22,7 +21,6 @@ const mapStateToProps = state => ( * @private */ const mapDispatchToProps = { - fetchManifest: actions.fetchManifest, setWorkspaceFullscreen: actions.setWorkspaceFullscreen, }; diff --git a/src/containers/ManifestForm.js b/src/containers/ManifestForm.js index 2f47b8cba5d401741246c860b44be0f6aa782750..baae7e3c29edc9fc0d9d1967acbee96c0d274228 100644 --- a/src/containers/ManifestForm.js +++ b/src/containers/ManifestForm.js @@ -1,4 +1,6 @@ import { connect } from 'react-redux'; +import { compose } from 'redux'; +import { withNamespaces } from 'react-i18next'; import * as actions from '../state/actions'; import ManifestForm from '../components/ManifestForm'; @@ -9,5 +11,10 @@ import ManifestForm from '../components/ManifestForm'; */ const mapDispatchToProps = { fetchManifest: actions.fetchManifest }; +const enhance = compose( + connect(null, mapDispatchToProps), + withNamespaces(), + // further HOC go here +); -export default connect(null, mapDispatchToProps)(ManifestForm); +export default enhance(ManifestForm); diff --git a/src/containers/ThumbnailNavigation.js b/src/containers/ThumbnailNavigation.js index 9ff864133eef034210f54e973b0e812d9b9f41a1..0f7b5658f41d6a193347fe3cc898b18d54668eb4 100644 --- a/src/containers/ThumbnailNavigation.js +++ b/src/containers/ThumbnailNavigation.js @@ -3,13 +3,14 @@ import { connect } from 'react-redux'; import miradorWithPlugins from '../lib/miradorWithPlugins'; import * as actions from '../state/actions'; import ThumbnailNavigation from '../components/ThumbnailNavigation'; - +import { getManifestCanvases } from '../state/selectors'; /** * mapStateToProps - used to hook up state to props * @memberof ThumbnailNavigation * @private */ -const mapStateToProps = ({ config }) => ({ +const mapStateToProps = ({ config }, { manifest }) => ({ + canvases: getManifestCanvases(manifest), config, }); diff --git a/src/containers/WindowIcon.js b/src/containers/WindowIcon.js new file mode 100644 index 0000000000000000000000000000000000000000..49e61e703c4b8c3c820b674fc009748a4e0c60ce --- /dev/null +++ b/src/containers/WindowIcon.js @@ -0,0 +1,10 @@ +import { connect } from 'react-redux'; +import { getWindowManifest, getManifestLogo } from '../state/selectors'; +import WindowIcon from '../components/WindowIcon'; + +/** */ +const mapStateToProps = (state, { windowId }) => ({ + manifestLogo: getManifestLogo(getWindowManifest(state, windowId)), +}); + +export default connect(mapStateToProps)(WindowIcon); diff --git a/src/containers/WindowList.js b/src/containers/WindowList.js index 2350e6831221fc8fda24aae91e3e7cd0b9a66669..86814d210609682ed68a4259d967afa028d1f23c 100644 --- a/src/containers/WindowList.js +++ b/src/containers/WindowList.js @@ -1,4 +1,6 @@ import { connect } from 'react-redux'; +import { compose } from 'redux'; +import { withNamespaces } from 'react-i18next'; import * as actions from '../state/actions'; import WindowList from '../components/WindowList'; @@ -23,4 +25,10 @@ const mapStateToProps = state => ( } ); -export default connect(mapStateToProps, mapDispatchToProps)(WindowList); +const enhance = compose( + connect(mapStateToProps, mapDispatchToProps), + withNamespaces(), + // further HOC +); + +export default enhance(WindowList); diff --git a/src/containers/WindowSideBarButtons.js b/src/containers/WindowSideBarButtons.js index 93a58e23f8b1ab6288bda18cc31f7b730e02c641..e4a652b3483c0e250c4d81a0f7fb49d13bf650aa 100644 --- a/src/containers/WindowSideBarButtons.js +++ b/src/containers/WindowSideBarButtons.js @@ -1,5 +1,6 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; +import { withNamespaces } from 'react-i18next'; import * as actions from '../state/actions'; import miradorWithPlugins from '../lib/miradorWithPlugins'; import WindowSideBarButtons from '../components/WindowSideBarButtons'; @@ -19,6 +20,7 @@ const mapDispatchToProps = (dispatch, props) => ({ const enhance = compose( connect(null, mapDispatchToProps), miradorWithPlugins, + withNamespaces(), // further HOC go here ); diff --git a/src/containers/WindowSideBarInfoPanel.js b/src/containers/WindowSideBarInfoPanel.js new file mode 100644 index 0000000000000000000000000000000000000000..0e2434944b72965b9656fc6a828a77b7ab0f2b43 --- /dev/null +++ b/src/containers/WindowSideBarInfoPanel.js @@ -0,0 +1,12 @@ +import { compose } from 'redux'; +import { withNamespaces } from 'react-i18next'; +import miradorWithPlugins from '../lib/miradorWithPlugins'; +import WindowSideBarInfoPanel from '../components/WindowSideBarInfoPanel'; + +const enhance = compose( + withNamespaces(), + miradorWithPlugins, + // further HOC +); + +export default enhance(WindowSideBarInfoPanel); diff --git a/src/containers/WindowThumbnailSettings.js b/src/containers/WindowThumbnailSettings.js index 75b0e947253ea17e06667eb8d7bfa27f146fb56b..a0463090f5707e1289ab7e8a47e208fc505e19b3 100644 --- a/src/containers/WindowThumbnailSettings.js +++ b/src/containers/WindowThumbnailSettings.js @@ -1,6 +1,9 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; +import { withNamespaces } from 'react-i18next'; +import miradorWithPlugins from '../lib/miradorWithPlugins'; import * as actions from '../state/actions'; +import { getThumbnailNavigationPosition } from '../state/selectors'; import WindowThumbnailSettings from '../components/WindowThumbnailSettings'; /** @@ -17,12 +20,14 @@ const mapDispatchToProps = { setWindowThumbnailPosition: actions.setWindowThumbn */ const mapStateToProps = (state, props) => ( { - thumbnailNavigationPosition: state.windows[props.windowId].thumbnailNavigationPosition, + thumbnailNavigationPosition: getThumbnailNavigationPosition(state, props.windowId), } ); const enhance = compose( connect(mapStateToProps, mapDispatchToProps), + withNamespaces(), + miradorWithPlugins, // further HOC go here ); diff --git a/src/containers/WindowTopBar.js b/src/containers/WindowTopBar.js index 2f1a0bbdabafd958df5ab85f0cbbbff151c85054..0bc54458ae7b4ef4712ece6e2f422459c746b68e 100644 --- a/src/containers/WindowTopBar.js +++ b/src/containers/WindowTopBar.js @@ -1,5 +1,6 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; +import { withNamespaces } from 'react-i18next'; import * as actions from '../state/actions'; import miradorWithPlugins from '../lib/miradorWithPlugins'; import WindowTopBar from '../components/WindowTopBar'; @@ -17,6 +18,7 @@ const mapDispatchToProps = (dispatch, props) => ({ const enhance = compose( connect(null, mapDispatchToProps), miradorWithPlugins, + withNamespaces(), // further HOC go here ); diff --git a/src/containers/WindowTopMenuButton.js b/src/containers/WindowTopMenuButton.js new file mode 100644 index 0000000000000000000000000000000000000000..ebfedfeb3d1d114cd7eabc4ea478137e681fe8fc --- /dev/null +++ b/src/containers/WindowTopMenuButton.js @@ -0,0 +1,12 @@ +import { compose } from 'redux'; +import { withNamespaces } from 'react-i18next'; +import miradorWithPlugins from '../lib/miradorWithPlugins'; +import WindowTopMenuButton from '../components/WindowTopMenuButton'; + +const enhance = compose( + withNamespaces(), + miradorWithPlugins, + // further HOC +); + +export default enhance(WindowTopMenuButton); diff --git a/src/containers/WorkspaceAddButton.js b/src/containers/WorkspaceAddButton.js index b9ed10040df56b50b030ec03866cc9427fd24ce7..d7faf821822af61679adf76eeedd1a521624ef93 100644 --- a/src/containers/WorkspaceAddButton.js +++ b/src/containers/WorkspaceAddButton.js @@ -1,4 +1,7 @@ import { connect } from 'react-redux'; +import { compose } from 'redux'; +import { withNamespaces } from 'react-i18next'; +import miradorWithPlugins from '../lib/miradorWithPlugins'; import WorkspaceAddButton from '../components/WorkspaceAddButton'; /** @@ -12,4 +15,10 @@ const mapStateToProps = state => ( } ); -export default connect(mapStateToProps)(WorkspaceAddButton); +const enhance = compose( + connect(mapStateToProps), + withNamespaces(), + miradorWithPlugins, +); + +export default enhance(WorkspaceAddButton); diff --git a/src/containers/WorkspaceExport.js b/src/containers/WorkspaceExport.js index a90a4670fb89a2dc0a37338be8349c198726f9ae..82dded070825354cb5fffba8ae5d5d3513218597 100644 --- a/src/containers/WorkspaceExport.js +++ b/src/containers/WorkspaceExport.js @@ -1,5 +1,7 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; +import { withNamespaces } from 'react-i18next'; +import miradorWithPlugins from '../lib/miradorWithPlugins'; import WorkspaceExport from '../components/WorkspaceExport'; /** @@ -11,6 +13,8 @@ const mapStateToProps = state => ({ state }); const enhance = compose( connect(mapStateToProps, {}), + miradorWithPlugins, + withNamespaces(), // further HOC go here ); diff --git a/src/containers/WorkspaceFullScreenButton.js b/src/containers/WorkspaceFullScreenButton.js index 60ccaa92c4331f6129ffd9b50595c01f656255dd..64f9bd73242ac485818d9b53a94ec1ea9196a16e 100644 --- a/src/containers/WorkspaceFullScreenButton.js +++ b/src/containers/WorkspaceFullScreenButton.js @@ -1,4 +1,7 @@ import { connect } from 'react-redux'; +import { compose } from 'redux'; +import { withNamespaces } from 'react-i18next'; +import miradorWithPlugins from '../lib/miradorWithPlugins'; import * as actions from '../state/actions'; import WorkspaceFullScreenButton from '../components/WorkspaceFullScreenButton'; @@ -10,4 +13,11 @@ import WorkspaceFullScreenButton */ const mapDispatchToProps = { setWorkspaceFullscreen: actions.setWorkspaceFullscreen }; -export default connect(null, mapDispatchToProps)(WorkspaceFullScreenButton); +const enhance = compose( + connect(null, mapDispatchToProps), + withNamespaces(), + miradorWithPlugins, + // further HOC +); + +export default enhance(WorkspaceFullScreenButton); diff --git a/src/containers/WorkspaceMenu.js b/src/containers/WorkspaceMenu.js new file mode 100644 index 0000000000000000000000000000000000000000..866358d4fede4ed20468237fbf47e8cb0288c2b6 --- /dev/null +++ b/src/containers/WorkspaceMenu.js @@ -0,0 +1,12 @@ +import { compose } from 'redux'; +import { withNamespaces } from 'react-i18next'; +import miradorWithPlugins from '../lib/miradorWithPlugins'; +import WorkspaceMenu from '../components/WorkspaceMenu'; + +const enhance = compose( + withNamespaces(), + miradorWithPlugins, + // further HOC +); + +export default enhance(WorkspaceMenu); diff --git a/src/containers/WorkspaceMenuButton.js b/src/containers/WorkspaceMenuButton.js new file mode 100644 index 0000000000000000000000000000000000000000..0ccd4218f13526ebaac8a74b08b4b81abe17eb1a --- /dev/null +++ b/src/containers/WorkspaceMenuButton.js @@ -0,0 +1,12 @@ +import { compose } from 'redux'; +import { withNamespaces } from 'react-i18next'; +import miradorWithPlugins from '../lib/miradorWithPlugins'; +import WorkspaceMenuButton from '../components/WorkspaceMenuButton'; + +const enhance = compose( + withNamespaces(), + miradorWithPlugins, + // further HOC +); + +export default enhance(WorkspaceMenuButton); diff --git a/src/containers/WorkspaceSettings.js b/src/containers/WorkspaceSettings.js index a52b463006165a812e01d231e9144683fc600838..50b6971352e0d0d3ec10e975d20410732cc7fc47 100644 --- a/src/containers/WorkspaceSettings.js +++ b/src/containers/WorkspaceSettings.js @@ -1,5 +1,6 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; +import { withNamespaces } from 'react-i18next'; import WorkspaceSettings from '../components/WorkspaceSettings'; import * as actions from '../state/actions'; @@ -25,6 +26,7 @@ const mapStateToProps = state => ( const enhance = compose( connect(mapStateToProps, mapDispatchToProps), + withNamespaces(), // further HOC go here ); diff --git a/src/i18n.js b/src/i18n.js new file mode 100644 index 0000000000000000000000000000000000000000..dac6958f05709d746f951f1f33fb68cbc2f62a6f --- /dev/null +++ b/src/i18n.js @@ -0,0 +1,22 @@ +import i18n from 'i18next'; +import { reactI18nextModule } from 'react-i18next'; +import en from '../locales/en/translation.json'; + +// Load translations for each language +const resources = { + en, +}; + +i18n + .use(reactI18nextModule) + .init({ + resources, + lng: 'en', + fallbackLng: 'en', + + interpolation: { + escapeValue: false, // react is already safe from xss + }, + }); + +export default i18n; diff --git a/src/lib/MiradorViewer.js b/src/lib/MiradorViewer.js index 3bd09c9171fd2d27ac310784449093394a57a5c6..2fd7d8b5558780c8d4e97db1ac81ed04bfd8870f 100644 --- a/src/lib/MiradorViewer.js +++ b/src/lib/MiradorViewer.js @@ -1,12 +1,14 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; +import { I18nextProvider } from 'react-i18next'; import deepmerge from 'deepmerge'; import App from '../containers/App'; import createRootReducer from '../state/reducers/index'; import createStore from '../state/createStore'; import * as actions from '../state/actions'; import settings from '../config/settings'; +import i18n from '../i18n'; /** * Default Mirador instantiation @@ -26,7 +28,9 @@ class MiradorViewer { ReactDOM.render( <Provider store={this.store}> - <App config={config} /> + <I18nextProvider i18n={i18n}> + <App config={config} /> + </I18nextProvider> </Provider>, document.getElementById(config.id), ); diff --git a/src/state/selectors/index.js b/src/state/selectors/index.js new file mode 100644 index 0000000000000000000000000000000000000000..7cc2c24eca30ff1640fd5ac163fb46ee0975b5a5 --- /dev/null +++ b/src/state/selectors/index.js @@ -0,0 +1,45 @@ + +/** +* Return the manifest that belongs to a certain window. +* @param {object} state +* @param {String} windowId +* @return {object} +*/ +export function getWindowManifest(state, windowId) { + return state.windows[windowId] + && state.windows[windowId].manifestId + && state.manifests[state.windows[windowId].manifestId]; +} + +/** +* Return the logo of a manifest or null +* @param {object} manifest +* @return {String|null} +*/ +export function getManifestLogo(manifest) { + return manifest.manifestation + && manifest.manifestation.getLogo(); +} + +/** +* Return the logo of a manifest or null +* @param {object} manifest +* @return {String|null} +*/ +export function getManifestCanvases(manifest) { + if (!manifest.manifestation) { + return []; + } + + return manifest.manifestation.getSequences()[0].getCanvases(); +} + +/** Return position of thumbnail navigation in a certain window. +* @param {object} state +* @param {String} windowId +* @param {String} +*/ +export function getThumbnailNavigationPosition(state, windowId) { + return state.windows[windowId] + && state.windows[windowId].thumbnailNavigationPosition; +}