diff --git a/.eslintrc b/.eslintrc index 4ba53d9675b1d23768bd88ef1c480261032c41f3..35d92042471a480ef2bfb832d34104b7531dd648 100644 --- a/.eslintrc +++ b/.eslintrc @@ -30,5 +30,6 @@ }], "react/jsx-props-no-spreading": "off", "arrow-parens": "off", + "import/no-anonymous-default-export": "off" } } diff --git a/.gitignore b/.gitignore index 6d95dc1313eab6b5524d10b157d243036153178e..e6ef744167cee4caf3da2acfd18c37ce8edd25b6 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ coverage/ node_modules/ package-lock.json *.log +*.tgz \ No newline at end of file diff --git a/README.md b/README.md index 65fe992f8829ef5ec99cbcc5af150d887547e385..fa15415b22868f246d5ef0949962b3dde6a943c8 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ https://dzkimgs.l.u-tokyo.ac.jp/videos/m3/mirador.min.js This project is dual-licensed under the Apache License 2.0 and the MIT license. See [LICENSE](LICENSE) for details. --- -*NOTE: This README reflects the latest version of Mirador, Mirador 3. For previous versions, please reference that release's README directly. Latest 2.x release: [v.2.7.0](https://github.com/ProjectMirador/mirador/tree/v2.7.0)* +⚠️ This project is for Mirador 3, the latest version of Mirador. For Mirador 2, please see [ProjectMirador/mirador2](https://github.com/projectmirador/mirador2) or legacy documentation on the [Mirador 2 wiki](https://github.com/ProjectMirador/mirador-2-wiki/wiki). Please note that the community's focus is on Mirador 3, and are unlikely to accept pull requests or provide support for Mirador 2. # Mirador  [](https://codecov.io/gh/ProjectMirador/mirador) @@ -105,6 +105,20 @@ $ npm run lint ``` ## Debugging -Useful browser extensions for debugging/development purposes + +### Local instance + +The following browser extensions are useful for debugging a local development instance of Mirador: + - [React DevTools](https://github.com/facebook/react-devtools) - [Redux DevTools](https://github.com/zalmoxisus/redux-devtools-extension) + +### Test suite + +To debug the test suite, run: + +```sh +$ npm run test:debug +``` + +then spin up a [nodejs inspector client](https://nodejs.org/en/docs/guides/debugging-getting-started/#inspector-clients) and set some breakpoints. See [here](https://www.digitalocean.com/community/tutorials/how-to-debug-node-js-with-the-built-in-debugger-and-chrome-devtools#step-3-%E2%80%94-debugging-node-js-with-chrome-devtools) for a guide to debugging with Chrome DevTools. diff --git a/__tests__/integration/mirador/index.html b/__tests__/integration/mirador/index.html index f40c058a38e5cce5a35c53a3cd01ed679081600a..43113f0074056a89e99fa96d4f61c093f238b3c1 100644 --- a/__tests__/integration/mirador/index.html +++ b/__tests__/integration/mirador/index.html @@ -38,7 +38,7 @@ { manifestId: "https://iiif.biblissima.fr/chateauroux/B360446201_MS0005/manifest.json", provider: "Biblissima"}, { manifestId: "https://iiif.durham.ac.uk/manifests/trifle/32150/t1/m4/q7/t1m4q77fr328/manifest", provider: "Durham University Library"}, //{ manifestId: "https://iiif.vam.ac.uk/collections/O1023003/manifest.json", provider: "Ocean liners"}, - { manifestId: "https://zavicajna.digitalna.rs/iiif/iiif/api/presentation/2/4aa44ad1-0b74-4590-ab09-534a38cb7c53%252F00000001%252Fostalo01%252F00000012/manifest", provider: "Библиотека 'Милутин Бојић'"}, + { manifestId: "https://zavicajna.digitalna.rs/iiif/iiif/api/presentation/3/96571949-03d6-478e-ab44-a2d5ad68f935%252F00000001%252Fostalo01%252F00000071/manifest", provider: "Библиотека 'Милутин Бојић'"}, ] }); </script> diff --git a/__tests__/integration/mirador/video.html b/__tests__/integration/mirador/video.html index f43c9ed04cd593ac04afe4498cce8d7b81c495f0..8f8d77aab2e81a6bd50644a5f7bb94707eda425c 100644 --- a/__tests__/integration/mirador/video.html +++ b/__tests__/integration/mirador/video.html @@ -24,7 +24,7 @@ manifestId: 'https://iiif.io/api/cookbook/recipe/0014-accompanyingcanvas/manifest.json' }, { - manifestId: 'https://iiif-commons.github.io/iiif-av-component/examples/data/iiif/lunchroom-manners.json' + manifestId: 'https://preview.iiif.io/cookbook/0219-using-caption-file/recipe/0219-using-caption-file/manifest.json' } ], }); diff --git a/__tests__/src/actions/window.test.js b/__tests__/src/actions/window.test.js index e7599b5d4b8f3b65aec0f6537d67f16ce662bba3..27eae582383cfe743c028569691d5105e391b6bd 100644 --- a/__tests__/src/actions/window.test.js +++ b/__tests__/src/actions/window.test.js @@ -45,7 +45,7 @@ describe('window actions', () => { ], elasticLayout: { height: 400, - width: 400, + width: 480, x: 260, y: 300, }, @@ -152,6 +152,31 @@ describe('window actions', () => { expect(action.companionWindows[0]).toMatchObject({ content: 'thumbnailNavigation' }); }); + it('enables a window to override the panel being displayed', () => { + const options = { + id: 'helloworld', + sideBarPanel: 'canvas', + }; + const mockState = { + companionWindows: {}, + config: { + thumbnailNavigation: {}, + window: { + defaultSideBarPanel: 'info', + }, + }, + workspace: {}, + }; + const mockDispatch = jest.fn(() => ({})); + const mockGetState = jest.fn(() => mockState); + const thunk = actions.addWindow(options); + + thunk(mockDispatch, mockGetState); + + const action = mockDispatch.mock.calls[0][0]; + expect(action.window.sideBarPanel).toEqual('canvas'); + }); + it('pulls a provided manifest out', () => { const options = { canvasIndex: 1, diff --git a/__tests__/src/components/AnnotationsOverlay.test.js b/__tests__/src/components/AnnotationsOverlay.test.js index ce3af690541cf14b44b6c47ed9bb559bdf91adf0..83459281043dd6a8eeeca347f7fe98937a9420c7 100644 --- a/__tests__/src/components/AnnotationsOverlay.test.js +++ b/__tests__/src/components/AnnotationsOverlay.test.js @@ -1,7 +1,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import OpenSeadragon from 'openseadragon'; -import { Utils } from 'manifesto.js/dist-esmodule/Utils'; +import { Utils } from 'manifesto.js'; import { AnnotationsOverlay } from '../../../src/components/AnnotationsOverlay'; import OpenSeadragonCanvasOverlay from '../../../src/lib/OpenSeadragonCanvasOverlay'; import AnnotationList from '../../../src/lib/AnnotationList'; diff --git a/__tests__/src/components/AudioViewer.test.js b/__tests__/src/components/AudioViewer.test.js index 1e807aaeeb4c5c2f4a2e7e5b748cd7b4c83ec9e7..cca67ee5dd9f4f10729c6a0a0dc955795025dadc 100644 --- a/__tests__/src/components/AudioViewer.test.js +++ b/__tests__/src/components/AudioViewer.test.js @@ -7,6 +7,7 @@ function createWrapper(props, suspenseFallback) { return shallow( <AudioViewer classes={{}} + audioOptions={{ crossOrigin: 'anonymous' }} {...props} />, ); @@ -22,8 +23,16 @@ describe('AudioViewer', () => { { getFormat: () => 'video/mp4', id: 2 }, ], }, true); - expect(wrapper.contains(<source src="1" type="video/mp4" />)); - expect(wrapper.contains(<source src="2" type="video/mp4" />)); + expect(wrapper.contains(<source src={1} type="video/mp4" />)).toBe(true); + expect(wrapper.contains(<source src={2} type="video/mp4" />)).toBe(true); + }); + it('passes through configurable options', () => { + wrapper = createWrapper({ + audioResources: [ + { getFormat: () => 'audio/mp3', id: 1 }, + ], + }, true); + expect(wrapper.exists('audio[crossOrigin="anonymous"]')).toBe(true); // eslint-disable-line jsx-a11y/media-has-caption }); it('captions', () => { wrapper = createWrapper({ @@ -31,12 +40,12 @@ describe('AudioViewer', () => { { getFormat: () => 'video/mp4', id: 1 }, ], captions: [ - { getLabel: () => 'English', getProperty: () => 'en', id: 1 }, - { getLabel: () => 'French', getProperty: () => 'fr', id: 2 }, + { getDefaultLabel: () => 'English', getProperty: () => 'en', id: 1 }, + { getDefaultLabel: () => 'French', getProperty: () => 'fr', id: 2 }, ], }, true); - expect(wrapper.contains(<track src="1" label="English" srcLang="en" />)); - expect(wrapper.contains(<track src="2" label="French" srcLang="fr" />)); + expect(wrapper.contains(<track src={1} label="English" srcLang="en" />)).toBe(true); + expect(wrapper.contains(<track src={2} label="French" srcLang="fr" />)).toBe(true); }); }); }); diff --git a/__tests__/src/components/CanvasAnnotations.test.js b/__tests__/src/components/CanvasAnnotations.test.js index b68f0e367ff857dc0d02c82f1e855aab20a7c4a8..63f0674ddb3bcd05b49eb131005d812fd99ecb75 100644 --- a/__tests__/src/components/CanvasAnnotations.test.js +++ b/__tests__/src/components/CanvasAnnotations.test.js @@ -5,6 +5,7 @@ import Chip from '@material-ui/core/Chip'; import MenuList from '@material-ui/core/MenuList'; import MenuItem from '@material-ui/core/MenuItem'; import { CanvasAnnotations } from '../../../src/components/CanvasAnnotations'; +import { ScrollTo } from '../../../src/components/ScrollTo'; /** Utility function to wrap CanvasAnnotations */ function createWrapper(props) { @@ -62,6 +63,14 @@ describe('CanvasAnnotations', () => { expect(wrapper.find(MenuItem).length).toEqual(2); }); + it('scrolls to the selected annotation', () => { + wrapper = createWrapper({ annotations, selectedAnnotationId: 'abc123' }); + + expect(wrapper.find(ScrollTo).length).toEqual(2); + expect(wrapper.find(ScrollTo).first().prop('scrollTo')).toEqual(true); + expect(wrapper.find(ScrollTo).last().prop('scrollTo')).toEqual(false); + }); + it('renders a Chip for every tag', () => { wrapper = createWrapper({ annotations }); diff --git a/__tests__/src/components/CollectionDialog.test.js b/__tests__/src/components/CollectionDialog.test.js index 5c0a08380ec0683b65db3b1c6d597d6e0f47d257..2b057a7d100d217269cc45e1107198a0bfe828ad 100644 --- a/__tests__/src/components/CollectionDialog.test.js +++ b/__tests__/src/components/CollectionDialog.test.js @@ -5,7 +5,7 @@ import DialogActions from '@material-ui/core/DialogActions'; import Button from '@material-ui/core/Button'; import MenuItem from '@material-ui/core/MenuItem'; import Skeleton from '@material-ui/lab/Skeleton'; -import { Utils } from 'manifesto.js/dist-esmodule/Utils'; +import { Utils } from 'manifesto.js'; import { CollectionDialog } from '../../../src/components/CollectionDialog'; import collection from '../../fixtures/version-2/collection.json'; diff --git a/__tests__/src/components/CompanionArea.test.js b/__tests__/src/components/CompanionArea.test.js index 92414cc4d738acc76842516abd61c8509b9b8a4e..6e390ad13632ff646f0ee7c8c7996cc170aaaba3 100644 --- a/__tests__/src/components/CompanionArea.test.js +++ b/__tests__/src/components/CompanionArea.test.js @@ -1,6 +1,8 @@ import React from 'react'; import { shallow } from 'enzyme'; import Slide from '@material-ui/core/Slide'; +import ArrowLeftIcon from '@material-ui/icons/ArrowLeftSharp'; +import ArrowRightIcon from '@material-ui/icons/ArrowRightSharp'; import MiradorMenuButton from '../../../src/containers/MiradorMenuButton'; import { CompanionArea } from '../../../src/components/CompanionArea'; import CompanionWindowFactory from '../../../src/containers/CompanionWindowFactory'; @@ -69,7 +71,7 @@ describe('CompanionArea', () => { }); expect(wrapper.find(MiradorMenuButton).length).toBe(1); - expect(wrapper.find(MiradorMenuButton).first().children('ArrowRightSharpIcon').length).toBe(1); + expect(wrapper.find(MiradorMenuButton).first().children(ArrowRightIcon).length).toBe(1); expect(wrapper.find(Slide).prop('direction')).toBe('right'); expect(wrapper.find(MiradorMenuButton).prop('aria-expanded')).toBe(false); expect(wrapper.find('div.mirador-companion-windows').length).toBe(1); @@ -91,7 +93,7 @@ describe('CompanionArea', () => { }); expect(wrapper.find(MiradorMenuButton).length).toBe(1); - expect(wrapper.find(MiradorMenuButton).first().children('ArrowLeftSharpIcon').length).toBe(1); + expect(wrapper.find(MiradorMenuButton).first().children(ArrowLeftIcon).length).toBe(1); expect(wrapper.find(MiradorMenuButton).prop('aria-expanded')).toBe(true); expect(wrapper.find('div.mirador-companion-windows').length).toBe(1); diff --git a/__tests__/src/components/FullScreenButton.test.js b/__tests__/src/components/FullScreenButton.test.js index cdd961c03880bf52e3e64b7c960996d18dff7604..48701a823ad8859c11f73a1b14ee6bcfcc6c8d28 100644 --- a/__tests__/src/components/FullScreenButton.test.js +++ b/__tests__/src/components/FullScreenButton.test.js @@ -1,5 +1,7 @@ import React from 'react'; import { shallow } from 'enzyme'; +import FullscreenIcon from '@material-ui/icons/FullscreenSharp'; +import FullscreenExitIcon from '@material-ui/icons/FullscreenExitSharp'; import MiradorMenuButton from '../../../src/containers/MiradorMenuButton'; import { FullScreenButton } from '../../../src/components/FullScreenButton'; @@ -36,7 +38,7 @@ describe('FullScreenButton', () => { }); it('has the FullscreenIcon', () => { - expect(menuButton.children('FullscreenSharpIcon').length).toBe(1); + expect(menuButton.children(FullscreenIcon).length).toBe(1); }); it('has the proper aria-label i18n key', () => { @@ -58,7 +60,7 @@ describe('FullScreenButton', () => { }); it('has the FullscreenExitIcon', () => { - expect(menuButton.children('FullscreenExitSharpIcon').length).toBe(1); + expect(menuButton.children(FullscreenExitIcon).length).toBe(1); }); it('has the proper aria-label', () => { diff --git a/__tests__/src/components/GalleryView.test.js b/__tests__/src/components/GalleryView.test.js index ba03ef2c10805ee236f43faffb7ff0d4ad38abe6..4a15795c472795bbc44dc813aebd928954cdea30 100644 --- a/__tests__/src/components/GalleryView.test.js +++ b/__tests__/src/components/GalleryView.test.js @@ -1,6 +1,6 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { Utils } from 'manifesto.js/dist-esmodule/Utils'; +import { Utils } from 'manifesto.js'; import Paper from '@material-ui/core/Paper'; import manifestJson from '../../fixtures/version-2/019.json'; import { GalleryView } from '../../../src/components/GalleryView'; diff --git a/__tests__/src/components/GalleryViewThumbnail.test.js b/__tests__/src/components/GalleryViewThumbnail.test.js index 1dd5fb80bcc0f34d5219ded175bd5c985a983f0f..70ab6d78c3a58627245c642fc64b7bc16fce4589 100644 --- a/__tests__/src/components/GalleryViewThumbnail.test.js +++ b/__tests__/src/components/GalleryViewThumbnail.test.js @@ -1,6 +1,6 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { Utils } from 'manifesto.js/dist-esmodule/Utils'; +import { Utils } from 'manifesto.js'; import Chip from '@material-ui/core/Chip'; import IntersectionObserver from '@researchgate/react-intersection-observer'; import manifestJson from '../../fixtures/version-2/019.json'; diff --git a/__tests__/src/components/IIIFThumbnail.test.js b/__tests__/src/components/IIIFThumbnail.test.js index fa6eb72fa33ef5fb6e93b254b532ac2ede40cc57..5b60b112a9f825f34ae7f5d55713f0728d5bda00 100644 --- a/__tests__/src/components/IIIFThumbnail.test.js +++ b/__tests__/src/components/IIIFThumbnail.test.js @@ -72,11 +72,6 @@ describe('IIIFThumbnail', () => { expect(wrapper.find('img').props().style).toMatchObject({ height: 60, width: 50 }); }); - it('relaxes constraints when the image dimensions are unknown', () => { - wrapper = createWrapper({ thumbnail: { url } }); - expect(wrapper.find('img').props().style).toMatchObject({ height: 'auto', width: 'auto' }); - }); - it('constrains what it can when the image dimensions are unknown', () => { wrapper = createWrapper({ maxHeight: 90, thumbnail: { height: 120, url } }); expect(wrapper.find('img').props().style).toMatchObject({ height: 90, width: 'auto' }); diff --git a/__tests__/src/components/LanguageSettings.test.js b/__tests__/src/components/LanguageSettings.test.js index 88909f66fd5d400235e4b8120e44e2e10fc6bb08..ff1731013b2dbeb8deebaabcdfd88f22081b016b 100644 --- a/__tests__/src/components/LanguageSettings.test.js +++ b/__tests__/src/components/LanguageSettings.test.js @@ -2,6 +2,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import ListItemText from '@material-ui/core/ListItemText'; import MenuItem from '@material-ui/core/MenuItem'; +import CheckIcon from '@material-ui/icons/CheckSharp'; import { LanguageSettings } from '../../../src/components/LanguageSettings'; /** @@ -63,7 +64,7 @@ describe('LanguageSettings', () => { wrapper .find(MenuItem) .first() - .find('CheckSharpIcon') + .find(CheckIcon) .length, ).toBe(1); }); diff --git a/__tests__/src/components/ManifestForm.test.js b/__tests__/src/components/ManifestForm.test.js index cb4bb113ad7b3688622d055e76af0f6aa23bb549..4d170be7a8962c0ffe5f2692db9a75e9ba1d4d14 100644 --- a/__tests__/src/components/ManifestForm.test.js +++ b/__tests__/src/components/ManifestForm.test.js @@ -14,11 +14,14 @@ function createWrapper(props) { } describe('ManifestForm', () => { - it('renders', () => { + it('renders nothing if it is not open', () => { const wrapper = createWrapper({ addResourcesOpen: false }); + expect(wrapper.find('ForwardRef(TextField)[label="addManifestUrl"]').length).toBe(0); + }); + + it('renders the form fields', () => { + const wrapper = createWrapper({ addResourcesOpen: true }); expect(wrapper.find('ForwardRef(TextField)[label="addManifestUrl"]').length).toBe(1); - wrapper.setProps({ addResourcesOpen: true }); - expect(wrapper.find('ForwardRef(TextField)[label="addManifestUrl"] input').instance()).toEqual(document.activeElement); expect(wrapper.find('button[type="submit"]').length).toBe(1); }); diff --git a/__tests__/src/components/ManifestListItem.test.js b/__tests__/src/components/ManifestListItem.test.js index 5a6620f1b6bc53b2f8414d1dce90c4b32cf0197a..bd75e02b3d4fe3b48169f61e54e84b0de41e1ea5 100644 --- a/__tests__/src/components/ManifestListItem.test.js +++ b/__tests__/src/components/ManifestListItem.test.js @@ -63,9 +63,9 @@ describe('ManifestListItem', () => { expect(wrapper.find('.mirador-manifest-list-item-provider').children().text()).toEqual('ACME'); }); - it('displays a placeholder provider if no information is given', () => { + it('displays nothing if no information is given', () => { const wrapper = createWrapper(); - expect(wrapper.find('.mirador-manifest-list-item-provider').children().text()).toEqual('addedFromUrl'); + expect(wrapper.find('.mirador-manifest-list-item-provider').children().length).toEqual(0); }); it('displays a collection label for collections', () => { diff --git a/__tests__/src/components/NestedMenu.test.js b/__tests__/src/components/NestedMenu.test.js index a2ce2bcd0f32e3d6cc65b453bdb014733f53af71..f6c49ba3cccc8b3865a1aefef80d8e5b953effa9 100644 --- a/__tests__/src/components/NestedMenu.test.js +++ b/__tests__/src/components/NestedMenu.test.js @@ -2,6 +2,8 @@ import React from 'react'; import { shallow } from 'enzyme'; import ListItemIcon from '@material-ui/core/ListItemIcon'; import ListItemText from '@material-ui/core/ListItemText'; +import ExpandLessIcon from '@material-ui/icons/ExpandLessSharp'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMoreSharp'; import MenuItem from '@material-ui/core/MenuItem'; import { NestedMenu } from '../../../src/components/NestedMenu'; @@ -61,11 +63,11 @@ describe('NestedMenu', () => { wrapper = createWrapper(); expect(wrapper.state().nestedMenuIsOpen).toBe(false); - expect(wrapper.find('ExpandMoreSharpIcon').length).toBe(1); - expect(wrapper.find('ExpandLessSharpIcon').length).toBe(0); + expect(wrapper.find(ExpandMoreIcon).length).toBe(1); + expect(wrapper.find(ExpandLessIcon).length).toBe(0); wrapper.setState({ nestedMenuIsOpen: true }); - expect(wrapper.find('ExpandMoreSharpIcon').length).toBe(0); - expect(wrapper.find('ExpandLessSharpIcon').length).toBe(1); + expect(wrapper.find(ExpandMoreIcon).length).toBe(0); + expect(wrapper.find(ExpandLessIcon).length).toBe(1); }); it("renders the component's children based on the nestedMenuIsOpen state", () => { diff --git a/__tests__/src/components/OpenSeadragonViewer.test.js b/__tests__/src/components/OpenSeadragonViewer.test.js index 46881f1da6097f1a84d03ef754554556b2c61a5d..7dfa0aae873b665d221d505927f71ee30034ec2b 100644 --- a/__tests__/src/components/OpenSeadragonViewer.test.js +++ b/__tests__/src/components/OpenSeadragonViewer.test.js @@ -1,7 +1,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import OpenSeadragon from 'openseadragon'; -import { Utils } from 'manifesto.js/dist-esmodule/Utils'; +import { Utils } from 'manifesto.js'; import { OpenSeadragonViewer } from '../../../src/components/OpenSeadragonViewer'; import CanvasWorld from '../../../src/lib/CanvasWorld'; import fixture from '../../fixtures/version-2/019.json'; diff --git a/__tests__/src/components/PrimaryWindow.test.js b/__tests__/src/components/PrimaryWindow.test.js index d9961f9f14ef37777fb13314e9206f0a711996bc..47c41be28c49a31d30581ed24262dcfd2f4c1093 100644 --- a/__tests__/src/components/PrimaryWindow.test.js +++ b/__tests__/src/components/PrimaryWindow.test.js @@ -20,6 +20,13 @@ describe('PrimaryWindow', () => { const wrapper = createWrapper(); expect(wrapper.find('.mirador-primary-window')).toHaveLength(1); }); + it('should only render children when available', () => { + const wrapper = createWrapper({ children: <span>hi</span>, isFetching: false }); + expect(wrapper.find('span')).toHaveLength(1); + const suspenseComponent = wrapper.find('Suspense'); + const lazyComponent = suspenseComponent.dive().find('lazy'); + expect(lazyComponent).toHaveLength(0); + }); it('should render <WindowSideBar>', () => { const wrapper = createWrapper(); expect(wrapper.find(WindowSideBar)).toHaveLength(1); diff --git a/__tests__/src/components/SearchPanelControls.test.js b/__tests__/src/components/SearchPanelControls.test.js index a4c14ee48511f3e46707134ebb6786fbb50687af..d09f641230cbb66e5cc7dcec22eaa22c2e3710db 100644 --- a/__tests__/src/components/SearchPanelControls.test.js +++ b/__tests__/src/components/SearchPanelControls.test.js @@ -4,6 +4,7 @@ import Autocomplete from '@material-ui/lab/Autocomplete'; import CircularProgress from '@material-ui/core/CircularProgress'; import Input from '@material-ui/core/Input'; import TextField from '@material-ui/core/TextField'; +import SearchIcon from '@material-ui/icons/SearchSharp'; import { SearchPanelControls } from '../../../src/components/SearchPanelControls'; /** @@ -56,7 +57,7 @@ describe('SearchPanelControls', () => { .dive() .dive(); expect(divedInput.find(CircularProgress).length).toEqual(0); - expect(divedInput.find('SearchSharpIcon').length).toEqual(1); + expect(divedInput.find(SearchIcon).length).toEqual(1); expect(divedInput.find('Connect(WithPlugins(MiradorMenuButton))[type="submit"]').length).toEqual(1); }); diff --git a/__tests__/src/components/SidebarIndexList.test.js b/__tests__/src/components/SidebarIndexList.test.js index 03cad91b6e943a2cf814c316b11e2667a94443e1..364a7646ad812bb8ecfcdc30d41b0604cfa526c1 100644 --- a/__tests__/src/components/SidebarIndexList.test.js +++ b/__tests__/src/components/SidebarIndexList.test.js @@ -2,7 +2,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import MenuList from '@material-ui/core/MenuList'; import MenuItem from '@material-ui/core/MenuItem'; -import { Utils } from 'manifesto.js/dist-esmodule/Utils'; +import { Utils } from 'manifesto.js'; import { SidebarIndexList } from '../../../src/components/SidebarIndexList'; import SidebarIndexItem from '../../../src/containers/SidebarIndexItem'; import manifestJson from '../../fixtures/version-2/019.json'; diff --git a/__tests__/src/components/SidebarIndexTableOfContents.test.js b/__tests__/src/components/SidebarIndexTableOfContents.test.js index 16722421136437be62f4e1a778d24d93fcbf1de3..15c3729ea513c20f2fd7c09197d20262e90e17aa 100644 --- a/__tests__/src/components/SidebarIndexTableOfContents.test.js +++ b/__tests__/src/components/SidebarIndexTableOfContents.test.js @@ -1,6 +1,6 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { Utils } from 'manifesto.js/dist-esmodule/Utils'; +import { Utils } from 'manifesto.js'; import TreeItem from '@material-ui/lab/TreeItem'; import TreeView from '@material-ui/lab/TreeView'; import { SidebarIndexTableOfContents } from '../../../src/components/SidebarIndexTableOfContents'; diff --git a/__tests__/src/components/SidebarIndexThumbnail.test.js b/__tests__/src/components/SidebarIndexThumbnail.test.js index a2c219a2f9df6def5bb5b21297121d070a623f7e..316bc8565d2d9a3ad396b6aaa431a929070ce85d 100644 --- a/__tests__/src/components/SidebarIndexThumbnail.test.js +++ b/__tests__/src/components/SidebarIndexThumbnail.test.js @@ -1,7 +1,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import Typography from '@material-ui/core/Typography'; -import { Utils } from 'manifesto.js/dist-esmodule/Utils'; +import { Utils } from 'manifesto.js'; import fixture from '../../fixtures/version-2/019.json'; import { SidebarIndexThumbnail } from '../../../src/components/SidebarIndexThumbnail'; import IIIFThumbnail from '../../../src/containers/IIIFThumbnail'; diff --git a/__tests__/src/components/ThumbnailCanvasGrouping.test.js b/__tests__/src/components/ThumbnailCanvasGrouping.test.js index ba27c5a71cbeaeab2f1a0c60f4c0c81271ef5ff8..23208396eab984161937f387444403778adff548 100644 --- a/__tests__/src/components/ThumbnailCanvasGrouping.test.js +++ b/__tests__/src/components/ThumbnailCanvasGrouping.test.js @@ -1,6 +1,6 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { Utils } from 'manifesto.js/dist-esmodule/Utils'; +import { Utils } from 'manifesto.js'; import { ThumbnailCanvasGrouping } from '../../../src/components/ThumbnailCanvasGrouping'; import IIIFThumbnail from '../../../src/containers/IIIFThumbnail'; import CanvasGroupings from '../../../src/lib/CanvasGroupings'; diff --git a/__tests__/src/components/ThumbnailNavigation.test.js b/__tests__/src/components/ThumbnailNavigation.test.js index 4bfbf173ff2859e039055e7ee0f7b78d9a7b9952..24940223925a16632dbd0a6dcbca8d0a7defd39a 100644 --- a/__tests__/src/components/ThumbnailNavigation.test.js +++ b/__tests__/src/components/ThumbnailNavigation.test.js @@ -1,6 +1,6 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { Utils } from 'manifesto.js/dist-esmodule/Utils'; +import { Utils } from 'manifesto.js'; import { ThumbnailNavigation } from '../../../src/components/ThumbnailNavigation'; import ThumbnailCanvasGrouping from '../../../src/containers/ThumbnailCanvasGrouping'; import CanvasGroupings from '../../../src/lib/CanvasGroupings'; diff --git a/__tests__/src/components/VideoViewer.test.js b/__tests__/src/components/VideoViewer.test.js index 31945a749ea8a6206c0eed0bf9637e0241f2a24f..7b3521c08ac2bff116aff550356b69d5c3710bb7 100644 --- a/__tests__/src/components/VideoViewer.test.js +++ b/__tests__/src/components/VideoViewer.test.js @@ -7,6 +7,7 @@ function createWrapper(props, suspenseFallback) { return shallow( <VideoViewer classes={{}} + videoOptions={{ crossOrigin: 'anonymous' }} {...props} />, ); @@ -22,21 +23,29 @@ describe('VideoViewer', () => { { getFormat: () => 'video/mp4', id: 2 }, ], }, true); - expect(wrapper.contains(<source src="1" type="video/mp4" />)); - expect(wrapper.contains(<source src="2" type="video/mp4" />)); + expect(wrapper.contains(<source src={1} type="video/mp4" />)).toBe(true); + expect(wrapper.contains(<source src={2} type="video/mp4" />)).toBe(true); + }); + it('passes through configurable options', () => { + wrapper = createWrapper({ + videoResources: [ + { getFormat: () => 'video/mp4', id: 1 }, + ], + }, true); + expect(wrapper.exists('video[crossOrigin="anonymous"]')).toBe(true); // eslint-disable-line jsx-a11y/media-has-caption }); it('captions', () => { wrapper = createWrapper({ captions: [ - { getLabel: () => 'English', getProperty: () => 'en', id: 1 }, - { getLabel: () => 'French', getProperty: () => 'fr', id: 2 }, + { getDefaultLabel: () => 'English', getProperty: () => 'en', id: 1 }, + { getDefaultLabel: () => 'French', getProperty: () => 'fr', id: 2 }, ], videoResources: [ { getFormat: () => 'video/mp4', id: 1 }, ], }, true); - expect(wrapper.contains(<track src="1" label="English" srcLang="en" />)); - expect(wrapper.contains(<track src="2" label="French" srcLang="fr" />)); + expect(wrapper.contains(<track src={1} label="English" srcLang="en" />)).toBe(true); + expect(wrapper.contains(<track src={2} label="French" srcLang="fr" />)).toBe(true); }); }); }); diff --git a/__tests__/src/components/ViewerNavigation.test.js b/__tests__/src/components/ViewerNavigation.test.js index 3ffe5b2657b6f6c4504fa8f0c55d7863c8b9b3f5..36ffe068bdeae5cc577e68b58754d195717de906 100644 --- a/__tests__/src/components/ViewerNavigation.test.js +++ b/__tests__/src/components/ViewerNavigation.test.js @@ -1,5 +1,6 @@ import React from 'react'; import { shallow } from 'enzyme'; +import NavigationIcon from '@material-ui/icons/PlayCircleOutlineSharp'; import MiradorMenuButton from '../../../src/containers/MiradorMenuButton'; import { ViewerNavigation } from '../../../src/components/ViewerNavigation'; @@ -89,8 +90,8 @@ describe('ViewerNavigation', () => { }); it('changes the arrow styles', () => { - const previous = wrapper.find(MiradorMenuButton).first().children('PlayCircleOutlineSharpIcon').props(); - const next = wrapper.find(MiradorMenuButton).last().children('PlayCircleOutlineSharpIcon').props(); + const previous = wrapper.find(MiradorMenuButton).first().children(NavigationIcon).props(); + const next = wrapper.find(MiradorMenuButton).last().children(NavigationIcon).props(); expect(previous.style).toEqual({}); expect(next.style).toEqual({ transform: 'rotate(180deg)' }); }); @@ -112,8 +113,8 @@ describe('ViewerNavigation', () => { }); it('changes the arrow styles', () => { - const previous = wrapper.find(MiradorMenuButton).first().children('PlayCircleOutlineSharpIcon').props(); - const next = wrapper.find(MiradorMenuButton).last().children('PlayCircleOutlineSharpIcon').props(); + const previous = wrapper.find(MiradorMenuButton).first().children(NavigationIcon).props(); + const next = wrapper.find(MiradorMenuButton).last().children(NavigationIcon).props(); expect(previous.style).toEqual({ transform: 'rotate(270deg)' }); expect(next.style).toEqual({ transform: 'rotate(90deg)' }); }); @@ -131,8 +132,8 @@ describe('ViewerNavigation', () => { }); it('changes the arrow styles', () => { - const previous = wrapper.find(MiradorMenuButton).first().children('PlayCircleOutlineSharpIcon').props(); - const next = wrapper.find(MiradorMenuButton).last().children('PlayCircleOutlineSharpIcon').props(); + const previous = wrapper.find(MiradorMenuButton).first().children(NavigationIcon).props(); + const next = wrapper.find(MiradorMenuButton).last().children(NavigationIcon).props(); expect(previous.style).toEqual({ transform: 'rotate(90deg)' }); expect(next.style).toEqual({ transform: 'rotate(270deg)' }); }); diff --git a/__tests__/src/components/WindowSideBarCanvasPanel.test.js b/__tests__/src/components/WindowSideBarCanvasPanel.test.js index 3d2c96ea5022ea470b3c80b61283d83dd5bd57fe..12ed93e36acaf9fa50ec20f8bb27fc5eccdfe454 100644 --- a/__tests__/src/components/WindowSideBarCanvasPanel.test.js +++ b/__tests__/src/components/WindowSideBarCanvasPanel.test.js @@ -1,6 +1,6 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { Utils } from 'manifesto.js/dist-esmodule/Utils'; +import { Utils } from 'manifesto.js'; import compact from 'lodash/compact'; import { WindowSideBarCanvasPanel } from '../../../src/components/WindowSideBarCanvasPanel'; import SidebarIndexList from '../../../src/containers/SidebarIndexList'; diff --git a/__tests__/src/components/WorkspaceControlPanelButtons.test.js b/__tests__/src/components/WorkspaceControlPanelButtons.test.js index 37c7eaf107fbada564310ce626a03ac3e9cf214f..c94ecdd0d08568ec3774ee447a6f221a7ed70eaa 100644 --- a/__tests__/src/components/WorkspaceControlPanelButtons.test.js +++ b/__tests__/src/components/WorkspaceControlPanelButtons.test.js @@ -2,8 +2,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import WorkspaceMenuButton from '../../../src/containers/WorkspaceMenuButton'; import FullScreenButton from '../../../src/containers/FullScreenButton'; -import { WorkspaceControlPanelButtons } - from '../../../src/components/WorkspaceControlPanelButtons'; +import { WorkspaceControlPanelButtons } from '../../../src/components/WorkspaceControlPanelButtons'; import { PluginHook } from '../../../src/components/PluginHook'; describe('WorkspaceControlPanelButtons', () => { diff --git a/__tests__/src/extend/withPlugins.test.js b/__tests__/src/extend/withPlugins.test.js index ac80f7de3f6d90b9908eb5e8dca786f6bb2aeebe..7fdda8ce5649b84a9f263908667350badd0da8dc 100644 --- a/__tests__/src/extend/withPlugins.test.js +++ b/__tests__/src/extend/withPlugins.test.js @@ -126,6 +126,7 @@ describe('PluginHoc: if wrap plugins AND add plugins exist for target', () => { }); it('renders the first wrap plugin, renders add plugins if plugin/props are passed through', () => { /** */ const WrapPluginComponentA = plugin => ( + // eslint-disable-next-line react/destructuring-assignment <plugin.TargetComponent {...plugin.targetProps} {...plugin} /> ); /** */ const WrapPluginComponentB = props => <div>look i am a plugin</div>; diff --git a/__tests__/src/lib/CanvasAnnotationDisplay.test.js b/__tests__/src/lib/CanvasAnnotationDisplay.test.js index ec4e3d80eda461d2eb4ca6619e215989f581805e..721ef34a6f9440ba37d120857eae9ad10098e17a 100644 --- a/__tests__/src/lib/CanvasAnnotationDisplay.test.js +++ b/__tests__/src/lib/CanvasAnnotationDisplay.test.js @@ -34,7 +34,7 @@ describe('CanvasAnnotationDisplay', () => { expect(subject.svgContext).toHaveBeenCalled(); expect(subject.fragmentContext).not.toHaveBeenCalled(); }); - it('selects fragmentSelector if no svg present', () => { + it('selects fragmentSelector if present and if no svg is present', () => { const context = { stroke: jest.fn(), }; @@ -47,6 +47,19 @@ describe('CanvasAnnotationDisplay', () => { expect(subject.svgContext).not.toHaveBeenCalled(); expect(subject.fragmentContext).toHaveBeenCalled(); }); + it('ignores annotations without selectors', () => { + const context = { + stroke: jest.fn(), + }; + const subject = createSubject({ + resource: new AnnotationResource({ on: 'www.example.com' }), + }); + subject.svgContext = jest.fn(); + subject.fragmentContext = jest.fn(); + subject.toContext(context); + expect(subject.svgContext).not.toHaveBeenCalled(); + expect(subject.fragmentContext).not.toHaveBeenCalled(); + }); }); describe('svgString', () => { it('selects the svg selector string value', () => { diff --git a/__tests__/src/lib/CanvasWorld.test.js b/__tests__/src/lib/CanvasWorld.test.js index ac755e202f11c636943b5f8f8b837cbcb1fc1950..cabd13a630eb787c2711431f882376b0b6428b55 100644 --- a/__tests__/src/lib/CanvasWorld.test.js +++ b/__tests__/src/lib/CanvasWorld.test.js @@ -1,4 +1,4 @@ -import { Utils } from 'manifesto.js/dist-esmodule/Utils'; +import { Utils } from 'manifesto.js'; import fixture from '../../fixtures/version-2/019.json'; import fragmentFixture from '../../fixtures/version-2/hamilton.json'; import CanvasWorld from '../../../src/lib/CanvasWorld'; diff --git a/__tests__/src/lib/MiradorCanvas.test.js b/__tests__/src/lib/MiradorCanvas.test.js index e40237b59748f58b00f5575ef9fc99dce49c7f03..0a41c260c327c832beb69823429cffc2a6e487eb 100644 --- a/__tests__/src/lib/MiradorCanvas.test.js +++ b/__tests__/src/lib/MiradorCanvas.test.js @@ -1,4 +1,4 @@ -import { Utils } from 'manifesto.js/dist-esmodule/Utils'; +import { Utils } from 'manifesto.js'; import MiradorCanvas from '../../../src/lib/MiradorCanvas'; import fixture from '../../fixtures/version-2/019.json'; import serviceFixture from '../../fixtures/version-2/canvasService.json'; diff --git a/__tests__/src/lib/MiradorManifest.test.js b/__tests__/src/lib/MiradorManifest.test.js index d4ffe0bcf1cff5e5e97b385859a837ff71c723dc..7832fa97e1df4d85f9df6e1f5e1de3ad3b1cb6b6 100644 --- a/__tests__/src/lib/MiradorManifest.test.js +++ b/__tests__/src/lib/MiradorManifest.test.js @@ -1,4 +1,4 @@ -import { Utils } from 'manifesto.js/dist-esmodule/Utils'; +import { Utils } from 'manifesto.js'; import MiradorManifest from '../../../src/lib/MiradorManifest'; /** */ diff --git a/__tests__/src/lib/MiradorViewer.test.js b/__tests__/src/lib/MiradorViewer.test.js index 1b260abf57105af118f35a73c0fee33e37e707aa..f3f2702b66bfb4d40bcdf3e0851332257b56ae49 100644 --- a/__tests__/src/lib/MiradorViewer.test.js +++ b/__tests__/src/lib/MiradorViewer.test.js @@ -1,4 +1,5 @@ import ReactDOM from 'react-dom'; +import { shallow } from 'enzyme'; import MiradorViewer from '../../../src/lib/MiradorViewer'; jest.unmock('react-i18next'); @@ -10,7 +11,7 @@ describe('MiradorViewer', () => { beforeAll(() => { ReactDOM.render = jest.fn(); ReactDOM.unmountComponentAtNode = jest.fn(); - instance = new MiradorViewer({}); + instance = new MiradorViewer({ id: 'mirador' }); }); describe('constructor', () => { it('returns viewer store', () => { @@ -104,6 +105,15 @@ describe('MiradorViewer', () => { })); }); }); + + describe('render', () => { + it('passes props through to the App component', () => { + const rendered = shallow(instance.render({ some: 'prop' })); + expect(rendered.find('App').length).toBe(1); + expect(rendered.find('App').prop('some')).toBe('prop'); + }); + }); + describe('unmount', () => { it('unmounts via ReactDOM', () => { instance.unmount(); diff --git a/__tests__/src/lib/OpenSeadragonCanvasOverlay.test.js b/__tests__/src/lib/OpenSeadragonCanvasOverlay.test.js index 23a785b56d3cbaf971b7f3748e6678045a16de64..22a661e7234a93a615d52dd272f0e0de8b1f68dd 100644 --- a/__tests__/src/lib/OpenSeadragonCanvasOverlay.test.js +++ b/__tests__/src/lib/OpenSeadragonCanvasOverlay.test.js @@ -17,7 +17,7 @@ describe('OpenSeadragonCanvasOverlay', () => { clientWidth: 200, }, viewport: { - getBoundsNoRotate: jest.fn(() => ({ + getBoundsNoRotateWithMargins: jest.fn(() => ({ height: 300, width: 200, x: 40, @@ -92,7 +92,7 @@ describe('OpenSeadragonCanvasOverlay', () => { clientWidth: 200, }, viewport: { - getBoundsNoRotate: jest.fn(() => (new OpenSeadragon.Rect(0, 0, 200, 200))), + getBoundsNoRotateWithMargins: jest.fn(() => (new OpenSeadragon.Rect(0, 0, 200, 200))), }, world: { getItemAt: jest.fn(), diff --git a/__tests__/src/lib/ThumbnailFactory.test.js b/__tests__/src/lib/ThumbnailFactory.test.js index 42344b82f0d1e649dfa6dd52405aab582b149ccb..6f0cc3cfe934558f837bac5437d4fb36f66c3e7e 100644 --- a/__tests__/src/lib/ThumbnailFactory.test.js +++ b/__tests__/src/lib/ThumbnailFactory.test.js @@ -1,12 +1,17 @@ -import { ManifestResource, Resource, Utils } from 'manifesto.js/dist-esmodule'; -import getThumbnail from '../../../src/lib/ThumbnailFactory'; +import { + ManifestResource, Resource, Service, Utils, +} from 'manifesto.js'; +import getThumbnail, { ThumbnailFactory } from '../../../src/lib/ThumbnailFactory'; import fixture from '../../fixtures/version-2/019.json'; const manifest = Utils.parseManifest(fixture); const canvas = manifest.getSequences()[0].getCanvases()[0]; /** */ -function createSubject(jsonld, iiifOpts) { +function createSubject(jsonld, resourceType, iiifOpts) { + if (resourceType === 'Image') { + return createImageSubject(jsonld, iiifOpts); + } return getThumbnail(new ManifestResource(jsonld, {}), iiifOpts); } @@ -36,37 +41,47 @@ describe('getThumbnail', () => { const iiifLevel0Service = iiifService(url, {}, { profile: 'level0' }); const iiifLevel1Service = iiifService(url, { height: 2000, width: 1000 }, { profile: 'level1' }); const iiifLevel2Service = iiifService(url, { height: 2000, width: 1000 }, { profile: 'level2' }); + const sizes = [ + { height: 25, width: 25 }, + { height: 100, width: 100 }, + { height: 125, width: 125 }, + { height: 1000, width: 1000 }, + ]; - describe('with a thumbnail', () => { - it('return the thumbnail and metadata', () => { - expect(createSubject({ '@id': 'xyz', '@type': 'Whatever', thumbnail: { '@id': url, height: 70, width: 50 } })).toMatchObject({ height: 70, url, width: 50 }); - }); + describe('with a IIIF resource', () => { + for (const type of ['Collection', 'Manifest', 'Canvas', 'Image']) { + describe('with a thumbnail', () => { + it('return the thumbnail and metadata', () => { + expect(createSubject({ '@id': 'xyz', '@type': type, thumbnail: { '@id': url, height: 70, width: 50 } }, type)).toMatchObject({ height: 70, url, width: 50 }); + }); - it('return the IIIF service of the thumbnail', () => { - expect(createSubject({ '@id': 'xyz', '@type': 'Whatever', thumbnail: iiifLevel1Service })).toMatchObject({ url: `${url}/full/,120/0/default.jpg` }); - }); + it('return the IIIF service of the thumbnail', () => { + expect(createSubject({ '@id': 'xyz', '@type': type, thumbnail: iiifLevel1Service }, type)).toMatchObject({ url: `${url}/full/,120/0/default.jpg` }); + }); - describe('with image size constraints', () => { - it('does nothing with a static resource', () => { - expect(createSubject({ '@id': 'xyz', '@type': 'Whatever', thumbnail: { '@id': url } }, { maxWidth: 50 })).toMatchObject({ url }); - }); + describe('with image size constraints', () => { + it('does nothing with a static resource', () => { + expect(createSubject({ '@id': 'xyz', '@type': type, thumbnail: { '@id': url } }, type, { maxWidth: 50 })).toMatchObject({ url }); + }); - it('does nothing with a IIIF level 0 service', () => { - expect(createSubject({ '@id': 'xyz', '@type': 'Whatever', thumbnail: iiifLevel0Service }, { maxWidth: 50 })).toMatchObject({ url: 'arbitrary-url' }); - }); + it('does nothing with a IIIF level 0 service', () => { + expect(createSubject({ '@id': 'xyz', '@type': type, thumbnail: iiifLevel0Service }, type, { maxWidth: 50 })).toMatchObject({ url: 'arbitrary-url' }); + }); - it('calculates constraints for a IIIF level 1 service', () => { - expect(createSubject({ '@id': 'xyz', '@type': 'Whatever', thumbnail: iiifLevel1Service }, { maxWidth: 150 })).toMatchObject({ height: 300, url: `${url}/full/150,/0/default.jpg`, width: 150 }); - }); + it('calculates constraints for a IIIF level 1 service', () => { + expect(createSubject({ '@id': 'xyz', '@type': type, thumbnail: iiifLevel1Service }, type, { maxWidth: 150 })).toMatchObject({ height: 300, url: `${url}/full/150,/0/default.jpg`, width: 150 }); + }); - it('calculates constraints for a IIIF level 2 service', () => { - expect(createSubject({ '@id': 'xyz', '@type': 'Whatever', thumbnail: iiifLevel2Service }, { maxHeight: 200, maxWidth: 150 })).toMatchObject({ height: 200, url: `${url}/full/!150,200/0/default.jpg`, width: 100 }); - }); + it('calculates constraints for a IIIF level 2 service', () => { + expect(createSubject({ '@id': 'xyz', '@type': type, thumbnail: iiifLevel2Service }, type, { maxHeight: 200, maxWidth: 150 })).toMatchObject({ height: 200, url: `${url}/full/!150,200/0/default.jpg`, width: 100 }); + }); - it('applies a minumum size to image constraints to encourage asset reuse', () => { - expect(createSubject({ '@id': 'xyz', '@type': 'Whatever', thumbnail: iiifLevel2Service }, { maxHeight: 100, maxWidth: 100 })).toMatchObject({ height: 120, url: `${url}/full/!120,120/0/default.jpg`, width: 60 }); + it('applies a minumum size to image constraints to encourage asset reuse', () => { + expect(createSubject({ '@id': 'xyz', '@type': type, thumbnail: iiifLevel2Service }, type, { maxHeight: 100, maxWidth: 100 })).toMatchObject({ height: 120, url: `${url}/full/!120,120/0/default.jpg`, width: 60 }); + }); + }); }); - }); + } }); describe('with an image resource', () => { @@ -86,12 +101,6 @@ describe('getThumbnail', () => { }); it('uses embedded sizes to find an appropriate size', () => { - const sizes = [ - { height: 25, width: 25 }, - { height: 100, width: 100 }, - { height: 125, width: 125 }, - { height: 1000, width: 1000 }, - ]; const obj = { ...(iiifService('some-url', {}, { profile: 'level0', sizes })), id: 'xyz', @@ -125,15 +134,55 @@ describe('getThumbnail', () => { describe('with a canvas', () => { it('uses the thumbnail', () => { - expect(createSubject({ ...canvas.__jsonld, thumbnail: { ...iiifLevel1Service } })).toMatchObject({ url: `${url}/full/,120/0/default.jpg` }); + expect(createSubject({ ...canvas.__jsonld, thumbnail: { ...iiifLevel1Service } }, 'Canvas')).toMatchObject({ url: `${url}/full/,120/0/default.jpg` }); }); it('uses the first image resource', () => { expect(getThumbnail(canvas)).toMatchObject({ url: 'https://stacks.stanford.edu/image/iiif/hg676jb4964%2F0380_796-44/full/,120/0/default.jpg' }); }); + + it('uses the width and height of a thumbnail without a IIIF Image API service', () => { + const myCanvas = { + ...canvas.__jsonld, + thumbnail: { + height: 240, + id: 'arbitrary-url', + width: 180, + }, + }; + expect(createSubject(myCanvas, 'Canvas')).toMatchObject({ height: 240, url: 'arbitrary-url', width: 180 }); + }); + + it('uses embedded sizes of a IIIF Image API service to find an appropriate size', () => { + const myCanvas = { + ...canvas.__jsonld, + thumbnail: { + height: 100, + id: 'arbitrary-url', + service: [{ + id: url, + profile: 'level2', + sizes, + type: 'ImageService3', + }], + width: 100, + }, + }; + expect(createSubject(myCanvas, 'Canvas', { maxHeight: 120, maxWidth: 120 })) + .toMatchObject({ height: 125, url: `${url}/full/125,125/0/default.jpg`, width: 125 }); + }); }); describe('with a manifest', () => { + it('does nothing with a plain URL', () => { + const manifestWithThumbnail = Utils.parseManifest({ + ...manifest.__jsonld, + thumbnail: url, + }); + + expect(getThumbnail(manifestWithThumbnail, { maxWidth: 50 })).toMatchObject({ url }); + }); + it('uses the thumbnail', () => { const manifestWithThumbnail = Utils.parseManifest({ ...manifest.__jsonld, @@ -198,3 +247,102 @@ describe('getThumbnail', () => { }); }); }); + +describe('picking the best format', () => { + const url = 'http://example.com'; + + it('defaults to jpg', () => { + const myCanvas = { + ...canvas.__jsonld, + thumbnail: { + height: 100, + id: 'arbitrary-url', + service: [{ + id: url, + profile: 'level2', + type: 'ImageService3', + }], + width: 100, + }, + }; + expect(createSubject(myCanvas, 'Canvas')) + .toMatchObject({ url: `${url}/full/,120/0/default.jpg` }); + }); + + it('uses the preferred format of the service', () => { + const myCanvas = { + ...canvas.__jsonld, + thumbnail: { + height: 100, + id: 'arbitrary-url', + service: [{ + id: url, + preferredFormats: ['webp'], + profile: 'level2', + type: 'ImageService3', + }], + width: 100, + }, + }; + expect(createSubject(myCanvas, 'Canvas')) + .toMatchObject({ url: `${url}/full/,120/0/default.webp` }); + }); + + it('can be filtered by application preferred formats', () => { + const myCanvas = { + ...canvas.__jsonld, + thumbnail: { + height: 100, + id: 'arbitrary-url', + service: [{ + id: url, + preferredFormats: ['webp', 'png'], + profile: 'level2', + type: 'ImageService3', + }], + width: 100, + }, + }; + expect(createSubject(myCanvas, 'Canvas', { preferredFormats: ['png', 'jpg'] })) + .toMatchObject({ url: `${url}/full/,120/0/default.png` }); + }); +}); + +describe('selectBestImageSize', () => { + const targetWidth = 120; + const targetHeight = 120; + + it('selects the smallest size larger than the target, if one is available', () => { + const sizes = [ + { height: 75, width: 75 }, + { height: 150, width: 150 }, + { height: 300, width: 300 }, + ]; + const service = new Service({ + id: 'arbitrary-url', + profile: 'level0', + sizes, + type: 'ImageService3', + }); + + expect(ThumbnailFactory.selectBestImageSize(service, targetWidth * targetHeight)) + .toEqual(sizes[1]); + }); + + it('selects the largest size smaller than the target, if none larger are available', () => { + const sizes = [ + { height: 25, width: 25 }, + { height: 50, width: 50 }, + { height: 75, width: 75 }, + ]; + const service = new Service({ + id: 'arbitrary-url', + profile: 'level0', + sizes, + type: 'ImageService3', + }); + + expect(ThumbnailFactory.selectBestImageSize(service, targetWidth * targetHeight)) + .toEqual(sizes[2]); + }); +}); diff --git a/__tests__/src/sagas/auth.test.js b/__tests__/src/sagas/auth.test.js index 229e9956e3a166de4ab843e322029fb1a05b4e12..8d31a5b445c18e70fd666b2ea4ceeee6d8002df3 100644 --- a/__tests__/src/sagas/auth.test.js +++ b/__tests__/src/sagas/auth.test.js @@ -1,6 +1,6 @@ import { call, select } from 'redux-saga/effects'; import { expectSaga } from 'redux-saga-test-plan'; -import { Utils } from 'manifesto.js/dist-esmodule/Utils'; +import { Utils } from 'manifesto.js'; import serviceFixture from '../../fixtures/version-2/canvasService.json'; import settings from '../../../src/config/settings'; import ActionTypes from '../../../src/state/actions/action-types'; diff --git a/__tests__/src/sagas/windows.test.js b/__tests__/src/sagas/windows.test.js index 7a69c828b640ae383aba6a9759e2cf0b5b94720f..796c0adc0072fecef6b2290c6bdd649232d08d32 100644 --- a/__tests__/src/sagas/windows.test.js +++ b/__tests__/src/sagas/windows.test.js @@ -1,6 +1,6 @@ import { call, select } from 'redux-saga/effects'; import { expectSaga } from 'redux-saga-test-plan'; -import { Utils } from 'manifesto.js/dist-esmodule/Utils'; +import { Utils } from 'manifesto.js'; import ActionTypes from '../../../src/state/actions/action-types'; import { setCanvas } from '../../../src/state/actions'; @@ -14,6 +14,7 @@ import { getSortedSearchAnnotationsForCompanionWindow, getVisibleCanvasIds, getCanvasForAnnotation, getCanvases, selectInfoResponses, + getWindowConfig, } from '../../../src/state/selectors'; import { fetchManifests } from '../../../src/state/sagas/iiif'; import { @@ -343,6 +344,7 @@ describe('window-level sagas', () => { return expectSaga(setCanvasOfFirstSearchResult, action) .provide([ + [select(getWindowConfig, { windowId }), { switchCanvasOnSearch: true }], [select(getSelectedContentSearchAnnotationIds, { companionWindowId, windowId }), []], [select(getSortedSearchAnnotationsForCompanionWindow, { companionWindowId, windowId }), [{ id: 'a' }, { id: 'b' }]], ]) @@ -365,10 +367,27 @@ describe('window-level sagas', () => { return expectSaga(setCanvasOfFirstSearchResult, action) .provide([ + [select(getWindowConfig, { windowId }), { switchCanvasOnSearch: true }], [select(getSelectedContentSearchAnnotationIds, { companionWindowId, windowId }), ['y']], ]) .run().then(({ allEffects }) => allEffects.length === 0); }); + + it('does nothing if canvas switching for searches is disabled', () => { + const companionWindowId = 'x'; + const windowId = 'y'; + const action = { + companionWindowId, + type: ActionTypes.RECEIVE_SEARCH, + windowId, + }; + + return expectSaga(setCanvasOfFirstSearchResult, action) + .provide([ + [select(getWindowConfig, { windowId }), { switchCanvasOnSearch: false }], + ]) + .run().then(({ allEffects }) => allEffects.length === 0); + }); }); describe('setCanvasforSelectedAnnotation', () => { diff --git a/__tests__/src/selectors/manifests.test.js b/__tests__/src/selectors/manifests.test.js index 0251ccb49a847c5ab968153a7dedffa58de37681..3c174506f35b8e0969d177dae3c8652e885a4231 100644 --- a/__tests__/src/selectors/manifests.test.js +++ b/__tests__/src/selectors/manifests.test.js @@ -1,4 +1,4 @@ -import { Utils } from 'manifesto.js/dist-esmodule/Utils'; +import { Utils } from 'manifesto.js'; import manifestFixture001 from '../../fixtures/version-2/001.json'; import manifestFixture002 from '../../fixtures/version-2/002.json'; import manifestFixture019 from '../../fixtures/version-2/019.json'; diff --git a/config/paths.js b/config/paths.js deleted file mode 100644 index 2060badce016cbe023d2af06e9a9015114a77858..0000000000000000000000000000000000000000 --- a/config/paths.js +++ /dev/null @@ -1,98 +0,0 @@ -const path = require('path'); -const fs = require('fs'); -const url = require('url'); - -// Make sure any symlinks in the project folder are resolved: -// https://github.com/facebook/create-react-app/issues/637 -const appDirectory = fs.realpathSync(process.cwd()); - -/** - * - * @param relativePath - * @returns {string} - */ -const resolveApp = relativePath => path.resolve(appDirectory, relativePath); - -const envPublicUrl = process.env.PUBLIC_URL; - -/** - * - * @param inputPath - * @param needsSlash - * @returns {*} - */ -function ensureSlash(inputPath, needsSlash) { - const hasSlash = inputPath.endsWith('/'); - if (hasSlash && !needsSlash) { - return inputPath.substr(0, inputPath.length - 1); - } else if (!hasSlash && needsSlash) { - return `${inputPath}/`; - } else { - return inputPath; - } -} - -/** - * - * @param appPackageJson - * @returns {string | *} - */ -const getPublicUrl = appPackageJson => envPublicUrl || require(appPackageJson).homepage; - -/** - * - * @param appPackageJson - * @returns {*} - */ -function getServedPath(appPackageJson) { - const publicUrl = getPublicUrl(appPackageJson); - const servedUrl = - envPublicUrl || (publicUrl ? url.parse(publicUrl).pathname : '/'); - return ensureSlash(servedUrl, true); -} - -const moduleFileExtensions = [ - 'web.mjs', - 'mjs', - 'web.js', - 'js', - 'web.ts', - 'ts', - 'web.tsx', - 'tsx', - 'json', - 'web.jsx', - 'jsx', -]; - -/** - * - * @param resolveFn - * @param filePath - * @returns {*} - */ -const resolveModule = (resolveFn, filePath) => { - const extension = moduleFileExtensions.find(extension => fs.existsSync(resolveFn(`${filePath}.${extension}`))); - - if (extension) { - return resolveFn(`${filePath}.${extension}`); - } - - return resolveFn(`${filePath}.js`); -}; - -module.exports = { - dotenv: resolveApp('.env'), - appPath: resolveApp('.'), - appBuild: resolveApp('build'), - appDist: resolveApp('dist'), - appIndexJs: resolveModule(resolveApp, 'src/index'), - appPackageJson: resolveApp('package.json'), - appSrc: resolveApp('src'), - testsSetup: resolveModule(resolveApp, 'src/setupTests'), - appNodeModules: resolveApp('node_modules'), - publicUrl: getPublicUrl(resolveApp('package.json')), - servedPath: getServedPath(resolveApp('package.json')), -}; - -module.exports.moduleFileExtensions = moduleFileExtensions; diff --git a/jest.json b/jest.json index fd8cf635a7e1b28ed051bcdc24850581f52e12cd..d635140982349d0f2b6e906c1b113cdc4ab04b88 100644 --- a/jest.json +++ b/jest.json @@ -17,8 +17,5 @@ "<rootDir>/**/__tests__/**/*.{js,jsx}", "<rootDir>/src/**/?(*.)(spec|test|unit).{js,jsx}" ], - "transformIgnorePatterns": [ - "<rootDir>/node_modules/(?!manifesto.js)" - ], "preset": "jest-puppeteer" } diff --git a/package.json b/package.json index 0e6fbf83457231eb331e533f029a2c7d0d14daae..ebb453fd4179d03b0f6aa9d9b49d706417d6c293 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "mirador", - "version": "3.0.0", + "version": "3.3.0", "description": "An open-source, web-based 'multi-up' viewer that supports zoom-pan-rotate functionality, ability to display/compare simple images, and images with annotations.", - "main": "dist/mirador.min.js", + "main": "dist/cjs/src/index.js", "module": "dist/es/src/index.js", "files": [ "dist" @@ -10,10 +10,12 @@ "sideEffects": false, "scripts": { "clean": "rm -rf ./dist", - "lint": "node_modules/.bin/eslint ./ && npm run lint:translations", + "lint": "node_modules/.bin/eslint ./ && npm run lint:translations && npm run lint:containers", + "lint:containers": "node ./scripts/container-lint.js", "lint:translations": "node ./scripts/i18n-lint.js", "server": "node_modules/.bin/http-server --cors", "test": "npm run build && npm run lint && npm run size && jest -c jest.json", + "test:debug": "node --inspect node_modules/.bin/jest -c jest.json --runInBand", "test:watch": "jest -c jest.json --watch", "build": "NODE_ENV=production webpack --mode=production", "build:dev": "webpack --mode=development", @@ -22,7 +24,7 @@ "build:watch": "webpack --watch --mode=development", "prepublishOnly": "npm run clean && npm run build:es && npm run build:cjs && npm run build", "size": "bundlewatch --config bundlewatch.config.json", - "start": "NODE_ENV=development webpack-dev-server --open" + "start": "NODE_ENV=development webpack serve --open" }, "license": "Apache-2.0", "contributors": [ @@ -32,7 +34,7 @@ "repository": "https://github.com/ProjectMirador/mirador", "dependencies": { "@material-ui/core": "^4.11.0", - "@material-ui/icons": "~4.9.1", + "@material-ui/icons": "^4.9.1", "@material-ui/lab": "^4.0.0-alpha.53", "@researchgate/react-intersection-observer": "^1.0.0", "classnames": "^2.2.6", @@ -68,7 +70,7 @@ "react-sizeme": "^2.6.7", "react-virtualized-auto-sizer": "^1.0.2", "react-window": "^1.8.5", - "redux": "4.0.5", + "redux": "^4.0.5", "redux-devtools-extension": "^2.13.2", "redux-saga": "^1.1.3", "redux-thunk": "^2.3.0", @@ -84,47 +86,48 @@ "@babel/plugin-transform-runtime": "^7.10.3", "@babel/preset-env": "^7.10.3", "@babel/preset-react": "^7.10.1", - "@pmmmwh/react-refresh-webpack-plugin": "^0.3.3", - "babel-eslint": "10.1.0", + "@pmmmwh/react-refresh-webpack-plugin": "^0.4.3", + "@typescript-eslint/eslint-plugin": "^4.21.0", + "@typescript-eslint/parser": "^4.21.0", + "babel-eslint": "^10.1.0", "babel-jest": "^26.0.1", "babel-loader": "^8.0.6", "babel-plugin-lodash": "^3.3.4", - "babel-plugin-macros": "^2.8.0", + "babel-plugin-macros": "^3.0.1", "babel-plugin-transform-react-remove-prop-types": "^0.4.24", - "bundlewatch": "^0.2.5", + "bundlewatch": "^0.3.2", "chalk": "^4.1.0", "codecov": "^3.7.0", "core-js": "^3.4.8", "enzyme": "^3.10.0", - "enzyme-adapter-react-16": "^1.14.0", - "eslint": "^6.0.0", + "enzyme-adapter-react-16": "^1.15.0", + "eslint": "^7.23.0", "eslint-config-airbnb": "^18.2.0", - "eslint-config-react-app": "^3.0.5", + "eslint-config-react-app": "^6.0.0", "eslint-loader": "^4.0.2", - "eslint-plugin-flowtype": "^4.7.0", - "eslint-plugin-import": "^2.18.0", - "eslint-plugin-jest": "^23.16.0", - "eslint-plugin-jsx-a11y": "^6.2.3", - "eslint-plugin-react": "^7.14.2", + "eslint-plugin-flowtype": "^5.6.0", + "eslint-plugin-import": "^2.22.1", + "eslint-plugin-jest": "^24.0.0", + "eslint-plugin-jsx-a11y": "^6.4.1", + "eslint-plugin-react": "^7.23.2", + "eslint-plugin-react-hooks": "^4.2.0", "glob": "^7.1.4", "http-server": "^0.12.3", "jest": "^26.0.1", "jest-fetch-mock": "^3.0.0", - "jest-puppeteer": "^4.1.1", - "jsdom": "15.1.1", - "puppeteer": "^4.0.0", + "jest-puppeteer": "^5.0.2", + "jsdom": "^16.5.3", + "puppeteer": "^9.0.0", "react": "^16.8.6", - "react-dev-utils": "^10.2.1", "react-dom": "^16.8.6", "react-refresh": "^0.8.3", "redux-mock-store": "^1.5.1", "redux-saga-test-plan": "^4.0.0-rc.3", - "supertest": "^4.0.2", - "terser-webpack-plugin": "^3.0.6", + "terser-webpack-plugin": "^4.0.0", "unfetch": "^4.1.0", "url-polyfill": "^1.1.7", "webpack": "^4.43.0", - "webpack-cli": "^3.3.5", + "webpack-cli": "^4.6.0", "webpack-dev-server": "^3.11.0" }, "peerDependencies": { diff --git a/scripts/container-lint.js b/scripts/container-lint.js new file mode 100644 index 0000000000000000000000000000000000000000..7ef296ba57479c6c643c602e23a3042d50fce3ae --- /dev/null +++ b/scripts/container-lint.js @@ -0,0 +1,18 @@ +const glob = require('glob'); // eslint-disable-line import/no-extraneous-dependencies +const fs = require('fs'); +const chalk = require('chalk'); // eslint-disable-line import/no-extraneous-dependencies + +const { error } = console; +const globOpts = { cwd: 'src/containers' }; +const files = glob.sync('**/*.js', globOpts); + +files.forEach((fileName) => { + const fileContent = fs.readFileSync(`src/containers/${fileName}`).toString(); + const withPlugins = fileContent.indexOf('withPlugins('); + if (withPlugins > 0) { + const correctCall = fileContent.indexOf(`withPlugins('${fileName.replace('.js', '')}')`); + if (withPlugins !== correctCall) { + error(chalk.red(`Check withPlugins for ${fileName} for an incorrect target`)); + } + } +}); diff --git a/src/components/AudioViewer.js b/src/components/AudioViewer.js index 85d9548c8850ed03414e8a03cf72d1fcdcc2e52d..fd63c4053fb73af660192e406cc6ac3b7f71db38 100644 --- a/src/components/AudioViewer.js +++ b/src/components/AudioViewer.js @@ -6,16 +6,23 @@ export class AudioViewer extends Component { /* eslint-disable jsx-a11y/media-has-caption */ /** */ render() { - const { classes, audioResources } = this.props; + const { + captions, classes, audioOptions, audioResources, + } = this.props; return ( <div className={classes.container}> - <audio controls className={classes.audio}> + <audio className={classes.audio} {...audioOptions}> {audioResources.map(audio => ( <Fragment key={audio.id}> <source src={audio.id} type={audio.getFormat()} /> </Fragment> ))} + {captions.map(caption => ( + <Fragment key={caption.id}> + <track src={caption.id} label={caption.getDefaultLabel()} srcLang={caption.getProperty('language')} /> + </Fragment> + ))} </audio> </div> ); @@ -24,10 +31,14 @@ export class AudioViewer extends Component { } AudioViewer.propTypes = { + audioOptions: PropTypes.object, // eslint-disable-line react/forbid-prop-types audioResources: PropTypes.arrayOf(PropTypes.object), + captions: PropTypes.arrayOf(PropTypes.object), classes: PropTypes.objectOf(PropTypes.string).isRequired, }; AudioViewer.defaultProps = { + audioOptions: {}, audioResources: [], + captions: [], }; diff --git a/src/components/CanvasAnnotations.js b/src/components/CanvasAnnotations.js index 23150c6a39414a3d8026902e74f3e259677ad44a..137a4ef0bcf3f1ed42a78e90ef5df08073752b2e 100644 --- a/src/components/CanvasAnnotations.js +++ b/src/components/CanvasAnnotations.js @@ -7,6 +7,7 @@ import MenuItem from '@material-ui/core/MenuItem'; import ListItemText from '@material-ui/core/ListItemText'; import Typography from '@material-ui/core/Typography'; import SanitizedHtml from '../containers/SanitizedHtml'; +import { ScrollTo } from './ScrollTo'; /** * CanvasAnnotations ~ @@ -59,6 +60,7 @@ export class CanvasAnnotations extends Component { const { annotations, classes, index, label, selectedAnnotationId, t, totalSize, listContainerComponent, htmlSanitizationRuleSet, hoveredAnnotationIds, + containerRef, } = this.props; if (annotations.length === 0) return <></>; @@ -70,38 +72,45 @@ export class CanvasAnnotations extends Component { <MenuList autoFocusItem variant="selectedMenu"> { annotations.map(annotation => ( - <MenuItem - button - component={listContainerComponent} - className={clsx( - classes.annotationListItem, - { - [classes.hovered]: hoveredAnnotationIds.includes(annotation.id), - }, - )} - key={annotation.id} - annotationid={annotation.id} - selected={selectedAnnotationId === annotation.id} - onClick={e => this.handleClick(e, annotation)} - onFocus={() => this.handleAnnotationHover(annotation)} - onBlur={this.handleAnnotationBlur} - onMouseEnter={() => this.handleAnnotationHover(annotation)} - onMouseLeave={this.handleAnnotationBlur} + <ScrollTo + containerRef={containerRef} + key={`${annotation.id}-scroll`} + offsetTop={96} // offset for the height of the form above + scrollTo={selectedAnnotationId === annotation.id} > - <ListItemText primaryTypographyProps={{ variant: 'body2' }}> - <SanitizedHtml - ruleSet={htmlSanitizationRuleSet} - htmlString={annotation.content} - /> - <div> + <MenuItem + button + component={listContainerComponent} + className={clsx( + classes.annotationListItem, { - annotation.tags.map(tag => ( - <Chip size="small" variant="outlined" label={tag} id={tag} className={classes.chip} key={tag.toString()} /> - )) - } - </div> - </ListItemText> - </MenuItem> + [classes.hovered]: hoveredAnnotationIds.includes(annotation.id), + }, + )} + key={annotation.id} + annotationid={annotation.id} + selected={selectedAnnotationId === annotation.id} + onClick={e => this.handleClick(e, annotation)} + onFocus={() => this.handleAnnotationHover(annotation)} + onBlur={this.handleAnnotationBlur} + onMouseEnter={() => this.handleAnnotationHover(annotation)} + onMouseLeave={this.handleAnnotationBlur} + > + <ListItemText primaryTypographyProps={{ variant: 'body2' }}> + <SanitizedHtml + ruleSet={htmlSanitizationRuleSet} + htmlString={annotation.content} + /> + <div> + { + annotation.tags.map(tag => ( + <Chip size="small" variant="outlined" label={tag} id={tag} className={classes.chip} key={tag.toString()} /> + )) + } + </div> + </ListItemText> + </MenuItem> + </ScrollTo> )) } </MenuList> @@ -118,6 +127,10 @@ CanvasAnnotations.propTypes = { }), ), classes: PropTypes.objectOf(PropTypes.string), + containerRef: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.shape({ current: PropTypes.instanceOf(Element) }), + ]), deselectAnnotation: PropTypes.func.isRequired, hoverAnnotation: PropTypes.func.isRequired, hoveredAnnotationIds: PropTypes.arrayOf(PropTypes.string), @@ -134,6 +147,7 @@ CanvasAnnotations.propTypes = { CanvasAnnotations.defaultProps = { annotations: [], classes: {}, + containerRef: undefined, hoveredAnnotationIds: [], htmlSanitizationRuleSet: 'iiif', listContainerComponent: 'li', diff --git a/src/components/CollapsibleSection.js b/src/components/CollapsibleSection.js index e5e0f5337ecc30526ba997485b3772c412d078b9..eca0a2ff33d747c7e6782acebd50774c9de49b3b 100644 --- a/src/components/CollapsibleSection.js +++ b/src/components/CollapsibleSection.js @@ -49,7 +49,7 @@ export class CollapsibleSection extends Component { aria-label={ t( open ? 'collapseSection' : 'expandSection', - { section: label.toLowerCase() }, + { section: label }, ) } aria-expanded={open} diff --git a/src/components/CollectionDialog.js b/src/components/CollectionDialog.js index f66e36a48cda54890294652bb2fdbcc51ea8a4ca..53e65d67117f2b899e2535677041313eb3fedd71 100644 --- a/src/components/CollectionDialog.js +++ b/src/components/CollectionDialog.js @@ -13,21 +13,12 @@ import { } from '@material-ui/core'; import ArrowBackIcon from '@material-ui/icons/ArrowBackSharp'; import Skeleton from '@material-ui/lab/Skeleton'; +import asArray from '../lib/asArray'; import { LabelValueMetadata } from './LabelValueMetadata'; import CollapsibleSection from '../containers/CollapsibleSection'; import ScrollIndicatedDialogContent from '../containers/ScrollIndicatedDialogContent'; import ManifestInfo from '../containers/ManifestInfo'; -/** - */ -function asArray(value) { - if (!Array.isArray(value)) { - return [value]; - } - - return value; -} - /** * a dialog providing the possibility to select the collection */ @@ -243,7 +234,11 @@ export class CollectionDialog extends Component { <MenuList> { collections.map(c => ( - <MenuItem key={c.id} onClick={() => { this.selectCollection(c); }}> + <MenuItem + key={c.id} + onClick={() => { this.selectCollection(c); }} + className={classes.collectionItem} + > {CollectionDialog.getUseableLabel(c)} </MenuItem> )) @@ -254,7 +249,11 @@ export class CollectionDialog extends Component { <MenuList> { manifest.getManifests().map(m => ( - <MenuItem key={m.id} onClick={() => { this.selectManifest(m); }}> + <MenuItem + key={m.id} + onClick={() => { this.selectManifest(m); }} + className={classes.collectionItem} + > {CollectionDialog.getUseableLabel(m)} </MenuItem> )) diff --git a/src/components/IIIFThumbnail.js b/src/components/IIIFThumbnail.js index 174e9af0bb922b4441b194df073241420c1f22db..c942f4824231739d722ab08c9682abdc3165148f 100644 --- a/src/components/IIIFThumbnail.js +++ b/src/components/IIIFThumbnail.js @@ -98,6 +98,12 @@ export class IIIFThumbnail extends Component { styleProps.height = maxHeight; } else if (!thumbHeight && thumbWidth) { styleProps.width = maxWidth; + } else { + // The thumbnail wasn't retrieved via an Image API service, + // and its dimensions are not specified in the JSON-LD + // (note that this may result in a blurry image) + styleProps.width = maxWidth; + styleProps.height = maxHeight; } return { @@ -109,12 +115,12 @@ export class IIIFThumbnail extends Component { /** */ image() { const { - thumbnail, resource, maxHeight, maxWidth, + thumbnail, resource, maxHeight, maxWidth, thumbnailsConfig, } = this.props; if (thumbnail) return thumbnail; - const image = getThumbnail(resource, { maxHeight, maxWidth }); + const image = getThumbnail(resource, { ...thumbnailsConfig, maxHeight, maxWidth }); if (image && image.url) return image; @@ -183,6 +189,7 @@ IIIFThumbnail.propTypes = { url: PropTypes.string.isRequired, width: PropTypes.number, }), + thumbnailsConfig: PropTypes.object, // eslint-disable-line react/forbid-prop-types variant: PropTypes.oneOf(['inside', 'outside']), }; @@ -197,5 +204,6 @@ IIIFThumbnail.defaultProps = { maxWidth: null, style: {}, thumbnail: null, + thumbnailsConfig: {}, variant: null, }; diff --git a/src/components/ManifestForm.js b/src/components/ManifestForm.js index c4e03ec51ec756c26c5ada8b82489bdbdf63d626..19702f071430b2d07f6f29889e811a012ae056d6 100644 --- a/src/components/ManifestForm.js +++ b/src/components/ManifestForm.js @@ -18,24 +18,11 @@ export class ManifestForm extends Component { formValue: '', }; - this.inputRef = React.createRef(); this.formSubmit = this.formSubmit.bind(this); this.handleCancel = this.handleCancel.bind(this); this.handleInputChange = this.handleInputChange.bind(this); } - /** - * - * @param {*} prevProps - * @param {*} prevState - */ - componentDidUpdate() { - const { addResourcesOpen } = this.props; - if (this.inputRef && this.inputRef.current && addResourcesOpen) { - this.inputRef.current.focus(); - } - } - /** * Reset the form state */ @@ -80,16 +67,19 @@ export class ManifestForm extends Component { render() { const { formValue } = this.state; const { + addResourcesOpen, classes, onCancel, t, } = this.props; + if (!addResourcesOpen) return null; + return ( <form onSubmit={this.formSubmit}> <Grid container spacing={2}> <Grid item xs={12} sm={8} md={9}> <TextField - inputRef={this.inputRef} + autoFocus fullWidth value={formValue} id="manifestURL" diff --git a/src/components/ManifestListItem.js b/src/components/ManifestListItem.js index 12f8516d2624005235febfce05cab80ae5bea972..edf14971b5ccc6f7115e67a7b92daebb2768ca50 100644 --- a/src/components/ManifestListItem.js +++ b/src/components/ManifestListItem.js @@ -104,21 +104,25 @@ export class ManifestListItem extends React.Component { > <Grid container spacing={2} className={classes.label} component="span"> <Grid item xs={4} sm={3} component="span"> - <Img - className={[classes.thumbnail, ns('manifest-list-item-thumb')].join(' ')} - src={[thumbnail]} - alt="" - height="80" - unloader={( - <Skeleton - variant="rect" - animation={false} - className={classes.placeholder} - height={80} - width={120} + { thumbnail + ? ( + <Img + className={[classes.thumbnail, ns('manifest-list-item-thumb')].join(' ')} + src={[thumbnail]} + alt="" + height="80" + unloader={( + <Skeleton + variant="rect" + animation={false} + className={classes.placeholder} + height={80} + width={120} + /> + )} /> - )} - /> + ) + : <Skeleton className={classes.placeholder} variant="rect" height={80} width={120} />} </Grid> <Grid item xs={8} sm={9} component="span"> { isCollection && ( @@ -134,26 +138,29 @@ export class ManifestListItem extends React.Component { </ButtonBase> </Grid> <Grid item xs={8} sm={4}> - <Typography className={ns('manifest-list-item-provider')}>{provider || t('addedFromUrl')}</Typography> - <Typography>{t('numItems', { number: size })}</Typography> + <Typography className={ns('manifest-list-item-provider')}>{provider}</Typography> + <Typography>{t('numItems', { count: size, number: size })}</Typography> </Grid> <Grid item xs={4} sm={2}> - <Img - src={[manifestLogo]} - alt="" - role="presentation" - className={classes.logo} - unloader={( - <Skeleton - variant="rect" - animation={false} - className={classes.placeholder} - height={60} - width={60} - /> + { manifestLogo + && ( + <Img + src={[manifestLogo]} + alt="" + role="presentation" + className={classes.logo} + unloader={( + <Skeleton + variant="rect" + animation={false} + className={classes.placeholder} + height={60} + width={60} + /> + )} + /> )} - /> </Grid> </Grid> ) : ( diff --git a/src/components/ManifestRelatedLinks.js b/src/components/ManifestRelatedLinks.js index 50d034e7b385700db1ec7e6038c102b315dc993f..533b4522d1b408ff62173ee5e964893651b50841 100644 --- a/src/components/ManifestRelatedLinks.js +++ b/src/components/ManifestRelatedLinks.js @@ -78,7 +78,7 @@ export class ManifestRelatedLinks extends Component { {related.label || related.value} </Link> { related.format && ( - <Typography component="span">{`(${related.format})`}</Typography> + <Typography component="span">{` (${related.format})`}</Typography> )} </Typography> )) diff --git a/src/components/PrimaryWindow.js b/src/components/PrimaryWindow.js index 852fef9f880c3194ff0ce022787f9d87bbc957a9..ee5d4e672c07edeaade90ed46aecd6058bca9661 100644 --- a/src/components/PrimaryWindow.js +++ b/src/components/PrimaryWindow.js @@ -75,14 +75,16 @@ export class PrimaryWindow extends Component { * Render the component */ render() { - const { isCollectionDialogVisible, windowId, classes } = this.props; + const { + isCollectionDialogVisible, windowId, classes, children, + } = this.props; return ( <div className={classNames(ns('primary-window'), classes.primaryWindow)}> <WindowSideBar windowId={windowId} /> <CompanionArea windowId={windowId} position="left" /> { isCollectionDialogVisible && <CollectionDialog windowId={windowId} /> } <Suspense fallback={<div />}> - {this.renderViewer()} + {children || this.renderViewer()} </Suspense> </div> ); @@ -91,6 +93,7 @@ export class PrimaryWindow extends Component { PrimaryWindow.propTypes = { audioResources: PropTypes.arrayOf(PropTypes.object), + children: PropTypes.node, classes: PropTypes.objectOf(PropTypes.string).isRequired, isCollection: PropTypes.bool, isCollectionDialogVisible: PropTypes.bool, @@ -102,6 +105,7 @@ PrimaryWindow.propTypes = { PrimaryWindow.defaultProps = { audioResources: [], + children: undefined, isCollection: false, isCollectionDialogVisible: false, isFetching: false, diff --git a/src/components/SearchPanelNavigation.js b/src/components/SearchPanelNavigation.js index 8a44237a8f7736f930acdc8e6f34937cefb4c8b9..59f9eb65b79d41e5e935791658c0ab5ab0106fcc 100644 --- a/src/components/SearchPanelNavigation.js +++ b/src/components/SearchPanelNavigation.js @@ -42,13 +42,17 @@ export class SearchPanelNavigation extends Component { */ render() { const { - searchHits, selectedContentSearchAnnotation, classes, t, direction, + numTotal, searchHits, selectedContentSearchAnnotation, classes, t, direction, } = this.props; const iconStyle = direction === 'rtl' ? { transform: 'rotate(180deg)' } : {}; const currentHitIndex = searchHits .findIndex(val => val.annotations.includes(selectedContentSearchAnnotation[0])); + let lengthText = searchHits.length; + if (searchHits.length < numTotal) { + lengthText += '+'; + } return ( <> {(searchHits.length > 0) && ( @@ -61,7 +65,7 @@ export class SearchPanelNavigation extends Component { <ChevronLeftIcon style={iconStyle} /> </MiradorMenuButton> <span style={{ unicodeBidi: 'plaintext' }}> - {t('pagination', { current: currentHitIndex + 1, total: searchHits.length })} + {t('pagination', { current: currentHitIndex + 1, total: lengthText })} </span> <MiradorMenuButton aria-label={t('searchNextResult')} @@ -79,6 +83,7 @@ export class SearchPanelNavigation extends Component { SearchPanelNavigation.propTypes = { classes: PropTypes.objectOf(PropTypes.string), direction: PropTypes.string.isRequired, + numTotal: PropTypes.number, searchHits: PropTypes.arrayOf(PropTypes.object), searchService: PropTypes.shape({ id: PropTypes.string, @@ -90,6 +95,7 @@ SearchPanelNavigation.propTypes = { }; SearchPanelNavigation.defaultProps = { classes: {}, + numTotal: undefined, searchHits: [], t: key => key, }; diff --git a/src/components/SearchResults.js b/src/components/SearchResults.js index 23365db543d4dda44a0ef5ba1228d069bb8270e8..7c5ca415b3d3d993451d6b3c8c0044358c5f7c4f 100644 --- a/src/components/SearchResults.js +++ b/src/components/SearchResults.js @@ -89,6 +89,7 @@ export class SearchResults extends Component { query, searchAnnotations, searchHits, + searchNumTotal, t, windowId, } = this.props; @@ -122,8 +123,14 @@ export class SearchResults extends Component { </LiveMessenger> </List> { nextSearch && ( - <Button color="secondary" onClick={() => fetchSearch(windowId, companionWindowId, nextSearch, query)}> + <Button + className={classes.moreButton} + color="secondary" + onClick={() => fetchSearch(windowId, companionWindowId, nextSearch, query)} + > {t('moreResults')} + <br /> + {`(${t('searchResultsRemaining', { numLeft: searchNumTotal - searchHits.length })})`} </Button> )} </> @@ -144,6 +151,7 @@ SearchResults.propTypes = { query: PropTypes.string, searchAnnotations: PropTypes.arrayOf(PropTypes.object), searchHits: PropTypes.arrayOf(PropTypes.object), + searchNumTotal: PropTypes.number, t: PropTypes.func, windowId: PropTypes.string.isRequired, // eslint-disable-line react/no-unused-prop-types }; @@ -156,5 +164,6 @@ SearchResults.defaultProps = { query: undefined, searchAnnotations: [], searchHits: [], + searchNumTotal: undefined, t: k => k, }; diff --git a/src/components/ThumbnailCanvasGrouping.js b/src/components/ThumbnailCanvasGrouping.js index cc966ff815eba1093a823238a750ce70f5ac309c..72f08bb4a3479bf7060b5ed174952e8eca3cefe8 100644 --- a/src/components/ThumbnailCanvasGrouping.js +++ b/src/components/ThumbnailCanvasGrouping.js @@ -44,7 +44,7 @@ export class ThumbnailCanvasGrouping extends PureComponent { ...style, boxSizing: 'content-box', height: (Number.isInteger(style.height)) ? style.height - SPACING : null, - left: style.left + SPACING, + left: (Number.isInteger(style.left)) ? style.left + SPACING : null, top: style.top + SPACING, width: (Number.isInteger(style.width)) ? style.width - SPACING : null, }} diff --git a/src/components/ThumbnailNavigation.js b/src/components/ThumbnailNavigation.js index d887f8bf94143389fd6566b4ac6d7a5c7dd123b6..fb85a483815b11b45d68ddccb007814fd9f54915 100644 --- a/src/components/ThumbnailNavigation.js +++ b/src/components/ThumbnailNavigation.js @@ -69,7 +69,7 @@ export class ThumbnailNavigation extends Component { */ calculateScaledSize(index) { const { thumbnailNavigation, canvasGroupings, position } = this.props; - const canvases = canvasGroupings[index]; + const canvases = canvasGroupings[index] || []; const world = new CanvasWorld(canvases); const bounds = world.worldBounds(); switch (position) { diff --git a/src/components/VideoViewer.js b/src/components/VideoViewer.js index 31810a21c86cb3e5898829f077409be78b6ed394..4c285507f1299632e70de74ea473c1b238f7d51d 100644 --- a/src/components/VideoViewer.js +++ b/src/components/VideoViewer.js @@ -1,6 +1,6 @@ import flatten from 'lodash/flatten'; import flattenDeep from 'lodash/flattenDeep'; -import React, { Component } from 'react'; +import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import AnnotationItem from '../lib/AnnotationItem'; import AnnotationsOverlayVideo from '../containers/AnnotationsOverlayVideo'; @@ -85,7 +85,7 @@ export class VideoViewer extends Component { /** */ render() { const { - canvas, classes, currentTime, windowId, + canvas, classes, currentTime, videoOptions, windowId, } = this.props; const videoResources = flatten( @@ -119,7 +119,7 @@ export class VideoViewer extends Component { <div className={classes.flexFill}> { video && ( <> - <video className={classes.video} key={video.id} ref={this.videoRef}> + <video className={classes.video} key={video.id} ref={this.videoRef} {...videoOptions}> <source src={video.id} type={video.getFormat()} /> </video> <AnnotationsOverlayVideo windowId={windowId} videoRef={this.videoRef} videoTarget={videoTargetTemporalfragment} key={`${windowId} ${video.id}`} /> @@ -141,6 +141,7 @@ VideoViewer.propTypes = { paused: PropTypes.bool, setCurrentTime: PropTypes.func, setPaused: PropTypes.func, + videoOptions: PropTypes.object, // eslint-disable-line react/forbid-prop-types windowId: PropTypes.string.isRequired, }; @@ -151,4 +152,5 @@ VideoViewer.defaultProps = { paused: true, setCurrentTime: () => {}, setPaused: () => {}, + videoOptions: {}, }; diff --git a/src/components/WindowSideBarAnnotationsPanel.js b/src/components/WindowSideBarAnnotationsPanel.js index 06068abe31e400d2b9500238310850fdc64962ea..f5a13189296267a8fb9aa9acb0b6b43cf41ed87f 100644 --- a/src/components/WindowSideBarAnnotationsPanel.js +++ b/src/components/WindowSideBarAnnotationsPanel.js @@ -10,6 +10,13 @@ import ns from '../config/css-ns'; * WindowSideBarAnnotationsPanel ~ */ export class WindowSideBarAnnotationsPanel extends Component { + /** */ + constructor(props) { + super(props); + + this.containerRef = React.createRef(); + } + /** * Returns the rendered component */ @@ -23,15 +30,18 @@ export class WindowSideBarAnnotationsPanel extends Component { paperClassName={ns('window-sidebar-annotation-panel')} windowId={windowId} id={id} + ref={this.containerRef} + otherRef={this.containerRef} titleControls={<AnnotationSettings windowId={windowId} />} > <div className={classes.section}> - <Typography component="p" variant="subtitle2">{t('showingNumAnnotations', { number: annotationCount })}</Typography> + <Typography component="p" variant="subtitle2">{t('showingNumAnnotations', { count: annotationCount, number: annotationCount })}</Typography> </div> {canvasIds.map((canvasId, index) => ( <CanvasAnnotations canvasId={canvasId} + containerRef={this.containerRef} key={canvasId} index={index} totalSize={canvasIds.length} diff --git a/src/components/WindowSideBarCollectionPanel.js b/src/components/WindowSideBarCollectionPanel.js index 78b6f2b0c93def01ae376b1375a11acbf6adff7e..f480ec42ca2bda9e07da7cf56283f0745e57a9e4 100644 --- a/src/components/WindowSideBarCollectionPanel.js +++ b/src/components/WindowSideBarCollectionPanel.js @@ -31,7 +31,7 @@ export class WindowSideBarCollectionPanel extends Component { const behaviors = collection.getProperty('behavior'); - if (Array.isArray(behaviors)) return collection.includes('multi-part'); + if (Array.isArray(behaviors)) return behaviors.includes('multi-part'); return behaviors === 'multi-part'; } diff --git a/src/components/WorkspaceControlPanel.js b/src/components/WorkspaceControlPanel.js index 8b7fc2443674cfe17b3b402891f93cb8f27b11fb..de849c8fb72e11fbc5c7fe0ec8e51996e684c35e 100644 --- a/src/components/WorkspaceControlPanel.js +++ b/src/components/WorkspaceControlPanel.js @@ -4,8 +4,7 @@ import classNames from 'classnames'; import AppBar from '@material-ui/core/AppBar'; import Toolbar from '@material-ui/core/Toolbar'; import WorkspaceAddButton from '../containers/WorkspaceAddButton'; -import WorkspaceControlPanelButtons - from '../containers/WorkspaceControlPanelButtons'; +import WorkspaceControlPanelButtons from '../containers/WorkspaceControlPanelButtons'; import Branding from '../containers/Branding'; import ns from '../config/css-ns'; diff --git a/src/components/WorkspaceExport.js b/src/components/WorkspaceExport.js index a9ce6ed290f8274d88a1912ad26e115a3b25d0bc..2a7ff7e0ce6a882742e5ee6ecb10ac30466d52ae 100644 --- a/src/components/WorkspaceExport.js +++ b/src/components/WorkspaceExport.js @@ -3,13 +3,17 @@ import Button from '@material-ui/core/Button'; import Dialog from '@material-ui/core/Dialog'; import DialogActions from '@material-ui/core/DialogActions'; import DialogTitle from '@material-ui/core/DialogTitle'; +import DialogContent from '@material-ui/core/DialogContent'; import Typography from '@material-ui/core/Typography'; import Snackbar from '@material-ui/core/Snackbar'; import IconButton from '@material-ui/core/IconButton'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import CloseIcon from '@material-ui/icons/Close'; +import Accordion from '@material-ui/core/Accordion'; +import AccordionSummary from '@material-ui/core/AccordionSummary'; +import AccordionDetails from '@material-ui/core/AccordionDetails'; import PropTypes from 'prop-types'; import { CopyToClipboard } from 'react-copy-to-clipboard'; -import ScrollIndicatedDialogContent from '../containers/ScrollIndicatedDialogContent'; /** */ @@ -50,7 +54,7 @@ export class WorkspaceExport extends Component { */ render() { const { - children, container, open, t, + children, classes, container, open, t, } = this.props; const { copied } = this.state; @@ -87,12 +91,24 @@ export class WorkspaceExport extends Component { <DialogTitle id="form-dialog-title" disableTypography> <Typography variant="h2">{t('downloadExport')}</Typography> </DialogTitle> - <ScrollIndicatedDialogContent> - {children} - <pre> - {this.exportedState()} - </pre> - </ScrollIndicatedDialogContent> + + <DialogContent> + <Accordion elevation={0}> + <AccordionSummary + classes={{ root: classes.accordionTitle }} + expandIcon={<ExpandMoreIcon />} + > + <Typography variant="h4">{t('viewWorkspaceConfiguration')}</Typography> + </AccordionSummary> + <AccordionDetails> + {children} + <pre> + {this.exportedState()} + </pre> + </AccordionDetails> + </Accordion> + </DialogContent> + <DialogActions> <Button onClick={this.handleClose}>{t('cancel')}</Button> <CopyToClipboard @@ -109,6 +125,7 @@ export class WorkspaceExport extends Component { WorkspaceExport.propTypes = { children: PropTypes.node, + classes: PropTypes.objectOf(PropTypes.string), container: PropTypes.object, // eslint-disable-line react/forbid-prop-types exportableState: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types handleClose: PropTypes.func.isRequired, @@ -118,6 +135,7 @@ WorkspaceExport.propTypes = { WorkspaceExport.defaultProps = { children: null, + classes: {}, container: null, open: false, t: key => key, diff --git a/src/config/settings.js b/src/config/settings.js index 414de473472616592b29ea6610e8c86200c010e7..50e2bc752798c25001332572c1e6826c66260dab 100644 --- a/src/config/settings.js +++ b/src/config/settings.js @@ -4,7 +4,7 @@ export default { state: { // slice: 'mirador' // Configure the top-level slice of state for mirador selectors }, - canvasNavigation: { // Set the hight and width of canvas thumbnails in the CanvasNavigation companion window + canvasNavigation: { // Set the height and width of canvas thumbnails in the CanvasNavigation companion window height: 50, width: 50, }, @@ -222,13 +222,18 @@ export default { en: 'English', fr: 'Français', ja: '日本語', + kr: '한국어', lt: 'Lietuvių', nl: 'Nederlands', + 'nb-NO': 'Norwegian Bokmål', + pl: 'Polski', 'pt-BR': 'Português do Brasil', + vi:'Tiếng Việt', 'zh-CN': '中文(简体)', 'zh-TW': '中文(繁體)', it: "Italiano", sr: 'Српски', + sv: 'Svenska' }, annotations: { htmlSanitizationRuleSet: 'iiif', // See src/lib/htmlRules.js for acceptable values @@ -268,13 +273,14 @@ export default { highlightAllAnnotations: false, // Configure whether to display annotations on the canvas by default showLocalePicker: false, // Configure locale picker for multi-lingual metadata sideBarOpen: false, // Configure if the sidebar (and its content panel) is open by default + switchCanvasOnSearch: true, // Configure if Mirador should automatically switch to the canvas of the first search result panels: { // Configure which panels are visible in WindowSideBarButtons info: true, attribution: true, canvas: true, annotations: true, search: true, - layers: false, + layers: true, }, views: [ { key: 'single', behaviors: ['individuals'] }, @@ -282,6 +288,10 @@ export default { { key: 'scroll', behaviors: ['continuous'] }, { key: 'gallery' }, ], + elastic: { + height: 400, + width: 480 + } }, windows: [ // Array of windows to be open when mirador initializes (each object should at least provide a manifestId key with the value of the IIIF presentation manifest to load) /** @@ -295,6 +305,9 @@ export default { // ../lib/MiradorViewer.js `windowAction` */ ], + thumbnails: { + preferredFormats: ['jpg', 'png', 'webp', 'tif'], + }, thumbnailNavigation: { defaultPosition: 'off', // Which position for the thumbnail navigation to be be displayed. Other possible values are "far-bottom" or "far-right" displaySettings: true, // Display the settings for this in WindowTopMenu @@ -342,6 +355,14 @@ export default { windows: true, workspace: true, }, + audioOptions: { // Additional props passed to <audio> element + controls: true, + crossOrigin: 'anonymous', + }, + videoOptions: { // Additional props passed to <audio> element + controls: true, + crossOrigin: 'anonymous', + }, auth: { serviceProfiles: [ { profile: 'http://iiif.io/api/auth/1/external', external: true }, diff --git a/src/containers/AudioViewer.js b/src/containers/AudioViewer.js index 65d198f5ad1be542536bb025064a6c25810c418e..e422c6d6b6e00458773de3c365080307590b9fd9 100644 --- a/src/containers/AudioViewer.js +++ b/src/containers/AudioViewer.js @@ -4,12 +4,14 @@ import { withTranslation } from 'react-i18next'; import { withStyles } from '@material-ui/core'; import { withPlugins } from '../extend/withPlugins'; import { AudioViewer } from '../components/AudioViewer'; -import { getVisibleCanvasAudioResources } from '../state/selectors'; +import { getConfig, getVisibleCanvasAudioResources, getVisibleCanvasCaptions } from '../state/selectors'; /** */ const mapStateToProps = (state, { windowId }) => ( { + audioOptions: getConfig(state).audioOptions, audioResources: getVisibleCanvasAudioResources(state, { windowId }) || [], + captions: getVisibleCanvasCaptions(state, { windowId }) || [], } ); diff --git a/src/containers/CollectionDialog.js b/src/containers/CollectionDialog.js index d663a90fa87a820e398fbe47bd5f248898a12d85..22210c1658156664525855c1142616563c5ce79b 100644 --- a/src/containers/CollectionDialog.js +++ b/src/containers/CollectionDialog.js @@ -54,6 +54,9 @@ const styles = theme => ({ padding: '16px', paddingTop: 0, }, + collectionItem: { + whiteSpace: 'normal', + }, collectionMetadata: { padding: '16px', }, @@ -64,7 +67,7 @@ const styles = theme => ({ position: 'absolute !important', }, dialogContent: { - padding: 0, + padding: theme.spacing(1), }, light: { color: theme.palette.grey[400], diff --git a/src/containers/CustomPanel.js b/src/containers/CustomPanel.js index c9dfa5dec5ad5ae2320f331132974eb51afeda4e..493ad146335bc1e55955512e721ad1bd83244ed4 100644 --- a/src/containers/CustomPanel.js +++ b/src/containers/CustomPanel.js @@ -4,8 +4,6 @@ import { withTranslation } from 'react-i18next'; import { withStyles } from '@material-ui/core/styles'; import { withPlugins } from '../extend/withPlugins'; import { CustomPanel } from '../components/CustomPanel'; -import { -} from '../state/selectors'; /** * mapStateToProps - to hook up connect diff --git a/src/containers/IIIFAuthentication.js b/src/containers/IIIFAuthentication.js index ddc37415752a29259249f3356abb9e57acdd7134..7d51087168d91d51425c07b196e6e911ba37a3b2 100644 --- a/src/containers/IIIFAuthentication.js +++ b/src/containers/IIIFAuthentication.js @@ -1,7 +1,7 @@ import { connect } from 'react-redux'; import { compose } from 'redux'; import { withTranslation } from 'react-i18next'; -import { Utils } from 'manifesto.js/dist-esmodule/Utils'; +import { Utils } from 'manifesto.js'; import { withPlugins } from '../extend/withPlugins'; import * as actions from '../state/actions'; import { diff --git a/src/containers/IIIFThumbnail.js b/src/containers/IIIFThumbnail.js index 71c95d0a4900ee5b46b177743c6952356b6e5644..c596b508cd92fee25b69b5e09e198011182363e7 100644 --- a/src/containers/IIIFThumbnail.js +++ b/src/containers/IIIFThumbnail.js @@ -1,9 +1,21 @@ import { compose } from 'redux'; +import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; import { withStyles } from '@material-ui/core/styles'; import { withPlugins } from '../extend/withPlugins'; +import { + getConfig, +} from '../state/selectors'; import { IIIFThumbnail } from '../components/IIIFThumbnail'; +/** + * mapStateToProps - to hook up connect + * @private + */ +const mapStateToProps = (state) => ({ + thumbnailsConfig: getConfig(state).thumbnails, +}); + /** * Styles for withStyles HOC */ @@ -50,6 +62,7 @@ const styles = theme => ({ const enhance = compose( withStyles(styles), withTranslation(), + connect(mapStateToProps), withPlugins('IIIFThumbnail'), ); diff --git a/src/containers/SearchPanelNavigation.js b/src/containers/SearchPanelNavigation.js index 2202addf23231006a747920cf0ffc1771d6b2d3d..614b2419209114973c256fc4029944724185b715 100644 --- a/src/containers/SearchPanelNavigation.js +++ b/src/containers/SearchPanelNavigation.js @@ -7,6 +7,7 @@ import { SearchPanelNavigation } from '../components/SearchPanelNavigation'; import * as actions from '../state/actions'; import { getSelectedContentSearchAnnotationIds, + getSearchNumTotal, getSortedSearchHitsForCompanionWindow, getThemeDirection, } from '../state/selectors'; @@ -18,6 +19,7 @@ import { */ const mapStateToProps = (state, { companionWindowId, windowId }) => ({ direction: getThemeDirection(state), + numTotal: getSearchNumTotal(state, { companionWindowId, windowId }), searchHits: getSortedSearchHitsForCompanionWindow(state, { companionWindowId, windowId }), selectedContentSearchAnnotation: getSelectedContentSearchAnnotationIds(state, { companionWindowId, windowId, diff --git a/src/containers/SearchResults.js b/src/containers/SearchResults.js index 8f451a3a3a6421ab59ebaa69a1e7ddea7f265dd2..15e660d99a297bfdb9a13a57d4f19e48697c8097 100644 --- a/src/containers/SearchResults.js +++ b/src/containers/SearchResults.js @@ -9,6 +9,7 @@ import { getNextSearchId, getSearchQuery, getSearchIsFetching, + getSearchNumTotal, getSortedSearchHitsForCompanionWindow, getSortedSearchAnnotationsForCompanionWindow, } from '../state/selectors'; @@ -25,6 +26,7 @@ const mapStateToProps = (state, { companionWindowId, windowId }) => ({ searchAnnotations: getSortedSearchAnnotationsForCompanionWindow(state, { companionWindowId, windowId }), searchHits: getSortedSearchHitsForCompanionWindow(state, { companionWindowId, windowId }), + searchNumTotal: getSearchNumTotal(state, { companionWindowId, windowId }), }); const mapDispatchToProps = { @@ -33,6 +35,9 @@ const mapDispatchToProps = { /** */ const styles = theme => ({ + moreButton: { + width: '100%', + }, navigation: { textTransform: 'none', }, diff --git a/src/containers/VideoViewer.js b/src/containers/VideoViewer.js index d75b83509c093b8b7470ded254cb951084683d93..bbd148c854fb53e0ad20a9fe6734f66683af6ffe 100644 --- a/src/containers/VideoViewer.js +++ b/src/containers/VideoViewer.js @@ -6,6 +6,7 @@ import { withPlugins } from '../extend/withPlugins'; import * as actions from '../state/actions'; import { VideoViewer } from '../components/VideoViewer'; import { + getConfig, getCurrentCanvas, getCurrentCanvasWorld, getWindowMutedStatus, @@ -20,6 +21,7 @@ const mapStateToProps = (state, { windowId }) => ({ currentTime: getWindowCurrentTime(state, { windowId }), muted: getWindowMutedStatus(state, { windowId }), paused: getWindowPausedStatus(state, { windowId }), + videoOptions: getConfig(state).videoOptions, }); /** */ diff --git a/src/containers/WindowTopMenuButton.js b/src/containers/WindowTopMenuButton.js index dfd9a1a66040d0b78ab5a056f05138f5c48b2aac..54d863e2b688ca032ac5ed7af9e093e176b7fc18 100644 --- a/src/containers/WindowTopMenuButton.js +++ b/src/containers/WindowTopMenuButton.js @@ -18,7 +18,7 @@ const styles = theme => ({ const enhance = compose( withTranslation(), withStyles(styles), - withPlugins('WindowTopMenuButtons'), + withPlugins('WindowTopMenuButton'), ); export default enhance(WindowTopMenuButton); diff --git a/src/containers/WorkspaceControlPanelButtons.js b/src/containers/WorkspaceControlPanelButtons.js index 28f912a226f0da3b45d357421a3b5746c06a20c1..eaef08bd775196782632a0bb6022759aa41b3d57 100644 --- a/src/containers/WorkspaceControlPanelButtons.js +++ b/src/containers/WorkspaceControlPanelButtons.js @@ -1,8 +1,7 @@ import { compose } from 'redux'; import { withStyles } from '@material-ui/core/styles'; import { withPlugins } from '../extend/withPlugins'; -import { WorkspaceControlPanelButtons } - from '../components/WorkspaceControlPanelButtons'; +import { WorkspaceControlPanelButtons } from '../components/WorkspaceControlPanelButtons'; /** * diff --git a/src/containers/WorkspaceExport.js b/src/containers/WorkspaceExport.js index 069a52542815f19231609bdfe7e0f81997efbff7..e1ac982c10aa03c0cff9451dd1c3fbb63bac98ab 100644 --- a/src/containers/WorkspaceExport.js +++ b/src/containers/WorkspaceExport.js @@ -1,5 +1,6 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; +import { withStyles } from '@material-ui/core/styles'; import { withTranslation } from 'react-i18next'; import { withPlugins } from '../extend/withPlugins'; import { WorkspaceExport } from '../components/WorkspaceExport'; @@ -16,8 +17,18 @@ const mapStateToProps = state => ({ exportableState: getExportableState(state), }); +/** + * Styles for the withStyles HOC + */ +const styles = theme => ({ + accordionTitle: { + padding: 0, + }, +}); + const enhance = compose( withTranslation(), + withStyles(styles), connect(mapStateToProps, {}), withPlugins('WorkspaceExport'), ); diff --git a/src/extend/PluginProvider.js b/src/extend/PluginProvider.js index ee9269e58f462c7a7500be9c946bd698d2604e0b..784191184315d514b59b25a64e23bd1897bc9f1b 100644 --- a/src/extend/PluginProvider.js +++ b/src/extend/PluginProvider.js @@ -16,7 +16,7 @@ export default function PluginProvider(props) { const connectedPlugins = connectPluginsToStore(plugins); addPluginsToCompanionWindowsRegistry(connectedPlugins); setPluginMap(createTargetToPluginMapping(connectedPlugins)); - }, []); + }, [plugins]); return ( <PluginContext.Provider value={pluginMap}> diff --git a/src/extend/withPlugins.js b/src/extend/withPlugins.js index b59530b83e9c41389945de3c94074925eff5d081..11320b1b737d222e4aa96ea20edec79de2cf50a9 100644 --- a/src/extend/withPlugins.js +++ b/src/extend/withPlugins.js @@ -43,7 +43,8 @@ function _withPlugins(targetName, TargetComponent) { // eslint-disable-line no-u ); }; - return plugins.wrap.reverse().reduce(pluginWrapper, <TargetComponent {...passDownProps} />); + return plugins.wrap.slice().reverse() + .reduce(pluginWrapper, <TargetComponent {...passDownProps} />); } const whatever = React.forwardRef(PluginHoc); diff --git a/src/i18n.js b/src/i18n.js index 3e96468d77b6f08ca2ab3e032d4853aa50eaf280..34f07f673713e0de8cee55b4a169ed9c0d34a844 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -7,14 +7,21 @@ import zhCn from './locales/zhCn/translation.json'; import zhTw from './locales/zhTw/translation.json'; import fr from './locales/fr/translation.json'; import ja from './locales/ja/translation.json'; +import kr from './locales/kr/translation.json'; import nl from './locales/nl/translation.json'; +import pl from './locales/pl/translation.json'; import ptBr from './locales/ptBr/translation.json'; import it from './locales/it/translation.json'; import sr from './locales/sr/translation.json'; +import sv from './locales/sv/translation.json'; import lt from './locales/lt/translation.json'; +import vi from './locales/vi/translation.json'; +import nbNo from './locales/nbNo/translation.json'; -export default () => { - // Load translations for each language +/** + * Load translations for each language + */ +function createI18nInstance() { const resources = { ar, de, @@ -22,10 +29,15 @@ export default () => { fr, it, ja, + kr, lt, + 'nb-NO': nbNo, nl, + pl, 'pt-BR': ptBr, sr, + sv, + vi, 'zh-CN': zhCn, 'zh-TW': zhTw, }; @@ -41,4 +53,6 @@ export default () => { }); return instance; -}; +} + +export default createI18nInstance; diff --git a/src/index.js b/src/index.js index a5ebe2e6e1c1356f03eb736c3975d6e9bef0d431..84091415de8d84c0280ce9dae386a265813f3ef8 100644 --- a/src/index.js +++ b/src/index.js @@ -1,9 +1,7 @@ import init from './init'; import state from './state'; -const exports = { +export default { ...init, ...state, }; - -export default exports; diff --git a/src/init.js b/src/init.js index 87aa17dcc636e841f4744ae18f76f723a49e9d19..1604375329f19d61786424596b798c19880bbe39 100644 --- a/src/init.js +++ b/src/init.js @@ -15,8 +15,6 @@ function viewer(config, pluginsOrStruct) { return new MiradorViewer(config, struct); } -const exports = { +export default { viewer, }; - -export default exports; diff --git a/src/lib/CanvasAnnotationDisplay.js b/src/lib/CanvasAnnotationDisplay.js index 46be9292df09febf2c845e966fa86c3bcbcf65d1..437f9cfe58baab8fbc7cc57274291fc08c4c9eb6 100644 --- a/src/lib/CanvasAnnotationDisplay.js +++ b/src/lib/CanvasAnnotationDisplay.js @@ -22,7 +22,7 @@ export default class CanvasAnnotationDisplay { this.context = context; if (this.resource.svgSelector) { this.svgContext(); - } else { + } else if (this.resource.fragmentSelector) { this.fragmentContext(); } } diff --git a/src/lib/MiradorViewer.js b/src/lib/MiradorViewer.js index 7cbd0b5c8d1838baf7ee0efdea10bb735e46f283..c9238b84d9057a70b8c96dd543ca2584177333ed 100644 --- a/src/lib/MiradorViewer.js +++ b/src/lib/MiradorViewer.js @@ -1,16 +1,11 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; -import deepmerge from 'deepmerge'; import HotApp from '../components/App'; -import createStore from '../state/createStore'; -import { importConfig } from '../state/actions/config'; import { filterValidPlugins, - getConfigFromPlugins, - getReducersFromPlugins, - getSagasFromPlugins, } from '../extend/pluginPreprocessing'; +import createPluggableStore from '../state/createPluggableStore'; /** * Default Mirador instantiation @@ -19,28 +14,25 @@ class MiradorViewer { /** */ constructor(config, viewerConfig = {}) { - this.config = config; this.plugins = filterValidPlugins(viewerConfig.plugins || []); + this.config = config; this.store = viewerConfig.store - || createStore(getReducersFromPlugins(this.plugins), getSagasFromPlugins(this.plugins)); - this.processConfig(); + || createPluggableStore(this.config, this.plugins); - ReactDOM.render( - <Provider store={this.store}> - <HotApp plugins={this.plugins} /> - </Provider>, + config.id && ReactDOM.render( + this.render(), document.getElementById(config.id), ); } /** - * Process config with plugin configs into actions + * Render the mirador viewer */ - processConfig() { - this.store.dispatch( - importConfig( - deepmerge(getConfigFromPlugins(this.plugins), this.config), - ), + render(props = {}) { + return ( + <Provider store={this.store}> + <HotApp plugins={this.plugins} {...props} /> + </Provider> ); } @@ -48,7 +40,7 @@ class MiradorViewer { * Cleanup method to unmount Mirador from the dom */ unmount() { - ReactDOM.unmountComponentAtNode(document.getElementById(this.config.id)); + this.config.id && ReactDOM.unmountComponentAtNode(document.getElementById(this.config.id)); } } diff --git a/src/lib/OpenSeadragonCanvasOverlay.js b/src/lib/OpenSeadragonCanvasOverlay.js index d10bd5014b0da1dc612f778caa049449cda5b8da..fd69cf31c98b31e7469d33b8d77777f8873e9f64 100644 --- a/src/lib/OpenSeadragonCanvasOverlay.js +++ b/src/lib/OpenSeadragonCanvasOverlay.js @@ -58,7 +58,7 @@ export default class OpenSeadragonCanvasOverlay { } this.viewportOrigin = new OpenSeadragon.Point(0, 0); - const boundsRect = this.viewer.viewport.getBoundsNoRotate(true); + const boundsRect = this.viewer.viewport.getBoundsNoRotateWithMargins(true); this.viewportOrigin.x = boundsRect.x; this.viewportOrigin.y = boundsRect.y * this.imgAspectRatio; diff --git a/src/lib/ThumbnailFactory.js b/src/lib/ThumbnailFactory.js index 7cbacef410ddd9097fc44eac8abc3369156fc210..79c063f8852c82b9044640ed28e33d25b8e6de3d 100644 --- a/src/lib/ThumbnailFactory.js +++ b/src/lib/ThumbnailFactory.js @@ -1,12 +1,7 @@ import { Utils } from 'manifesto.js'; import MiradorManifest from './MiradorManifest'; import MiradorCanvas from './MiradorCanvas'; - -/** */ -function asArray(value) { - if (value === undefined) return []; - return Array.isArray(value) ? value : [value]; -} +import asArray from './asArray'; /** */ function isLevel0ImageProfile(service) { @@ -67,8 +62,66 @@ class ThumbnailFactory { } /** - * Creates a canonical image request for a thumb - * @param {Number} height + * Selects the image resource that is representative of the given canvas. + * @param {Object} canvas A Manifesto Canvas + * @return {Object} A Manifesto Image Resource + */ + static getPreferredImage(canvas) { + const miradorCanvas = new MiradorCanvas(canvas); + return miradorCanvas.iiifImageResources[0] || miradorCanvas.imageResource; + } + + /** + * Chooses the best available image size based on a target area (w x h) value. + * @param {Object} service A IIIF Image API service that has a `sizes` array + * @param {Number} targetArea The target area value to compare potential sizes against + * @return {Object|undefined} The best size, or undefined if none are acceptable + */ + static selectBestImageSize(service, targetArea) { + const sizes = asArray(service.getProperty('sizes')); + + let closestSize = { + default: true, + height: service.getProperty('height') || Number.MAX_SAFE_INTEGER, + width: service.getProperty('width') || Number.MAX_SAFE_INTEGER, + }; + + /** Compare the total image area to our target */ + const imageFitness = (test) => test.width * test.height - targetArea; + + /** Look for the size that's just bigger than we prefer... */ + closestSize = sizes.reduce( + (best, test) => { + const score = imageFitness(test); + + if (score < 0) return best; + + return Math.abs(score) < Math.abs(imageFitness(best)) + ? test + : best; + }, closestSize, + ); + + /** .... but not "too" big; we'd rather scale up an image than download too much */ + if (closestSize.width * closestSize.height > targetArea * 6) { + closestSize = sizes.reduce( + (best, test) => ( + Math.abs(imageFitness(test)) < Math.abs(imageFitness(best)) + ? test + : best + ), closestSize, + ); + } + + if (closestSize.default) return undefined; + + return closestSize; + } + + /** + * Determines the appropriate thumbnail to use to represent an Image Resource. + * @param {Object} resource The Image Resource from which to derive a thumbnail + * @return {Object} The thumbnail URL and any spatial dimensions that can be determined */ iiifThumbnailUrl(resource) { let size; @@ -84,63 +137,26 @@ class ThumbnailFactory { const service = iiifImageService(resource); - if (!service) return undefined; + if (!service) return ThumbnailFactory.staticImageUrl(resource); const aspectRatio = resource.getWidth() && resource.getHeight() && (resource.getWidth() / resource.getHeight()); + const target = (requestedMaxWidth && requestedMaxHeight) + ? requestedMaxWidth * requestedMaxHeight + : maxHeight * maxWidth; + const closestSize = ThumbnailFactory.selectBestImageSize(service, target); - // just bail to a static image, even though sizes might provide something better - if (isLevel0ImageProfile(service)) { - const sizes = asArray(service.getProperty('sizes')); - const serviceHeight = service.getProperty('height'); - const serviceWidth = service.getProperty('width'); - - const target = (requestedMaxWidth && requestedMaxHeight) - ? requestedMaxWidth * requestedMaxHeight - : maxHeight * maxWidth; - - let closestSize = { - default: true, - height: serviceHeight || Number.MAX_SAFE_INTEGER, - width: serviceWidth || Number.MAX_SAFE_INTEGER, - }; - - /** Compare the total image area to our target */ - const imageFitness = (test) => test.width * test.height - target; - - /** Look for the size that's just bigger than we prefer... */ - closestSize = sizes.reduce( - (best, test) => { - const score = imageFitness(test); - - if (score < 0) return best; - - return Math.abs(score) < Math.abs(imageFitness(best)) - ? test - : best; - }, closestSize, - ); - - /** .... but not "too" big; we'd rather scale up an image than download too much */ - if (closestSize.width * closestSize.height > target * 6) { - closestSize = sizes.reduce( - (best, test) => ( - Math.abs(imageFitness(test)) < Math.abs(imageFitness(best)) - ? test - : best - ), closestSize, - ); - } - - /** Bail if the best available size is the full size.. maybe we'll get lucky with the @id */ - if (closestSize.default && !serviceHeight && !serviceWidth) { - return ThumbnailFactory.staticImageUrl(resource); - } - + if (closestSize) { + // Embedded service advertises an appropriate size width = closestSize.width; height = closestSize.height; size = `${width},${height}`; + } else if (isLevel0ImageProfile(service)) { + /** Bail if the best available size is the full size.. maybe we'll get lucky with the @id */ + if (!service.getProperty('height') && !service.getProperty('width')) { + return ThumbnailFactory.staticImageUrl(resource); + } } else if (requestedMaxHeight && requestedMaxWidth) { // IIIF level 2, no problem. if (isLevel2ImageProfile(service)) { @@ -176,7 +192,7 @@ class ThumbnailFactory { const region = 'full'; const quality = Utils.getImageQuality(service.getProfile()); const id = service.id.replace(/\/+$/, ''); - const format = 'jpg'; + const format = this.getFormat(service); return { height, url: [id, region, size, 0, `${quality}.${format}`].join('/'), @@ -184,77 +200,111 @@ class ThumbnailFactory { }; } - /** */ - getThumbnail(resource, { requireIiif, quirksMode }) { - if (!resource) return undefined; - const thumb = resource.getThumbnail(); - if (thumb && iiifImageService(thumb)) return this.iiifThumbnailUrl(thumb); + /** + * Figure out what format thumbnail to use by looking at the preferred formats + * on offer, and selecting a format shared in common with the application's + * preferred format list. + * + * Fall back to jpg, which is required to work for all IIIF services. + */ + getFormat(service) { + const { preferredFormats = [] } = this.iiifOpts; + const servicePreferredFormats = service.getProperty('preferredFormats'); - if (requireIiif) return undefined; - if (thumb && typeof thumb.__jsonld !== 'string') return ThumbnailFactory.staticImageUrl(thumb); + if (!servicePreferredFormats) return 'jpg'; - if (!quirksMode) return undefined; + const filteredFormats = servicePreferredFormats.filter( + value => preferredFormats.includes(value), + ); - return (thumb && typeof thumb.__jsonld === 'string') ? { url: thumb.__jsonld } : undefined; - } + // this is a format found in common between the preferred formats of the service + // and the application + if (filteredFormats[0]) return filteredFormats[0]; - /** */ - getResourceThumbnail(resource) { - const thumb = this.getThumbnail(resource, { requireIiif: true }); - - if (thumb) return thumb; + // IIIF Image API guarantees jpg support; if it wasn't provided by the service + // but the application is fine with it, we might as well try it. + if (!servicePreferredFormats.includes('jpg') && preferredFormats.includes('jpg')) { + return 'jpg'; + } - if (iiifImageService(resource)) return this.iiifThumbnailUrl(resource); - if (['image', 'dctypes:Image'].includes(resource.getProperty('type'))) return ThumbnailFactory.staticImageUrl(resource); + // there were no formats in common, and the application didn't want jpg... so + // just trust that the IIIF service is advertising something useful? + if (servicePreferredFormats[0]) return servicePreferredFormats[0]; - return this.getThumbnail(resource, { quirksMode: true, requireIiif: false }); + // JPG support is guaranteed by the spec, so it's a good worst-case fallback + return 'jpg'; } - /** */ - getIIIFThumbnail(canvas) { - const thumb = this.getThumbnail(canvas, { requireIiif: true }); - if (thumb) return thumb; + /** + * Determines the content resource from which to derive a thumbnail to represent a given resource. + * This method is recursive. + * @param {Object} resource A IIIF resource to derive a thumbnail from + * @return {Object|undefined} The Image Resource to derive a thumbnail from, or undefined + * if no appropriate resource exists + */ + getSourceContentResource(resource) { + const thumbnail = resource.getThumbnail(); + + // Any resource type may have a thumbnail + if (thumbnail) { + if (typeof thumbnail.__jsonld === 'string') return thumbnail.__jsonld; + + // Prefer an image's ImageService over its image's thumbnail + // Note that Collection, Manifest, and Canvas don't have `getType()` + if (!resource.isCollection() && !resource.isManifest() && !resource.isCanvas()) { + if (resource.getType() === 'image' && iiifImageService(resource) && !iiifImageService(thumbnail)) { + return resource; + } + } - const miradorCanvas = new MiradorCanvas(canvas); + return thumbnail; + } - const preferredCanvasResource = miradorCanvas.iiifImageResources[0] - || canvas.imageResource; + if (resource.isCollection()) { + const firstManifest = resource.getManifests()[0]; + if (firstManifest) return this.getSourceContentResource(firstManifest); - return (preferredCanvasResource && this.getResourceThumbnail(preferredCanvasResource)) - || this.getThumbnail(canvas, { quirksMode: true, requireIiif: false }); - } + return undefined; + } - /** */ - getManifestThumbnail(manifest) { - const thumb = this.getThumbnail(manifest, { requireIiif: true }); - if (thumb) return thumb; + if (resource.isManifest()) { + const miradorManifest = new MiradorManifest(resource); + const canvas = miradorManifest.startCanvas || miradorManifest.canvasAt(0); + if (canvas) return this.getSourceContentResource(canvas); - const miradorManifest = new MiradorManifest(manifest); - const canvas = miradorManifest.startCanvas || miradorManifest.canvasAt(0); + return undefined; + } - return (canvas && this.getIIIFThumbnail(canvas)) - || this.getThumbnail(manifest, { quirksMode: true, requireIiif: false }); - } + if (resource.isCanvas()) { + const image = ThumbnailFactory.getPreferredImage(resource); + if (image) return this.getSourceContentResource(image); - /** */ - getCollectionThumbnail(collection) { - const thumb = this.getThumbnail(collection, { requireIiif: true }); - if (thumb) return thumb; + return undefined; + } - const firstManifest = this.resource.getManifests()[0]; + if (resource.getType() === 'image') { + return resource; + } - return (firstManifest && this.getManifestThumbnail(firstManifest)) - || this.getThumbnail(collection, { quirksMode: true, requireIiif: false }); + return undefined; } - /** */ + /** + * Gets a thumbnail representing the resource. + * @return {Object|undefined} A thumbnail representing the resource, or undefined if none could + * be determined + */ get() { if (!this.resource) return undefined; - if (this.resource.isCanvas()) return this.getIIIFThumbnail(this.resource); - if (this.resource.isManifest()) return this.getManifestThumbnail(this.resource); - if (this.resource.isCollection()) return this.getCollectionThumbnail(this.resource); - return this.getResourceThumbnail(this.resource, { requireIiif: true }); + // Determine which content resource we should use to derive a thumbnail + const sourceContentResource = this.getSourceContentResource(this.resource); + if (!sourceContentResource) return undefined; + + // Special treatment for external resources + if (typeof sourceContentResource === 'string') return { url: sourceContentResource }; + + return this.iiifThumbnailUrl(sourceContentResource); } } @@ -263,4 +313,4 @@ function getBestThumbnail(resource, iiifOpts) { return new ThumbnailFactory(resource, iiifOpts).get(); } -export default getBestThumbnail; +export { getBestThumbnail as default, ThumbnailFactory }; diff --git a/src/lib/asArray.js b/src/lib/asArray.js new file mode 100644 index 0000000000000000000000000000000000000000..31557e924d1af3aa5fa01992f46470259f79bfac --- /dev/null +++ b/src/lib/asArray.js @@ -0,0 +1,11 @@ +/** + */ +export default function asArray(value) { + if (value === undefined) return []; + + if (!Array.isArray(value)) { + return [value]; + } + + return value; +} diff --git a/src/lib/htmlRules.js b/src/lib/htmlRules.js index 43fd823275b1bf50c98b29f68b4f8ef34353c09a..f50c3ae246f8b1219a2fe40af0a7947e8c798be9 100644 --- a/src/lib/htmlRules.js +++ b/src/lib/htmlRules.js @@ -18,9 +18,11 @@ const mirador2 = { ALLOWED_TAGS: ['a', 'b', 'br', 'i', 'img', 'p', 'span', 'strong', 'em', 'ul', 'ol', 'li'], }; -export default { +const htmlRules = { iiif, liberal, mirador2, noHtml, }; + +export default htmlRules; diff --git a/src/locales/de/translation.json b/src/locales/de/translation.json index c62314460a6d2dda76c8eb2ff16aba874248365b..b1d3ca8b74c2123e5fadf13309ce9e5a181057a0 100644 --- a/src/locales/de/translation.json +++ b/src/locales/de/translation.json @@ -25,7 +25,7 @@ "closeAddResourceMenu": "Ressourcenliste schließen", "closeCompanionWindow": "Hilfsfenster schließen", "closeWindow": "Fenster schließen", - "collapseSection": "Bereich {{section}} zuklappen", + "collapseSection": "Bereich \"{{section}}\" zuklappen", "collapseSidePanel": "Seitenleiste zuklappen", "itemList": "Kompaktliste", "continue": "Fortfahren", @@ -48,7 +48,7 @@ "errorDialogConfirm": "OK", "errorDialogTitle": "Es ist ein Fehler aufgetreten", "exitFullScreen": "Vollbildmodus verlassen", - "expandSection": "Bereich {{section}} aufklappen", + "expandSection": "Bereich \"{{section}}\" aufklappen", "expandSidePanel": "Seitenleiste aufklappen", "exportCopied": "Die Konfiguration der Arbeitsfläche wurde in die Zwischenablage kopiert.", "fetchManifest": "Hinzufügen", @@ -114,6 +114,7 @@ "searchNextResult": "Nächster Treffer", "searchNoResults": "Keine Treffer", "searchPreviousResult": "Vorheriger Treffer", + "searchResultsRemaining": "{{numLeft}} weitere", "searchSubmitAria": "Suchen", "searchTitle": "Suche", "selectWorkspaceMenu": "Wählen Sie einen Arbeitsflächentyp", diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 8c81ce23800a31175cadc714199b2f1805cc540e..1fe15e7418ee188bd100aacc7c2ef2ac7c12bf76 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -26,7 +26,7 @@ "closeAddResourceMenu": "Close resource list", "closeCompanionWindow": "Close panel", "closeWindow": "Close window", - "collapseSection": "Collapse {{section}} section", + "collapseSection": "Collapse \"{{section}}\" section", "collapseSidePanel": "Collapse sidebar", "collection": "Collection", "itemList": "Item list", @@ -50,7 +50,7 @@ "errorDialogConfirm": "OK", "errorDialogTitle": "An error occurred", "exitFullScreen": "Exit full screen", - "expandSection": "Expand {{section}} section", + "expandSection": "Expand \"{{section}}\" section", "expandSidePanel": "Expand sidebar", "exportCopied": "The workspace configuration was copied to your clipboard", "fetchManifest": "Add", @@ -91,9 +91,11 @@ "mosaicDescription": "Move and size windows in relation to each other, within the visible frame.", "moveCompanionWindowToBottom": "Move to bottom", "moveCompanionWindowToRight": "Move to right", + "multipartCollection": "Multipart Collection", "nextCanvas": "Next item", "noItemSelected": "No item selected", - "numItems": "{{number}} items", + "numItems": "{{number}} item", + "numItems_plural": "{{number}} items", "off": "Off", "openCompanionWindow_annotations": "Annotations", "openCompanionWindow_attribution": "Rights", @@ -117,10 +119,12 @@ "searchNextResult": "Next result", "searchNoResults": "No results found", "searchPreviousResult": "Previous result", + "searchResultsRemaining": "{{numLeft}} remaining", "searchSubmitAria": "Submit search", "searchTitle": "Search", "selectWorkspaceMenu": "Select workspace type", - "showingNumAnnotations": "Showing {{number}} annotations", + "showingNumAnnotations": "Showing {{number}} annotation", + "showingNumAnnotations_plural": "Showing {{number}} annotations", "showCollection": "Show collection", "showZoomControls": "Show zoom controls", "sidebarPanelsNavigation": "Sidebar panels navigation", @@ -133,11 +137,14 @@ "thumbnailNavigation": "Thumbnails", "thumbnails": "Thumbnails", "toggleWindowSideBar": "Toggle sidebar", - "totalCollections": "{{count}} collections", - "totalManifests": "{{count}} manifests", + "totalCollections": "{{count}} collection", + "totalCollections_plural": "{{count}} collections", + "totalManifests": "{{count}} manifest", + "totalManifests_plural": "{{count}} manifests", "tryAgain": "Try again", "untitled": "[Untitled]", "view": "View", + "viewWorkspaceConfiguration": "View workspace configuration", "welcome": "Welcome to Mirador", "window": "Window: {{label}}", "windowMenu": "Window views & thumbnail display", diff --git a/src/locales/fr/translation.json b/src/locales/fr/translation.json index 6c97b9e3861bcd05bb79fc4a195fdf23b6213fe9..4bb09cbbe478aa6cf25a46382703ca6a288d3711 100644 --- a/src/locales/fr/translation.json +++ b/src/locales/fr/translation.json @@ -25,7 +25,7 @@ "closeAddResourceMenu": "Fermer la liste des ressources", "closeCompanionWindow": "Fermer le panneau", "closeWindow": "Fermer cette fenêtre", - "collapseSection": "Replier la section {{section}} ", + "collapseSection": "Replier la section \"{{section}}\"", "collapseSidePanel": "Replier le panneau", "itemList": "Liste compacte", "continue": "Continuer", @@ -48,7 +48,7 @@ "errorDialogConfirm": "OK", "errorDialogTitle": "Une erreur est survenue", "exitFullScreen": "Quitter le plein écran", - "expandSection": "Déplier la section {{section}}", + "expandSection": "Déplier la section \"{{section}}\"", "expandSidePanel": "Déplier le panneau", "exportCopied": "La configuration de l'espace de travail a été copiée dans votre presse-papier", "fetchManifest": "Ajouter", @@ -91,7 +91,8 @@ "moveCompanionWindowToRight": "Déplacer à droite", "nextCanvas": "Suivant", "noItemSelected": "Aucun élément sélectionné", - "numItems": "{{number}} images", + "numItems": "{{number}} image", + "numItems_plural": "{{number}} images", "off": "aucun", "openCompanionWindow_annotations": "Annotations", "openCompanionWindow_attribution": "Droits", @@ -118,7 +119,8 @@ "searchSubmitAria": "Lancer la recherche", "searchTitle": "Rechercher", "selectWorkspaceMenu": "Changer de type d'espace de travail", - "showingNumAnnotations": "{{number}} annotations affichées", + "showingNumAnnotations": "{{number}} annotation affichée", + "showingNumAnnotations_plural": "{{number}} annotations affichées", "showCollection": "Voir la collection", "showZoomControls": "Activer les commandes de zoom", "sidebarPanelsNavigation": "Navigation dans les panneaux latéraux", @@ -131,8 +133,10 @@ "thumbnailNavigation": "Vignettes", "thumbnails": "Afficher les vignettes", "toggleWindowSideBar": "Afficher le menu latéral", - "totalCollections": "{{count}} collections", - "totalManifests": "{{count}} manifestes", + "totalCollections": "{{count}} collection", + "totalCollections_plural": "{{count}} collections", + "totalManifests": "{{count}} manifeste", + "totalManifests_plural": "{{count}} manifestes", "tryAgain": "Essayer à nouveau", "untitled": "[Sans titre]", "view": "Voir les images en mode", @@ -141,7 +145,7 @@ "windowMenu": "Options de fenêtre", "windowNavigation": "Navigation dans les fenêtres", "windowPluginButtons": "Options", - "windowPluginMenu": "Options de fenêtre", + "windowPluginMenu": "Autres options et outils", "workspace": "Espace de travail", "workspaceNavigation": "Menu de l'espace de travail", "workspaceFullScreen": "Plein écran", diff --git a/src/locales/it/translation.json b/src/locales/it/translation.json index c3b4ac8c793cb6649c478ae7b6aeef7a0252a073..9dfae93ef25a7a294182395a950402cd3cfcb2de 100644 --- a/src/locales/it/translation.json +++ b/src/locales/it/translation.json @@ -1,10 +1,11 @@ { "translation": { + "aboutMirador": "Informazioni su Mirador", "aboutThisItem": "Informazioni sull'oggetto", - "addedFromUrl": "(Aggiunto dall'URL)", "addManifestUrl": "URL della risorsa", "addManifestUrlHelp": "L'URL di una risorsa IIIF", "addResource": "Aggiungi una risorsa", + "addedFromUrl": "(Aggiunto dall'URL)", "annotationCanvasLabel_1/1": "Oggetto: [{{label}}]", "annotationCanvasLabel_1/2": "Sinistra: [{{label}}]", "annotationCanvasLabel_2/2": "Destra: [{{label}}]", @@ -20,13 +21,14 @@ "canvasIndex": "Indice", "changeTheme": "Cambia tema", "clearSearch": "pulisci", + "close": "Chiudi", "closeAddResourceForm": "Chiudi il modulo", "closeAddResourceMenu": "Chiudi la lista di risorse", "closeCompanionWindow": "Chiudi il pannello", "closeWindow": "Chiudi finestra", - "collapseSection": "Collassa la sezione {{section}}", + "collapseSection": "Collassa la sezione \"{{section}}\"", "collapseSidePanel": "Collassa la barra laterale", - "itemList": "Lista compatta", + "collection": "Collezione", "continue": "Continua", "copy": "Copia", "currentItem": "Oggetto corrente", @@ -34,35 +36,49 @@ "currentItem_1/2": "Sinistra", "currentItem_2/2": "Destra", "dark": "Tema scuro", + "digitizedView": "Digitized view", "dismiss": "Dismiss", - "highlightAllAnnotations": "Evidenzia tutto", + "displayNoAnnotations": "Nascondi evidenziazione", "downloadExport": "Esporta il workspace", "downloadExportWorkspace": "Esporta il workspace", "elastic": "Elastico", "elasticDescription": "Muovi e ridimensiona le finestre liberamente in un workspace illimitato. Le finestre possono sovrapporsi.", "emptyResourceList": "La tua lista di risorse è vuota.", + "error": "Errore", "errorDialogConfirm": "OK", "errorDialogTitle": "Si è verificato un errore.", "exitFullScreen": "Esci da schermo intero", - "expandSection": "Espandi la sezione {{section}}", + "expandSection": "Espandi la sezione \"{{section}}\"", "expandSidePanel": "Espandi la barra laterale", + "exportCopied": "La configurazione del workspace è stata copiata nella tua clipboard", "fetchManifest": "Aggiungi", "fullScreen": "Schermo intero", "gallery": "Galleria", "hideZoomControls": "Nascondi i controlli di zoom", + "highlightAllAnnotations": "Evidenzia tutto", "iiif_homepage": "Informazioni su questa risorsa", "iiif_manifest": "IIIF manifest", "iiif_renderings": "Formati alternativi", "iiif_seeAlso": "Vedi anche", - "import" : "Importa", + "import": "Importa", "importWorkspace": "Importa workspace", "importWorkspaceHint": "Incolla una configurazione di Mirador 3 da importare", "item": "Oggetto: {{label}}", + "itemList": "Lista compatta", + "jsError": "Dettagli tecnici", + "jsStack": "{{ stack }}", "language": "Lingua", + "layer_hide": "Nascondi livello", + "layer_move": "Sposta livello", + "layer_moveToTop": "Sposta il livello in alto", + "layer_opacity": "Opacità del livello", + "layer_show": "Visualizza livello", + "layers": "Livelli", "light": "Tema chiaro", "links": "Link", "listAllOpenWindows": "Vai alla finestra", "login": "Entra", + "logout": "Esci", "manifestError": "La risorsa non può essere aggiunta:", "maximizeWindow": "Massimizza la finestra", "minimizeWindow": "Minimizza la finestra", @@ -76,12 +92,15 @@ "moveCompanionWindowToBottom": "Sposta in fondo", "moveCompanionWindowToRight": "Sposta a destra", "nextCanvas": "Prossimo oggetto", + "noItemSelected": "Nessun oggetto selezionato", "numItems": "{{number}} oggetti", + "numItems_plural": "{{number}} oggetti", "off": "Off", "openCompanionWindow_annotations": "Annotazioni", "openCompanionWindow_attribution": "Diritti", "openCompanionWindow_canvas": "Indice", "openCompanionWindow_info": "Informazioni", + "openCompanionWindow_layers": "Livelli", "openCompanionWindow_search": "Cerca", "openInCompanionWindow": "Apri in un pannello separato", "openWindows": "Finestre aperte in questo momento", @@ -94,27 +113,37 @@ "retry": "Riprova", "right": "Right", "rights": "Licenza", + "scroll": "Scorri", "searchInputLabel": "termini di ricerca", "searchNextResult": "Prossimo risultato", "searchNoResults": "Nessun risultato", "searchPreviousResult": "Risultato precedente", + "searchResultsRemaining": "{{numLeft}} rimanenti", "searchSubmitAria": "Cerca", "searchTitle": "Cerca", "selectWorkspaceMenu": "Selezione il tipo di workspace", - "showingNumAnnotations": "Sto mostrando {{number}} annotazioni", + "showCollection": "Visualizza la collezione", "showZoomControls": "Mostra i controlli di zoom", + "showingNumAnnotations": "Sto mostrando {{number}} annotazioni", + "showingNumAnnotations_plural": "Visualizzando {{number}} annotazioni", "sidebarPanelsNavigation": "Navigazione dei pannelli della barra laterale", "single": "Singolo", "startHere": "Inizia qui", "suggestSearch": "Cerca in questo documento: \"{{ query }}\"", + "tableOfContentsList": "Sommario", "theme": "Tema", "thumbnailList": "Lista thumbnail", "thumbnailNavigation": "Thumbnails", "thumbnails": "Thumbnails", "toggleWindowSideBar": "Apri/Chiudi la barra", + "totalCollections": "{{count}} collezione", + "totalCollections_plural": "{{count}} collezioni", + "totalManifests": "{{count}} manifest", + "totalManifests_plural": "{{count}} manifests", "tryAgain": "Riprova", "untitled": "[senza titolo]", "view": "Visualizza", + "viewWorkspaceConfiguration": "Visualizza la configurazione del workspace", "welcome": "Benvenuto in Mirador", "window": "Finestra: {{label}}", "windowMenu": "Visualizzazioni finestra e display thumbnail", @@ -124,6 +153,7 @@ "workspace": "Workspace", "workspaceFullScreen": "Schermo intero", "workspaceMenu": "Configurazioni Workspace", + "workspaceNavigation": "Navigazione del workspace", "workspaceOptions": "Opzioni Workspace", "workspaceSelectionTitle": "Seleziona il tipo di workspace", "zoomIn": "Zoom in", diff --git a/src/locales/ja/translation.json b/src/locales/ja/translation.json index 4f23e62ea8b1366619b82255602d10173ebc6d07..866d13b35107d1f1a85c68877696bbf636f83358 100644 --- a/src/locales/ja/translation.json +++ b/src/locales/ja/translation.json @@ -1,5 +1,6 @@ { "translation": { + "aboutMirador": "Project Miradorについて", "aboutThisItem": "この資料について", "addedFromUrl": "(URLで追加)", "addManifestUrl": "資料のURL", @@ -11,70 +12,95 @@ "annotations": "アノテーション", "attribution": "帰属", "attributionTitle": "権利", + "authenticationFailed": "認証失敗", + "authenticationRequired": "フルアクセスには認証が必要", + "backToResults": "結果に戻る", "book": "見開き", "bottom": "下部", "cancel": "キャンセル", "canvasIndex": "インデックス", "changeTheme": "テーマの変更", + "clearSearch": "クリア", + "close": "閉じる", "closeAddResourceForm": "フォームを閉じる", "closeAddResourceMenu": "資料一覧を閉じる", "closeCompanionWindow": "パネルを閉じる", "closeWindow": "ウインドウを閉じる", - "collapseSection": " {{section}} セクションを畳む", + "collapseSection": "{{section}} セクションを畳む", "collapseSidePanel": "サイドバーを畳む", - "itemList": "一覧を最小化", + "collection": "コレクション", + "itemList": "アイテム一覧", + "continue": "続ける", "copy": "コピー", "currentItem": "現在のアイテム", "currentItem_1/1": "現在のアイテム", "currentItem_1/2": "左", "currentItem_2/2": "右", "dark": "ダークなテーマ", + "digitizedView": "デジタルビュー", "dismiss": "片付け", "highlightAllAnnotations": "すべてを表示", + "displayNoAnnotations": "アノテーションを非表示", "downloadExport": "ワークスペースをエクスポート", "downloadExportWorkspace": "ワークスペースをエクスポート", "elastic": "伸縮", "elasticDescription": "自由なウインドウの伸縮", "emptyResourceList": "資料の一覧が空です", + "error": "エラー", "errorDialogConfirm": "OK", "errorDialogTitle": "エラー発生", "exitFullScreen": "全画面を解除", "expandSection": "セクション {{section}}を拡大", "expandSidePanel": "サイドバーを拡大", + "exportCopied": "ワークスペースの設定がクリップボードにコピーされました。", "fetchManifest": "追加", "fullScreen": "全画面", "gallery": "ギャラリー", "hideZoomControls": "zoom制御を隠す", - "iiif_homepage": "この資料について", + "iiif_homepage": "IIIFホームページ", "iiif_manifest": "IIIF マニフェスト", "iiif_renderings": "別の形式", "iiif_seeAlso": "参照", - "import" : "取り込み", + "import": "取り込み", "importWorkspace": "ワークスペースの取り込み", "importWorkspaceHint": "Mirador3の設定を貼り付け", - "アイテム": "アイテム: {{label}}", + "item": "アイテム: {{label}}", + "jsError": "技術的な詳細", + "jsStack": "{{ stack }}", "language": "言語", + "layer_hide": "レイヤーを隠す", + "layer_move": "レイヤーを動かす", + "layer_opacity": "レイヤーの透過度", + "layer_show": "レイヤーを表示", + "layer_moveToTop": "レイヤーをトップへ", + "layers": "レイヤー", "light": "明るいテーマ", "links": "リンク", "listAllOpenWindows": "ウインドウにジャンプ", "login": "ログイン", + "logout": "ログアウト", "manifestError": "資料追加に失敗:", "maximizeWindow": "ウインドウを最大化", "minimizeWindow": "ウインドウを最小化", "mirador": "Mirador", "miradorResources": "Mirador資料", "miradorViewer": "Miradorビューワ", + "more": "さらに...", + "moreResults": "さらに結果を", "mosaic": "モザイク", "mosaicDescription": "モザイク表示", "moveCompanionWindowToBottom": "下部に移動", "moveCompanionWindowToRight": "右に移動", "nextCanvas": "次のアイテム", + "noItemSelected": "アイテムが未選択", "numItems": "{{number}} アイテム", "off": "オフ", "openCompanionWindow_annotations": "アノテーション", "openCompanionWindow_attribution": "権利", "openCompanionWindow_canvas": "インデックス", "openCompanionWindow_info": "情報", + "openCompanionWindow_layers": "レイヤー", + "openCompanionWindow_search": "検索", "openInCompanionWindow": "別のパネルで開く", "openWindows": "現在開いているウインドウ", "pagination": "{{current}} of {{total}}", @@ -83,28 +109,43 @@ "previousCanvas": "前のアイテム", "related": "関連", "resource": "資料", + "retry": "リトライ", "right": "右側", "rights": "利用条件", + "scroll": "スクロール", + "searchInputLabel": "検索語", + "searchNextResult": "次の結果", + "searchNoResults": "ヒットせず", + "searchPreviousResult": "前の結果", + "searchSubmitAria": "検索", + "searchTitle": "検索", "selectWorkspaceMenu": "ワークスペースタイプの選択", "showingNumAnnotations": "アノテーション {{number}} を表示", + "showCollection": "コレクションを表示", "showZoomControls": "ズーム操作を表示", "sidebarPanelsNavigation": "サイドバーパネルの操作", "single": "単一", "startHere": "ここから始める", + "suggestSearch": "この文書を \"{{ query }}\" で検索", + "tableOfContentsList": "目次", "theme": "テーマ", "thumbnailList": "サムネイル一覧", "thumbnailNavigation": "サムネイル", "thumbnails": "サムネイル表示", "toggleWindowSideBar": "サイドバー切り替え", + "totalCollections": "{{count}} コレクション", + "totalManifests": "{{count}} マニフェスト", "tryAgain": "もう一度試す", "untitled": "[タイトル無し]", "view": "表示の仕方", "welcome": "Miradorにようこそ", "window": "{{label}} ウインドウ", - "windowMenu": "ウインドウオプション", + "windowMenu": "ウインドウメニュー", "windowNavigation": "ウィンドウ操作", "windowPluginButtons": "オプション", + "windowPluginMenu": "ウインドウオプション", "workspace": "ワークスペース", + "workspaceNavigation": "ワークスペースナビ", "workspaceFullScreen": "全画面", "workspaceMenu": "ワークスペースの設定", "workspaceOptions": "ワークスペースのオプション", diff --git a/src/locales/kr/translation.json b/src/locales/kr/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..ce2b9328262957ac3a25257161d0d0d169f2330e --- /dev/null +++ b/src/locales/kr/translation.json @@ -0,0 +1,163 @@ +{ + "translation": { + "aboutMirador": "Mirador 프로젝트에 대하여", + "aboutThisItem": "해당 아이템에 대하여", + "addedFromUrl": "(URL에서 추가됨)", + "addManifestUrl": "리소스의 위치", + "addManifestUrlHelp": "IIIF 리소스의 URL", + "addResource": "리소스 추가", + "annotationCanvasLabel_1/1": "아이템: [{{label}}]", + "annotationCanvasLabel_1/2": "왼쪽: [{{label}}]", + "annotationCanvasLabel_2/2": "오른쪽: [{{label}}]", + "annotations": "주석", + "attribution": "귀속", + "attributionTitle": "권리", + "authenticationFailed": "인증 실패.", + "authenticationRequired": "모든 정보에 접근하기 위해선 인증이 필요합니다.", + "backToResults": "결과로 돌아가기", + "book": "책", + "bottom": "아래", + "cancel": "취소", + "canvasIndex": "인덱스", + "changeTheme": "테마 변경", + "clearSearch": "지우기", + "close": "닫기", + "closeAddResourceForm": "폼 닫기", + "closeAddResourceMenu": "리소스 목록 닫기", + "closeCompanionWindow": "패널 닫기", + "closeWindow": "윈도우 닫기", + "collapseSection": "\"{{section}}\" 섹션 접기", + "collapseSidePanel": "사이드바 접기", + "collection": "컬렉션", + "itemList": "아이템 목록", + "continue": "계속하기", + "copy": "복사", + "currentItem": "현재 아이템", + "currentItem_1/1": "현재 아이템", + "currentItem_1/2": "왼쪽", + "currentItem_2/2": "오른쪽", + "dark": "다크 모드", + "digitizedView": "디지털 뷰", + "dismiss": "무시하기", + "highlightAllAnnotations": "모두 하이라이트하기", + "displayNoAnnotations": "하이라이트 해제", + "downloadExport": "작업공간 내보내기", + "downloadExportWorkspace": "작업공간 내보내기", + "elastic": "신축성", + "elasticDescription": "무한한 작업공간에서 윈도우를 자유롭게 움직이고 조절해보세요. 윈도우는 서로 겹칠 수 있습니다.", + "emptyResourceList": "리소스 목록이 비어있습니다", + "error": "오류", + "errorDialogConfirm": "허락", + "errorDialogTitle": "오류 발생", + "exitFullScreen": "전체화면에서 나가기", + "expandSection": "\"{{section}}\" 섹션 확장", + "expandSidePanel": "사이드바 확장", + "exportCopied": "작업공간 환경설정을 클립보드에 복사했습니다", + "fetchManifest": "추가하기", + "fullScreen": "전체화면", + "gallery": "갤러리", + "hideZoomControls": "확대/축소 기능 숨기기", + "iiif_homepage": "해당 리소스에 대하여", + "iiif_manifest": "IIIF 매니페스트", + "iiif_renderings": "대체 포맷", + "iiif_seeAlso": "참고", + "import" : "가져오기", + "importWorkspace": "작업공간 가져오기", + "importWorkspaceHint": "Mirador 3 환경설정 가져와 붙여넣기", + "item": "아이템: {{label}}", + "jsError": "기술적인 세부사항", + "jsStack": "{{ stack }}", + "language": "언어", + "layer_hide": "레이어 숨기기", + "layer_move": "레이어 움직이기", + "layer_opacity": "레이어 불투명도", + "layer_show": "레이어 보이기", + "layer_moveToTop": "레이어를 위로 옮기기", + "layers": "레이어", + "light": "라이트 모드", + "links": "링크", + "listAllOpenWindows": "윈도우로 이동하기", + "login": "로그인", + "logout": "로그아웃", + "manifestError": "리소스를 추가할 수 없습니다:", + "maximizeWindow": "윈도우 크기 최대화", + "minimizeWindow": "윈도우 크기 최소화", + "mirador": "Mirador", + "miradorResources": "Mirador 리소스", + "miradorViewer": "Mirador 뷰어", + "more": "더 보기...", + "moreResults": "결과 더 보기", + "mosaic": "모자이크", + "mosaicDescription": "프레임 내에서 윈도우를 이동하고 크기를 조정합니다.", + "moveCompanionWindowToBottom": "아래로 옮기기", + "moveCompanionWindowToRight": "오른쪽으로 옮기기", + "nextCanvas": "다음 아이템", + "noItemSelected": "아이템이 선택되지 않았습니다", + "numItems": "{{number}}개의 아이템", + "numItems_plural": "{{number}}개의 아이템", + "off": "끄기", + "openCompanionWindow_annotations": "주석", + "openCompanionWindow_attribution": "권리", + "openCompanionWindow_canvas": "인덱스", + "openCompanionWindow_info": "정보", + "openCompanionWindow_layers": "레이어", + "openCompanionWindow_search": "검색", + "openInCompanionWindow": "별도의 패널에서 열기", + "openWindows": "현재 열려있는 윈도우", + "pagination": "{{current}} of {{total}}", + "position": "배치", + "previewWindowTitle": "{{title}}", + "previousCanvas": "이전 아이템", + "related": "관련", + "resource": "리소스", + "retry": "재시도", + "right": "오른쪽", + "rights": "라이선스", + "scroll": "스크롤", + "searchInputLabel": "용어 찾기", + "searchNextResult": "다음 결과", + "searchNoResults": "해당 결과 없음", + "searchPreviousResult": "이전 결과", + "searchResultsRemaining": "{{numLeft}}개 남음", + "searchSubmitAria": "검색하기", + "searchTitle": "검색", + "selectWorkspaceMenu": "작업공간 유형 선택", + "showingNumAnnotations": "{{number}}개의 주석 나타내기", + "showingNumAnnotations_plural": "{{number}}개의 주석 나타내기", + "showCollection": "컬렉션 보이기", + "showZoomControls": "확대/축소 기능 보이기", + "sidebarPanelsNavigation": "사이드바 패널 탐색", + "single": "한 개", + "startHere": "시작하기", + "suggestSearch": "이 문서를 \"{{ query }}\"로 검색하기", + "tableOfContentsList": "목차", + "theme": "테마", + "thumbnailList": "썸네일 목록", + "thumbnailNavigation": "썸네일", + "thumbnails": "썸네일", + "toggleWindowSideBar": "사이드바 전환", + "totalCollections": "{{count}}개의 컬렉션", + "totalCollections_plural": "{{count}}개의 컬렉션", + "totalManifests": "{{count}}개의 매니페스트", + "totalManifests_plural": "{{count}}개의 매니페스트", + "tryAgain": "다시 시도하세요", + "untitled": "[타이틀 없음]", + "view": "뷰", + "viewWorkspaceConfiguration": "작업공간 환경설정 ㅂ괴", + "welcome": "Mirador에 오신 것을 환영합니다", + "window": "윈도우: {{label}}", + "windowMenu": "윈도우 뷰 & 썸네일 표시", + "windowNavigation": "윈도우 탐색", + "windowPluginButtons": "옵션", + "windowPluginMenu": "윈도우 옵션", + "workspace": "작업공간", + "workspaceNavigation": "작업공간 탐색", + "workspaceFullScreen": "전체화면", + "workspaceMenu": "작업공간 설정", + "workspaceOptions": "작업공간 옵션", + "workspaceSelectionTitle": "작업공간 유형을 선택하세요", + "zoomIn": "확대", + "zoomOut": "축소", + "zoomReset": "줌 재설정" + } +} diff --git a/src/locales/lt/translation.json b/src/locales/lt/translation.json index 7feafb80f18ccadd293db80b7ad24e6bd2fe2e55..3955a9d5b586e4ee3f7e2ab9cdce3f114b08bc4c 100644 --- a/src/locales/lt/translation.json +++ b/src/locales/lt/translation.json @@ -25,7 +25,7 @@ "closeAddResourceMenu": "Uždaryti šaltinių sąrašą", "closeCompanionWindow": "Uždaryti panelę", "closeWindow": "Uždaryti langą", - "collapseSection": "Suskleisti {{section}} sekciją", + "collapseSection": "Suskleisti \"{{section}}\" sekciją", "collapseSidePanel": "Suskleisti šoninę juostą", "itemList": "Įrašų sąrašas", "continue": "Tęsti", @@ -48,7 +48,7 @@ "errorDialogConfirm": "Gerai", "errorDialogTitle": "Įvyko klaida", "exitFullScreen": "Išjungti pilno ekrano režimą", - "expandSection": "Išplėsti {{section}} sekciją", + "expandSection": "Išplėsti \"{{section}}\" sekciją", "expandSidePanel": "Išplėsti šoninę juostą", "exportCopied": "Darbalaukio nustatymai nukopijuoti", "fetchManifest": "Pridėti", diff --git a/src/locales/nbNo/translation.json b/src/locales/nbNo/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..c3118f0b87e3406b0e3fc8f81d80d188bdb46735 --- /dev/null +++ b/src/locales/nbNo/translation.json @@ -0,0 +1,163 @@ +{ + "translation": { + "aboutMirador": "Om Projekt Mirador", + "aboutThisItem": "Om dette objektet", + "addedFromUrl": "(Lagt til fra URL)", + "addManifestUrl": "Nettadresse til samling eller manifest", + "addManifestUrlHelp": "URL til en IIIF-ressurs", + "addResource": "Legg til ressurs", + "annotationCanvasLabel_1/1": "Objekt: [{{label}}]", + "annotationCanvasLabel_1/2": "Venstre: [{{label}}]", + "annotationCanvasLabel_2/2": "Høyre: [{{label}}]", + "annotations": "Annoteringer", + "attribution": "Tilskrivelse", + "attributionTitle": "Rettigheter", + "authenticationFailed": "Autentiseringen feilet.", + "authenticationRequired": "Autentisering kreves for full tilgang", + "backToResults": "Tilbake til resultat", + "book": "Bok", + "bottom": "Nederst", + "cancel": "Avbryt", + "canvasIndex": "Index", + "changeTheme": "Skift tema", + "clearSearch": "Fjern søket", + "close": "Lukk", + "closeAddResourceForm": "Lukk skjemaet", + "closeAddResourceMenu": "Lukk ressurslisten", + "closeCompanionWindow": "Lukk panelet", + "closeWindow": "Lukk vinduet", + "collapseSection": "Lukk seksjonen \"{{section}}\"", + "collapseSidePanel": "Lukk sidemenyen", + "collection": "Samling", + "itemList": "Objektliste", + "continue": "Fortsett", + "copy": "Kopiere", + "currentItem": "Valgt objekt", + "currentItem_1/1": "Valgt objekt", + "currentItem_1/2": "Venstre", + "currentItem_2/2": "Høyre", + "dark": "Mørkt tema", + "digitizedView": "Digitalisert visning", + "dismiss": "Lukk", + "highlightAllAnnotations": "Markér alle", + "displayNoAnnotations": "Avmarkér alle", + "downloadExport": "Eksportér arbeidsområde", + "downloadExportWorkspace": "Eksportér arbeidsområde", + "elastic": "Elastisk", + "elasticDescription": "Flytt og endre størrelsen på vinduet fritt i et ubegrenset arbeidsområde. Vindu kan overlappe.", + "emptyResourceList": "Din resursliste er tom", + "error": "Error", + "errorDialogConfirm": "OK", + "errorDialogTitle": "Et problem oppstod", + "exitFullScreen": "Forlat fullskjermsvisning", + "expandSection": "Ekspandér seksjonen \"{{section}}\"", + "expandSidePanel": "Ekspandér sidemenyen", + "exportCopied": "Konfiguration av arbeidsområdet ble kopiert til din utklippstavle", + "fetchManifest": "Legg til", + "fullScreen": "Fullskjermsvisning", + "gallery": "Galleri", + "hideZoomControls": "Skjul zoomkontroll", + "iiif_homepage": "Om denne ressursen", + "iiif_manifest": "IIIF manifest", + "iiif_renderings": "Alternativt format", + "iiif_seeAlso": "Se også", + "import": "Importér", + "importWorkspace": "Importér arbeidsområde", + "importWorkspaceHint": "Lim inn en Mirador 3 konfigurasjon for import", + "item": "Objekt: {{label}}", + "jsError": "Tekniske detaljer", + "jsStack": "{{ stack }}", + "language": "Språk", + "layer_hide": "Skjul lag", + "layer_move": "Flytt lag", + "layer_opacity": "Lag-gjennomsiktighet", + "layer_show": "Vis lag", + "layer_moveToTop": "Flytt laget øverst", + "layers": "Lag", + "light": "Lyst tema", + "links": "Lenker", + "listAllOpenWindows": "Gå til vindu", + "login": "Logg inn", + "logout": "Logg ut", + "manifestError": "Ressursen kan ikke legges til:", + "maximizeWindow": "Maksimér vinduet", + "minimizeWindow": "Minimér vinduet", + "mirador": "Mirador", + "miradorResources": "Miradorressurser", + "miradorViewer": "Mirador bildeviser", + "more": "mer...", + "moreResults": "Flere resultat", + "mosaic": "Mosaik", + "mosaicDescription": "Flytt og endre størrelse på vinduet i relasjon til hverandre, innenfor den synlige rammen.", + "moveCompanionWindowToBottom": "Flytt til bunnen", + "moveCompanionWindowToRight": "Flytt til høyre", + "nextCanvas": "Neste objekt", + "noItemSelected": "Ingen valgte objekt", + "numItems": "{{number}} objekt", + "numItems_plural": "{{number}} objekter", + "off": "Av", + "openCompanionWindow_annotations": "Annoteringer", + "openCompanionWindow_attribution": "Rettigheter", + "openCompanionWindow_canvas": "Indeks", + "openCompanionWindow_info": "Informasjon", + "openCompanionWindow_layers": "Lag", + "openCompanionWindow_search": "Søk", + "openInCompanionWindow": "Åpne i eget panel", + "openWindows": "Åpne vindu", + "pagination": "{{current}} av {{total}}", + "position": "Posisjon", + "previewWindowTitle": "{{title}}", + "previousCanvas": "Forrige objekt", + "related": "Relatert", + "resource": "Ressurs", + "retry": "Forsøk igjen", + "right": "Til høyre", + "rights": "Lisens", + "scroll": "Bla", + "searchInputLabel": "Søkeord", + "searchNextResult": "Neste resultat", + "searchNoResults": "Ingen treff", + "searchPreviousResult": "Forrige resultat", + "searchResultsRemaining": "{{numLeft}} igjen", + "searchSubmitAria": "Søk", + "searchTitle": "Søk", + "selectWorkspaceMenu": "Velg arbeidsområde-type", + "showingNumAnnotations": "Vis {{number}} annotasjon", + "showingNumAnnotations_plural": "Vis {{number}} annotasjoner", + "showCollection": "Vis samling", + "showZoomControls": "Vis zoomkontroll", + "sidebarPanelsNavigation": "Sidemeny-panel navigering", + "single": "En og en", + "startHere": "Start her", + "suggestSearch": "Søk etter \"{{ query }}\" i dette dokument", + "tableOfContentsList": "Innholdsfortegnelse", + "theme": "Tema", + "thumbnailList": "Miniatyrliste", + "thumbnailNavigation": "Miniatyrer", + "thumbnails": "Miniatyrer", + "toggleWindowSideBar": "Vis/skjul sidemenyen", + "totalCollections": "{{count}} samling", + "totalCollections_plural": "{{count}} samlinger", + "totalManifests": "{{count}} manifest", + "totalManifests_plural": "{{count}} manifester", + "tryAgain": "Forsøk igjen", + "untitled": "[uten tittel]", + "view": "Visning", + "viewWorkspaceConfiguration": "Vis konfigurasjon av arbeidsområde.", + "welcome": "Velkommen til Mirador", + "window": "Vindu: {{label}}", + "windowMenu": "Vindusvisning & miniatyrvisning", + "windowNavigation": "Vindusnavigasjon", + "windowPluginButtons": "Innstillinger", + "windowPluginMenu": "Vindusinnstillinger", + "workspace": "Arbeidsområde", + "workspaceNavigation": "Navigasjon i arbeidsområdet", + "workspaceFullScreen": "Fullskjermsvisning", + "workspaceMenu": "Innstillinger for arbeidsområdet", + "workspaceOptions": "Flere valg for arbeidsområdet", + "workspaceSelectionTitle": "Velg arbeidsområde-type", + "zoomIn": "Zoom inn", + "zoomOut": "Zoom ut", + "zoomReset": "Tilbakestill zoom" + } +} diff --git a/src/locales/nl/translation.json b/src/locales/nl/translation.json index 63c1f6b2b02634611d35955f32149a1c5c62e12c..80d6e370c3d090b6b92870600d6f2e3406abaa47 100644 --- a/src/locales/nl/translation.json +++ b/src/locales/nl/translation.json @@ -24,7 +24,7 @@ "closeAddResourceMenu": "Sluit lijst met bronnen", "closeCompanionWindow": "Sluit paneel", "closeWindow": "Sluit venster", - "collapseSection": "Klap {{section}} sectie in", + "collapseSection": "Klap \"{{section}}\" sectie in", "collapseSidePanel": "Klap zijbalk in", "itemList": "Compacte lijst", "continue": "Ga verder", @@ -44,7 +44,7 @@ "errorDialogConfirm": "OK", "errorDialogTitle": "Er is een fout opgetreden", "exitFullScreen": "Verlaat volledig scherm", - "expandSection": "Klap {{section}} sectie uit", + "expandSection": "Klap \"{{section}}\" sectie uit", "expandSidePanel": "Klap zijbalk uit", "fetchManifest": "Voeg toe", "fullScreen": "Volledig scherm", @@ -76,7 +76,8 @@ "moveCompanionWindowToBottom": "Verplaats naar beneden", "moveCompanionWindowToRight": "Verplaats naar rechts", "nextCanvas": "Volgend item", - "numItems": "{{number}} items", + "numItems": "{{number}} item", + "numItems_plural": "{{number}} items", "off": "Uit", "openCompanionWindow_annotations": "Annotaties", "openCompanionWindow_attribution": "Rechten", @@ -101,7 +102,8 @@ "searchSubmitAria": "Zoeken", "searchTitle": "Zoek", "selectWorkspaceMenu": "Selecteer workspacetype", - "showingNumAnnotations": "{{number}} annotaties weergegeven", + "showingNumAnnotations": "{{number}} annotatie weergegeven", + "showingNumAnnotations_plural": "{{number}} annotaties weergegeven", "showZoomControls": "Toon zoomknoppen", "sidebarPanelsNavigation": "Zijbalk panelen navigatie", "single": "Enkel", @@ -112,6 +114,10 @@ "thumbnailNavigation": "Thumbnails", "thumbnails": "Thumbnails", "toggleWindowSideBar": "Toon zijbalk", + "totalCollections": "{{count}} collectie", + "totalCollections_plural": "{{count}} collecties", + "totalManifests": "{{count}} manifest", + "totalManifests_plural": "{{count}} manifests", "tryAgain": "Probeer opnieuw", "untitled": "[Zonder titel]", "view": "Weergave", diff --git a/src/locales/pl/translation.json b/src/locales/pl/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..a4d3bf59adc3641afd3ba0ae6b199a9a2f0d9357 --- /dev/null +++ b/src/locales/pl/translation.json @@ -0,0 +1,163 @@ +{ + "translation": { + "aboutMirador": "O Projekcie Mirador", + "aboutThisItem": "O bieżącej pozycji", + "addedFromUrl": "(Dodano z URL)", + "addManifestUrl": "Lokalizacja zasobów", + "addManifestUrlHelp": "URL zasobów IIIF", + "addResource": "Dodaj zasoby", + "annotationCanvasLabel_1/1": "Pozycja: [{{label}}]", + "annotationCanvasLabel_1/2": "Lewo: [{{label}}]", + "annotationCanvasLabel_2/2": "Prawo: [{{label}}]", + "annotations": "Adnotacje", + "attribution": "Atrybucja", + "attributionTitle": "Prawa", + "authenticationFailed": "Uwierzytelnianie nie powiodło się.", + "authenticationRequired": "Do pełnego dostępu wymagane jest uwierzytelnienie", + "backToResults": "Powrót do wyników", + "book": "Książka", + "bottom": "Dół", + "cancel": "Anuluj", + "canvasIndex": "Indeks", + "changeTheme": "Zmień motyw", + "clearSearch": "wyczyść", + "close": "Zamknij", + "closeAddResourceForm": "Zamknij formularz", + "closeAddResourceMenu": "Zamknij listę zasobów", + "closeCompanionWindow": "Zamknij panel", + "closeWindow": "Zamknij okno", + "collapseSection": "Zwiń sekcję \"{{section}}\"", + "collapseSidePanel": "Zwiń panel boczny", + "collection": "Zbiór", + "itemList": "Lista pozycji", + "continue": "Kontynuuj", + "copy": "Kopiuj", + "currentItem": "Bieżąca pozycja", + "currentItem_1/1": "Bieżąca pozycja", + "currentItem_1/2": "Lewo", + "currentItem_2/2": "Prawo", + "dark": "Ciemny motyw", + "digitizedView": "Widok zdigitalizowany", + "dismiss": "Odrzuć", + "highlightAllAnnotations": "Podświetl wszystko", + "displayNoAnnotations": "Nie podświetlaj", + "downloadExport": "Eksportuj obszar roboczy", + "downloadExportWorkspace": "Eksportuj obszar roboczy", + "elastic": "Elastyczny", + "elasticDescription": "Swobodnie przesuwaj i dopasowuj okna w nieograniczonej przestrzeni roboczej. Okna mogą zachodzić na siebie.", + "emptyResourceList": "Twoja lista zasobów jest pusta", + "error": "Błąd", + "errorDialogConfirm": "OK", + "errorDialogTitle": "Wystąpił błąd", + "exitFullScreen": "Wyłącz tryb pełnoekranowy", + "expandSection": "Rozwiń sekcję \"{{section}}\"", + "expandSidePanel": "Rozwiń panel boczny", + "exportCopied": "Konfiguracja obszaru roboczego została skopiowana do schowka", + "fetchManifest": "Dodaj", + "fullScreen": "Pełny ekran", + "gallery": "Galeria", + "hideZoomControls": "Ukryj kontrolki powiększenia", + "iiif_homepage": "O tym zasobie", + "iiif_manifest": "Manifest IIIF", + "iiif_renderings": "Alternatywne formaty", + "iiif_seeAlso": "Zobacz też", + "import" : "Importuj", + "importWorkspace": "Importuj obszar roboczy", + "importWorkspaceHint": "Wklej konfigurację Mirador 3 do zaimportowania", + "item": "Pozycja: {{label}}", + "jsError": "Szczegóły techniczne", + "jsStack": "{{ stack }}", + "language": "Język", + "layer_hide": "Ukryj warstwę", + "layer_move": "Przenieś warstwę", + "layer_opacity": "Krycie warstwy", + "layer_show": "Pokaż warstwę", + "layer_moveToTop": "Przenieś warstwę na górę", + "layers": "Warstwy", + "light": "Jasny motyw", + "links": "Linki", + "listAllOpenWindows": "Przejdź do okna", + "login": "Zaloguj", + "logout": "Wyloguj", + "manifestError": "Następujące zasoby nie mogą być dodane:", + "maximizeWindow": "Maksymalizuj okno", + "minimizeWindow": "Minimalizuj okno", + "mirador": "Mirador", + "miradorResources": "Zasoby Mirador", + "miradorViewer": "Przeglądarka Mirador", + "more": "więcej...", + "moreResults": "Więcej wyników", + "mosaic": "Mozaika", + "mosaicDescription": "Przesuwaj i zmieniaj rozmiary okien względem siebie, w widocznej ramce.", + "moveCompanionWindowToBottom": "Przesuń na dół", + "moveCompanionWindowToRight": "Przesuń w prawo", + "nextCanvas": "Następna pozycja", + "noItemSelected": "Nie wybrano pozycji", + "numItems": "{{number}} pozycja", + "numItems_plural": "{{number}} pozycje", + "off": "Wyłącz", + "openCompanionWindow_annotations": "Adnotacje", + "openCompanionWindow_attribution": "Prawa", + "openCompanionWindow_canvas": "Indeks", + "openCompanionWindow_info": "Informacje", + "openCompanionWindow_layers": "Warstwy", + "openCompanionWindow_search": "Szukaj", + "openInCompanionWindow": "Otwórz w oddzielnym panelu", + "openWindows": "Aktualnie otwarte okna", + "pagination": "{{current}} of {{total}}", + "position": "Pozycja", + "previewWindowTitle": "{{title}}", + "previousCanvas": "Poprzednia pozycja", + "related": "Powiązane", + "resource": "Zasoby", + "retry": "Spróbuj ponownie", + "right": "Prawo", + "rights": "Licencja", + "scroll": "Przewiń", + "searchInputLabel": "szukane słowa", + "searchNextResult": "Następny wynik", + "searchNoResults": "Nie znaleziono wyników", + "searchPreviousResult": "Poprzedni wynik", + "searchResultsRemaining": "Pozostało {{numLeft}}", + "searchSubmitAria": "Wyszukaj", + "searchTitle": "Wyszukaj", + "selectWorkspaceMenu": "Wybierz typ obszaru roboczego", + "showingNumAnnotations": "Wyświetlanie {{number}} adnotacji", + "showingNumAnnotations_plural": "Wyświetlanie {{number}} adnotacji", + "showCollection": "Pokaż zbiór", + "showZoomControls": "Pokaż kontrolki powiększenia", + "sidebarPanelsNavigation": "Nawigacja pasków panelu bocznego", + "single": "Pojedynczy", + "startHere": "Zacznij tutaj", + "suggestSearch": "Przeszukaj ten dokument pod kątem \"{{ query }}\"", + "tableOfContentsList": "Spis treści", + "theme": "Motyw", + "thumbnailList": "Lista miniatur", + "thumbnailNavigation": "Miniatury", + "thumbnails": "Miniatury", + "toggleWindowSideBar": "Przełącz panel boczny", + "totalCollections": "{{count}} zbiór", + "totalCollections_plural": "{{count}} zbiorów", + "totalManifests": "{{count}} manifest", + "totalManifests_plural": "{{count}} manifestów", + "tryAgain": "Spróbuj ponownie", + "untitled": "[Bez nazwy]", + "view": "Widok", + "viewWorkspaceConfiguration": "Wyświetl konfigurację obszaru roboczego", + "welcome": "Witaj w Miradorze", + "window": "Okno: {{label}}", + "windowMenu": "Widoki okien i wyświetlanie miniatur", + "windowNavigation": "Nawigacja okna", + "windowPluginButtons": "Opcje", + "windowPluginMenu": "Opcje okna", + "workspace": "Obszar roboczy", + "workspaceNavigation": "Nawigacja obszaru roboczego", + "workspaceFullScreen": "Pełny ekran", + "workspaceMenu": "Ustawienia obszaru roboczego", + "workspaceOptions": "Opcje obszaru roboczego", + "workspaceSelectionTitle": "Wybierz typ obszaru roboczego", + "zoomIn": "Przybliż", + "zoomOut": "Oddal", + "zoomReset": "Zresetuj powiększenie" + } +} diff --git a/src/locales/ptBr/translation.json b/src/locales/ptBr/translation.json index b258baf177d015bc1250604982bce171d9172224..bbf06666a8019f7beb3822e6a5fdc3cf1c69de55 100644 --- a/src/locales/ptBr/translation.json +++ b/src/locales/ptBr/translation.json @@ -24,7 +24,7 @@ "closeAddResourceMenu": "Fechar lista de conteúdo", "closeCompanionWindow": "Fechar painel", "closeWindow": "Fechar janela", - "collapseSection": "Suprimir seção {{section}}", + "collapseSection": "Suprimir seção \"{{section}}\"", "collapseSidePanel": "Suprimir barra lateral", "itemList": "Lista compacta", "continue": "Continuar", @@ -44,7 +44,7 @@ "errorDialogConfirm": "OK", "errorDialogTitle": "Um erro ocorreu", "exitFullScreen": "Sair do modo tela cheia", - "expandSection": "Expandir seção {{section}}", + "expandSection": "Expandir seção \"{{section}}\"", "expandSidePanel": "Expandir barra lateral", "fetchManifest": "Adicionar", "fullScreen": "Tela cheia", diff --git a/src/locales/sr/translation.json b/src/locales/sr/translation.json index 1e60143ccbe7e8d695dc24e4bf808726282676d3..032e40fb612cacaa8ea774feff6dab9b914fd08f 100644 --- a/src/locales/sr/translation.json +++ b/src/locales/sr/translation.json @@ -26,7 +26,7 @@ "closeAddResourceMenu": "Затворите листу ресурса", "closeCompanionWindow": "Затворите панел", "closeWindow": "Затворите приказ", - "collapseSection": "Сакријте {{section}} секцију", + "collapseSection": "Сакријте \"{{section}}\" секцију", "collapseSidePanel": "Сакријте", "collection": "Колекција", "itemList": "Листа страница", @@ -50,7 +50,7 @@ "errorDialogConfirm": "OK", "errorDialogTitle": "Дошло је до грешке", "exitFullScreen": "Изађите из приказа преко целог екрана", - "expandSection": "Проширите {{section}} секцију", + "expandSection": "Проширите \"{{section}}\" секцију", "expandSidePanel": "Прикажите", "exportCopied": "Конфигурација радног окружења је копирана у привремену меморију", "fetchManifest": "Додајте", diff --git a/src/locales/sv/translation.json b/src/locales/sv/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..866d0c5e5f2250ab9efa0a9264a9f4ba66e6829c --- /dev/null +++ b/src/locales/sv/translation.json @@ -0,0 +1,158 @@ +{ + "translation": { + "aboutMirador": "Om Projekt Mirador", + "aboutThisItem": "Om det här objektet", + "addedFromUrl": "(Tillagd från URL)", + "addManifestUrl": "Webbadress till samling eller manifest", + "addManifestUrlHelp": "URL till en IIIF-resurs", + "addResource": "Lägg till resurs", + "annotationCanvasLabel_1/1": "Objekt: [{{label}}]", + "annotationCanvasLabel_1/2": "Vänster: [{{label}}]", + "annotationCanvasLabel_2/2": "Höger: [{{label}}]", + "annotations": "Noteringar", + "attribution": "Tillskrivning", + "attributionTitle": "Rättigheter", + "authenticationFailed": "Autentisering misslyckades.", + "authenticationRequired": "Autentisering krävs för full åtkomst", + "backToResults": "Tillbaka till resultat", + "book": "Bok", + "bottom": "Nederkant", + "cancel": "Avbryt", + "canvasIndex": "Index", + "changeTheme": "Ändra tema", + "clearSearch": "Ta bort sökning", + "close": "Stäng", + "closeAddResourceForm": "Stäng formulär", + "closeAddResourceMenu": "Stäng resurslista", + "closeCompanionWindow": "Stäng panel", + "closeWindow": "Stäng fönster", + "collapseSection": "Stäng sektionen \"{{section}}\"", + "collapseSidePanel": "Stäng sidofält", + "collection": "Samling", + "itemList": "Objektlista", + "continue": "Fortsätt", + "copy": "Kopiera", + "currentItem": "Aktuellt objekt", + "currentItem_1/1": "Aktuellt objekt", + "currentItem_1/2": "Vänster", + "currentItem_2/2": "Höger", + "dark": "Mörkt tema", + "digitizedView": "Digitaliserad vy", + "dismiss": "Stäng", + "highlightAllAnnotations": "Markera alla", + "displayNoAnnotations": "Avmarkera alla", + "downloadExport": "Exportera arbetsyta", + "downloadExportWorkspace": "Exportera arbetsyta", + "elastic": "Elastisk", + "elasticDescription": "Flytta och ändra storlek på fönster fritt i en obegränsad arbetsyta. Fönster kan överlappa.", + "emptyResourceList": "Din resurslista är tom", + "error": "Error", + "errorDialogConfirm": "OK", + "errorDialogTitle": "Ett problem uppstod", + "exitFullScreen": "Lämna helskärmsläge", + "expandSection": "Expandera sektionen \"{{section}}\"", + "expandSidePanel": "Expandera sidofält", + "exportCopied": "Konfiguration av arbetsytan har kopierats till dina urklipp", + "fetchManifest": "Lägg till", + "fullScreen": "Helskärmsläge", + "gallery": "Galleri", + "hideZoomControls": "Dölj zoomkontroller", + "iiif_homepage": "Om den här resursen", + "iiif_manifest": "IIIF manifest", + "iiif_renderings": "Alternativa format", + "iiif_seeAlso": "Se även", + "import" : "Importera", + "importWorkspace": "Importera arbetsyta", + "importWorkspaceHint": "Klistra in en Mirador 3 konfiguration att importera", + "item": "Objekt: {{label}}", + "jsError": "Tekniska detaljer", + "jsStack": "{{ stack }}", + "language": "Språk", + "layer_hide": "Dölj lager", + "layer_move": "Flytta lager", + "layer_opacity": "Lageropacitet", + "layer_show": "Visa lager", + "layer_moveToTop": "Flytta lager till toppen", + "layers": "Lager", + "light": "Ljust tema", + "links": "Länkar", + "listAllOpenWindows": "Gå till fönster", + "login": "Logga in", + "logout": "Logga ut", + "manifestError": "Resursen kan inte läggas till:", + "maximizeWindow": "Maximera fönster", + "minimizeWindow": "Minimera fönster", + "mirador": "Mirador", + "miradorResources": "Miradorresurser", + "miradorViewer": "Mirador bildvisare", + "more": "mer...", + "moreResults": "Fler resultat", + "mosaic": "Mosaik", + "mosaicDescription": "Flytta och ändra storlek på fönster i relation till varandra, innanför den synliga ramen.", + "moveCompanionWindowToBottom": "Flytta till botten", + "moveCompanionWindowToRight": "Flytta till höger", + "nextCanvas": "Nästa objekt", + "noItemSelected": "Inga valda objekt", + "numItems": "{{number}} objekt", + "off": "Av", + "openCompanionWindow_annotations": "Noteringar", + "openCompanionWindow_attribution": "Rättigheter", + "openCompanionWindow_canvas": "Index", + "openCompanionWindow_info": "Information", + "openCompanionWindow_layers": "Lager", + "openCompanionWindow_search": "Sök", + "openInCompanionWindow": "Öppna i separat panel", + "openWindows": "Öppna fönster", + "pagination": "{{current}} av {{total}}", + "position": "Position", + "previewWindowTitle": "{{title}}", + "previousCanvas": "Föregående objekt", + "related": "Relaterat", + "resource": "Resurs", + "retry": "Försök igen", + "right": "Till höger", + "rights": "Licens", + "scroll": "Scrolla", + "searchInputLabel": "Sökord", + "searchNextResult": "Nästa resultat", + "searchNoResults": "Inga resultat hittades", + "searchPreviousResult": "Föregående resultat", + "searchResultsRemaining": "{{numLeft}} kvar", + "searchSubmitAria": "Sök", + "searchTitle": "Sök", + "selectWorkspaceMenu": "Välj typ av arbetsyta", + "showingNumAnnotations": "Visar {{number}} noteringar", + "showCollection": "Visa samling", + "showZoomControls": "Visa zoomkontroller", + "sidebarPanelsNavigation": "Sidofältspaneler navigering", + "single": "En och en", + "startHere": "Börja här", + "suggestSearch": "Sök i detta dokument efter \"{{ query }}\"", + "tableOfContentsList": "Innehållsförteckning", + "theme": "Tema", + "thumbnailList": "Miniatyrlista", + "thumbnailNavigation": "Miniatyrer", + "thumbnails": "Miniatyrer", + "toggleWindowSideBar": "Visa/dölj sidofält", + "totalCollections": "{{count}} samlingar", + "totalManifests": "{{count}} manifest", + "tryAgain": "Försök igen", + "untitled": "[namnlös]", + "view": "Vy", + "welcome": "Välkommen till Mirador", + "window": "Fönster: {{label}}", + "windowMenu": "Fönstervyer & miniatyrdisplay", + "windowNavigation": "Fönsternavigation", + "windowPluginButtons": "Inställningar", + "windowPluginMenu": "Fönsterinställningar", + "workspace": "Arbetsyta", + "workspaceNavigation": "Navigera i arbetsyta", + "workspaceFullScreen": "Helskärmsläge", + "workspaceMenu": "Inställningar för arbetsyta", + "workspaceOptions": "Fler val för arbetsyta", + "workspaceSelectionTitle": "Välj typ av arbetsyta", + "zoomIn": "Zooma in", + "zoomOut": "Zooma ut", + "zoomReset": "Återställ zoom" + } +} diff --git a/src/locales/vi/translation.json b/src/locales/vi/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..9429e5ece7cda375d6e68ad6c92950634c042be5 --- /dev/null +++ b/src/locales/vi/translation.json @@ -0,0 +1,157 @@ +{ + "translation": { + "aboutMirador": "Về dự án Mirador", + "aboutThisItem": "Về khoản mục này", + "addedFromUrl": "(Được thêm từ URL)", + "addManifestUrl": "Vị trí tài nguyên", + "addManifestUrlHelp": "URL của tài nguyên IIIF", + "addResource": "Bổ sung tài nguyên", + "annotationCanvasLabel_1/1": "Khoản mục: [{{label}}]", + "annotationCanvasLabel_1/2": "Trái: [{{label}}]", + "annotationCanvasLabel_2/2": "Phải: [{{label}}]", + "annotations": "Chú giải", + "attribution": "Quyền hạn", + "attributionTitle": "Quyền", + "authenticationFailed": "Xác thực thất bại.", + "authenticationRequired": "Xác thực được yêu cầu cho truy nhập đầy đủ", + "backToResults": "Trở lại kết quả", + "book": "Sách", + "bottom": "Đáy", + "cancel": "Huỷ bỏ", + "canvasIndex": "Chỉ mục", + "changeTheme": "Đổi chủ đề", + "clearSearch": "xoá", + "close": "Đóng", + "closeAddResourceForm": "Đóng mẫu", + "closeAddResourceMenu": "Đóng danh sách tài nguyên", + "closeCompanionWindow": "Đóng panel", + "closeWindow": "Đóng cửa sổ", + "collapseSection": "Co sập {{section}} mục", + "collapseSidePanel": "Co sập thanh bên", + "collection": "Tuyển tập", + "itemList": "Danh sách khoản mục", + "continue": "Tiếp tục", + "copy": "Sao", + "currentItem": "Khoản mục hiện thời", + "currentItem_1/1": "Khoản mục hiện thời", + "currentItem_1/2": "Trái", + "currentItem_2/2": "Phải", + "dark": "Chủ đề tối", + "digitizedView": "Cái nhìn số hoá", + "dismiss": "Bác bỏ", + "highlightAllAnnotations": "Làm nổi bật tất", + "displayNoAnnotations": "Không làm nổi bật", + "downloadExport": "Vùng xuất khẩu", + "downloadExportWorkspace": "Vùng xuất khẩu", + "elastic": "Co giãn", + "elasticDescription": "Di chuyển và định cỡ cửa sổ tự do trong vùng vô giới hạn. Cửa sổ có thể chèn lấp.", + "emptyResourceList": "Danh sách tài nguyên của bạn là trống", + "error": "Lỗi", + "errorDialogConfirm": "OK", + "errorDialogTitle": "Lỗi đã xuất hiện", + "exitFullScreen": "Ra khỏi toàn màn hình", + "expandSection": "Mở rộng {{section}} mục", + "expandSidePanel": "Mở rộng thanh bên", + "exportCopied": "Cấu hình vùng làm việc được sao vào bảng đệm của bạn", + "fetchManifest": "Thêm", + "fullScreen": "Toàn màn hình", + "gallery": "Phòng tranh", + "hideZoomControls": "Ẩn điều khiển thu phóng", + "iiif_homepage": "Về tài nguyên này", + "iiif_manifest": "Bản kê IIIF", + "iiif_renderings": "Dạng thức luân phiên", + "iiif_seeAlso": "Cũng xem", + "import" : "Nhập khẩu", + "importWorkspace": "Vùng nhập khẩu", + "importWorkspaceHint": "Dán cấu hình Mirador 3 để được nhập khẩu", + "item": "Khoản mục: {{label}}", + "jsError": "Chi tiết kĩ thuật", + "jsStack": "{{ stack }}", + "language": "Ngôn ngữ", + "layer_hide": "Giấu tầng", + "layer_move": "Chuyển tầng", + "layer_opacity": "Làm mờ tầng", + "layer_show": "Hiện tầng", + "layer_moveToTop": "Chuyển tầng lên đỉnh", + "layers": "Tầng", + "light": "Chủ đề sáng", + "links": "Móc nối", + "listAllOpenWindows": "Nhảy tới cửa sổ", + "login": "Đăng nhập", + "logout": "Đăng xuất", + "manifestError": "Tài nguyên không thể được bổ sung:", + "maximizeWindow": "Cực đại cửa sổ", + "minimizeWindow": "Cực tiểu cửa sổ", + "mirador": "Mirador", + "miradorResources": "Tài nguyên Mirador", + "miradorViewer": "Bộ xem Mirador", + "more": "thêm...", + "moreResults": "Thêm kết quả", + "mosaic": "Mosaic", + "mosaicDescription": "Di chuyển và định cỡ cửa sổ trong quan hệ lẫn nhau, bên trong khung thấy được.", + "moveCompanionWindowToBottom": "Chuyển tới đáy", + "moveCompanionWindowToRight": "Chuyển sang phải", + "nextCanvas": "Khoản mục tiếp", + "noItemSelected": "Không khoản mục nào được chọn", + "numItems": "{{number}} khoản mục", + "off": "Off", + "openCompanionWindow_annotations": "Chú giải", + "openCompanionWindow_attribution": "Quyền", + "openCompanionWindow_canvas": "Chỉ mục", + "openCompanionWindow_info": "Thông tin", + "openCompanionWindow_layers": "Tầng", + "openCompanionWindow_search": "Tìm", + "openInCompanionWindow": "Mở trong ngăn tách rời", + "openWindows": "Cửa sổ mở hiện thời", + "pagination": "{{current}} trong {{total}}", + "position": "Vị trí", + "previewWindowTitle": "{{title}}", + "previousCanvas": "Khoản mục trước", + "related": "Có liên quan", + "resource": "Tài nguyên", + "retry": "Thử lại", + "right": "Quyền", + "rights": "Cấp phép", + "scroll": "Cuộn", + "searchInputLabel": "Từ tìm kiếm", + "searchNextResult": "Kết quả tiếp", + "searchNoResults": "Không tìm được kết quả nào", + "searchPreviousResult": "Kết quả trước", + "searchSubmitAria": "Đệ trình việc tìm", + "searchTitle": "Tìm", + "selectWorkspaceMenu": "Chọn kiểu vùng làm việc", + "showingNumAnnotations": "Hiện {{number}} chú giải", + "showCollection": "Hiện bộ sưu tập", + "showZoomControls": "Hiện kiểm soát thu phóng", + "sidebarPanelsNavigation": "Dẫn lái ngăn thanh bên", + "single": "Chỉ một", + "startHere": "Bắt đầu ở đây", + "suggestSearch": "Tìm tài liệu này cho \"{{ query }}\"", + "tableOfContentsList": "Mục lục", + "theme": "Chủ đề", + "thumbnailList": "Danh sách ảnh thu nhỏ", + "thumbnailNavigation": "Ảnh thu nhỏ", + "thumbnails": "Ảnh thu nhỏ", + "toggleWindowSideBar": "chốt thanh bên", + "totalCollections": "{{count}} bộ sưu tập", + "totalManifests": "{{count}} bản kê", + "tryAgain": "Thử lại", + "untitled": "[Untitled]", + "view": "Xem", + "welcome": "Chào mừng bạn tới Mirador", + "window": "Cửa sổ: {{label}}", + "windowMenu": "Xem cửa sổ & hiển thị ảnh thu nhỏ", + "windowNavigation": "Dẫn lái cửa sổ", + "windowPluginButtons": "Tuỳ chọn", + "windowPluginMenu": "Tuỳ chọn cửa sổ", + "workspace": "Vùng làm việc", + "workspaceNavigation": "Dẫn lái vùng làm việc", + "workspaceFullScreen": "Toàn màn hình", + "workspaceMenu": "Thiết đặt vùng làm việc", + "workspaceOptions": "Tuỳ chọn vùng làm việc", + "workspaceSelectionTitle": "Chọn kiểu vùng làm việc", + "zoomIn": "Thu nhỏ", + "zoomOut": "Phóng to", + "zoomReset": "Đặt lại thu phóng" + } +} \ No newline at end of file diff --git a/src/locales/zhCn/translation.json b/src/locales/zhCn/translation.json index b64f7e837a984084cca9023214a95d3c74d15a45..15542807ba647e0cb8c8a4dffb66208486f128b8 100644 --- a/src/locales/zhCn/translation.json +++ b/src/locales/zhCn/translation.json @@ -1,116 +1,164 @@ { "translation": { - "aboutThisItem": "有关此物件", + "aboutMirador": "关于Mirador项目", + "aboutThisItem": "有关此条目", "addedFromUrl": "(从URL添加)", "addManifestUrl": "来源", "addManifestUrlHelp": "IIIF资源的URL", "addResource": "添加资源", - "annotationCanvasLabel_1/1": "物件: [{{label}}]", + "annotationCanvasLabel_1/1": "条目: [{{label}}]", "annotationCanvasLabel_1/2": "左方: [{{label}}]", "annotationCanvasLabel_2/2": "右方: [{{label}}]", - "annotations": "注释", - "attribution": "着作权", - "attributionTitle": "着作权", + "annotations": "标注", + "attribution": "著作权", + "attributionTitle": "著作权", + "authenticationFailed": "认证失败。", + "authenticationRequired": "完全访问需要认证", + "backToResults": "返回到结果", "book": "书籍", "bottom": "下方", "cancel": "取消", "canvasIndex": "索引", - "changeTheme": "变更佈景主题", + "changeTheme": "变更背景主题", + "clearSearch": "清除", + "close": "关闭", "closeAddResourceForm": "关闭表格", "closeAddResourceMenu": "关闭资源列表", - "closeCompanionWindow": "关闭附属视窗", - "closeWindow": "关闭视窗", + "closeCompanionWindow": "关闭附属窗口", + "closeWindow": "关闭窗口", "collapseSection": "关闭{{section}}分页", "collapseSidePanel": "关闭边栏", + "collection": "集合", "itemList": "标题列表", - "copy": "複製", - "currentItem": "目前物件", - "currentItem_1/1": "目前物件", + "continue": "继续", + "copy": "复制", + "currentItem": "当前条目", + "currentItem_1/1": "当前条目", "currentItem_1/2": "左方", "currentItem_2/2": "右方", - "dark": "黑色主题", + "dark": "暗色主题", + "digitizedView": "数字视图", "dismiss": "关闭信息", - "highlightAllAnnotations": "显示所有注释", - "downloadExport": "滙出桌面排版", - "downloadExportWorkspace": "滙出桌面排版", + "highlightAllAnnotations": "高亮所有标注", + "displayNoAnnotations": "不高亮", + "downloadExport": "导出桌面排版", + "downloadExportWorkspace": "导出桌面排版", "elastic": "弹性", - "elasticDescription": "在桌面上自由摆放视窗", - "emptyResourceList": "资源列表没有物件", + "elasticDescription": "在桌面上自由摆放窗口", + "emptyResourceList": "空资源列表", + "error": "错误", "errorDialogConfirm": "确定", "errorDialogTitle": "发生错误", - "exitFullScreen": "退出全萤幕", + "exitFullScreen": "退出全屏", "expandSection": "开启{{section}}分页", "expandSidePanel": "开启边栏", + "exportCopied": "工作区配置被复制到你的剪贴板上了", "fetchManifest": "添加", - "fullScreen": "全萤幕", - "gallery": "矩列", + "fullScreen": "全屏", + "gallery": "画廊", "hideZoomControls": "隐藏缩放选项", - "iiif_homepage": "有关此资源", - "iiif_manifest": "IIIF", + "iiif_homepage": "主页", + "iiif_manifest": "IIIF清单", "iiif_renderings": "其他格式", "iiif_seeAlso": "另见", - "import" : "滙入", - "importWorkspace": "滙入桌面排版", + "import" : "导入", + "importWorkspace": "导入桌面排版", "importWorkspaceHint": "在此贴上Mirador 3排版设定码", - "item": "物件: {{label}}", + "item": "条目: {{label}}", + "jsError": "技术细节", + "jsStack": "{{ stack }}", "language": "语言", - "light": "白色主题", - "links": "连结", - "listAllOpenWindows": "切换至视窗", + "layer_hide": "隐藏图层", + "layer_move": "移动图层", + "layer_opacity": "图层不透明度", + "layer_show": "显示图层", + "layer_moveToTop": "将图层移到顶部", + "layers": "图层", + "light": "亮色主题", + "links": "链接", + "listAllOpenWindows": "切换至窗口", "login": "登入", + "logout": "登出", "manifestError": "无法增添资源:", - "maximizeWindow": "视窗最大化", - "minimizeWindow": "视窗最小化", + "maximizeWindow": "窗口最大化", + "minimizeWindow": "窗口最小化", "mirador": "Mirador", "miradorResources": "Mirador资源", "miradorViewer": "Mirador阅览器", + "more": "更多...", + "moreResults": "更多结果", "mosaic": "马赛克", - "mosaicDescription": "在桌面上以格状方式排列视窗", + "mosaicDescription": "在桌面上以格状方式排列窗口", "moveCompanionWindowToBottom": "移至下方", "moveCompanionWindowToRight": "移至右方", + "multipartCollection": "多卷集合", "nextCanvas": "下一页", - "numItems": "{{number}} 项物件", + "noItemSelected": "没有条目被选中", + "numItems": "{{number}} 项条目", + "numItems_plural": "{{number}} 项条目", "off": "关闭", - "openCompanionWindow_annotations": "注释", - "openCompanionWindow_attribution": "着作权", + "openCompanionWindow_annotations": "标注", + "openCompanionWindow_attribution": "著作权", "openCompanionWindow_canvas": "目录", "openCompanionWindow_info": "资讯", - "openInCompanionWindow": "移至新附属视窗", - "openWindows": "现有视窗", + "openCompanionWindow_layers": "图层", + "openCompanionWindow_search": "搜索", + "openInCompanionWindow": "移至新附属窗口", + "openWindows": "当前窗口", "pagination": "{{current}} / {{total}}", "position": "位置", "previewWindowTitle": "{{title}}", "previousCanvas": "上一页", - "related": "相关资讯", + "related": "相关信息", "resource": "资源", + "retry": "重试", "right": "右方", "rights": "版权", + "scroll": "滚动", + "searchInputLabel": "搜索关键字", + "searchNextResult": "下一个结果", + "searchNoResults": "没有搜索到结果", + "searchPreviousResult": "前一个结果", + "searchResultsRemaining": "{{numLeft}}剩余", + "searchSubmitAria": "提交搜索", + "searchTitle": "搜索", "selectWorkspaceMenu": "选择桌面排版方式", - "showingNumAnnotations": "显示 {{number}} 项注释", + "showingNumAnnotations": "显示 {{number}} 项标注", + "showingNumAnnotations_plural": "显示 {{number}} 项标注", + "showCollection": "显示集合", "showZoomControls": "显示缩放选项", "sidebarPanelsNavigation": "切换边栏", "single": "单项", "startHere": "按此开始", - "theme": "佈景主题", - "thumbnailList": "缩图列表", - "thumbnailNavigation": "缩图", - "thumbnails": "显示缩图", + "suggestSearch": "搜索本文档以\"{{ query }}\"", + "tableOfContentsList": "目录", + "theme": "背景主题", + "thumbnailList": "缩略图列表", + "thumbnailNavigation": "缩略图", + "thumbnails": "缩略图", "toggleWindowSideBar": "切换边栏开关", + "totalCollections": "{{count}} 集合", + "totalCollections_plural": "{{count}} 集合", + "totalManifests": "{{count}} 清单", + "totalManifests_plural": "{{count}} 清单", "tryAgain": "请重试", "untitled": "[无标题]", - "view": "物件排列方式", + "view": "条目排列方式", + "viewWorkspaceConfiguration": "查看工作区配置", "welcome": "欢迎使用Mirador", - "window": "视窗: {{label}}", - "windowMenu": "视窗选项", - "windowNavigation": "切换视窗", + "window": "窗口: {{label}}", + "windowMenu": "窗口视图 & 缩略图显示", + "windowNavigation": "切换窗口", "windowPluginButtons": "选项", + "windowPluginMenu": "窗口选项", "workspace": "桌面", - "workspaceFullScreen": "全萤幕", + "workspaceNavigation": "工作区导航", + "workspaceFullScreen": "全屏", "workspaceMenu": "桌面设定", "workspaceOptions": "桌面选项", "workspaceSelectionTitle": "选择桌面排版方式", "zoomIn": "放大", - "zoomOut": "放小", + "zoomOut": "缩小", "zoomReset": "重设缩放" } } diff --git a/src/locales/zhTw/translation.json b/src/locales/zhTw/translation.json index 0c864dd7b4374174eda25cd20ed121681941a8e7..08eff9ccc6a0da8bd81fff829bc5c09f92665553 100644 --- a/src/locales/zhTw/translation.json +++ b/src/locales/zhTw/translation.json @@ -1,5 +1,6 @@ { "translation": { + "aboutMirador": "關於Mirador項目", "aboutThisItem": "有關此物件", "addedFromUrl": "(從URL添加)", "addManifestUrl": "來源", @@ -8,73 +9,100 @@ "annotationCanvasLabel_1/1": "物件: [{{label}}]", "annotationCanvasLabel_1/2": "左方: [{{label}}]", "annotationCanvasLabel_2/2": "右方: [{{label}}]", - "annotations": "注釋", + "annotations": "標註", "attribution": "著作權", "attributionTitle": "著作權", + "authenticationFailed": "認證失敗。", + "authenticationRequired": "完全訪問需要認證", + "backToResults": "返回到結果", "book": "書籍", "bottom": "下方", "cancel": "取消", "canvasIndex": "索引", "changeTheme": "變更佈景主題", + "clearSearch": "清除", + "close": "關閉", "closeAddResourceForm": "關閉表格", "closeAddResourceMenu": "關閉資源列表", "closeCompanionWindow": "關閉附屬視窗", "closeWindow": "關閉視窗", "collapseSection": "關閉{{section}}分頁", "collapseSidePanel": "關閉邊欄", + "collection": "集合", "itemList": "標題列表", + "continue": "繼續", "copy": "複製", "currentItem": "目前物件", "currentItem_1/1": "目前物件", "currentItem_1/2": "左方", "currentItem_2/2": "右方", - "dark": "黑色主題", + "dark": "暗色主題", + "digitizedView": "數字視圖", "dismiss": "關閉信息", - "highlightAllAnnotations": "顯示所有注釋", + "highlightAllAnnotations": "高亮所有標註", + "displayNoAnnotations": "不高亮", "downloadExport": "滙出桌面排版", "downloadExportWorkspace": "滙出桌面排版", "elastic": "彈性", "elasticDescription": "在桌面上自由擺放視窗", "emptyResourceList": "資源列表沒有物件", + "error": "錯誤", "errorDialogConfirm": "確定", "errorDialogTitle": "發生錯誤", "exitFullScreen": "退出全螢幕", "expandSection": "開啟{{section}}分頁", + "exportCopied": "工作區配置被複製到你的剪貼板上了", "expandSidePanel": "開啟邊欄", "fetchManifest": "添加", "fullScreen": "全螢幕", "gallery": "矩列", "hideZoomControls": "隱藏縮放選項", "iiif_homepage": "有關此資源", - "iiif_manifest": "IIIF", + "iiif_manifest": "IIIF清單", "iiif_renderings": "其他格式", "iiif_seeAlso": "另見", "import" : "滙入", "importWorkspace": "滙入桌面排版", "importWorkspaceHint": "在此貼上Mirador 3排版設定碼", "item": "物件: {{label}}", + "jsError": "技術細節", + "jsStack": "{{ stack }}", "language": "語言", - "light": "白色主題", + "layer_hide": "隱藏圖層", + "layer_move": "移動圖層", + "layer_opacity": "圖層不透明度", + "layer_show": "顯示圖層", + "layer_moveToTop": "將圖層移到頂部", + "layers": "圖層", + "light": "亮色主題", "links": "連結", "listAllOpenWindows": "切換至視窗", "login": "登入", + "logout": "登出", "manifestError": "無法增添資源:", "maximizeWindow": "視窗最大化", "minimizeWindow": "視窗最小化", "mirador": "Mirador", "miradorResources": "Mirador資源", "miradorViewer": "Mirador閱覽器", + "more": "更多...", + "moreResults": "更多結果", "mosaic": "馬賽克", "mosaicDescription": "在桌面上以格狀方式排列視窗", "moveCompanionWindowToBottom": "移至下方", "moveCompanionWindowToRight": "移至右方", + "multipartCollection": "多卷集合", "nextCanvas": "下一頁", + "noItemSelected": "沒有物件被選中", "numItems": "{{number}} 項物件", + "numItems_plural": "{{number}} 項物件", "off": "關閉", - "openCompanionWindow_annotations": "注釋", + "openCompanionWindow_annotations": "標註", "openCompanionWindow_attribution": "著作權", "openCompanionWindow_canvas": "目錄", "openCompanionWindow_info": "資訊", + "openCompanionWindow_layers": "圖層", + "openCompanionWindow_search": "搜索", "openInCompanionWindow": "移至新附屬視窗", "openWindows": "現有視窗", "pagination": "{{current}} / {{total}}", @@ -83,34 +111,54 @@ "previousCanvas": "上一頁", "related": "相關資訊", "resource": "資源", + "retry": "重試", "right": "右方", "rights": "版權", + "scroll": "滾動", + "searchInputLabel": "搜索關鍵字", + "searchNextResult": "下一個結果", + "searchNoResults": "沒有搜索到結果", + "searchPreviousResult": "前一個結果", + "searchResultsRemaining": "{{numLeft}}剩餘", + "searchSubmitAria": "提交搜索", + "searchTitle": "搜索", "selectWorkspaceMenu": "選擇桌面排版方式", - "showingNumAnnotations": "顯示 {{number}} 項注釋", + "showingNumAnnotations": "顯示 {{number}} 項標註", + "showingNumAnnotations_plural": "顯示 {{number}} 項標註", + "showCollection": "顯示集合", "showZoomControls": "顯示縮放選項", "sidebarPanelsNavigation": "切換邊欄", "single": "單項", "startHere": "按此開始", + "suggestSearch": "搜索本文檔以\"{{ query }}\"", + "tableOfContentsList": "目錄", "theme": "佈景主題", - "thumbnailList": "縮圖列表", - "thumbnailNavigation": "縮圖", - "thumbnails": "顯示縮圖", + "thumbnailList": "縮略圖列表", + "thumbnailNavigation": "縮略圖", + "thumbnails": "縮略圖", "toggleWindowSideBar": "切換邊欄開關", + "totalCollections": "{{count}} 集合", + "totalCollections_plural": "{{count}} 集合", + "totalManifests": "{{count}} 清單", + "totalManifests_plural": "{{count}} 清單", "tryAgain": "請重試", "untitled": "[無標題]", "view": "物件排列方式", + "viewWorkspaceConfiguration": "查看工作區配置", "welcome": "歡迎使用Mirador", "window": "視窗: {{label}}", - "windowMenu": "視窗選項", + "windowMenu": "視窗視圖 & 縮略圖顯示", "windowNavigation": "切換視窗", - "windowPluginButtons": "插件", + "windowPluginButtons": "選項", + "windowPluginMenu": "視窗選項", "workspace": "桌面", + "workspaceNavigation": "工作區導航", "workspaceFullScreen": "全螢幕", "workspaceMenu": "桌面設定", "workspaceOptions": "桌面選項", "workspaceSelectionTitle": "選擇桌面排版方式", "zoomIn": "放大", - "zoomOut": "放小", + "zoomOut": "縮小", "zoomReset": "重設縮放" } } diff --git a/src/state/actions/window.js b/src/state/actions/window.js index 52d9b4fdb2c10158084a47815d603ff49a1dee5b..c0e63ee91b58fa4c4b7bdfe2f60bbce7bd3c12a8 100644 --- a/src/state/actions/window.js +++ b/src/state/actions/window.js @@ -44,10 +44,14 @@ export function addWindow({ companionWindows, manifest, ...options }) { ), ]; - if (config.window.defaultSideBarPanel || config.window.sideBarPanel) { + if (options.sideBarPanel || config.window.defaultSideBarPanel || config.window.sideBarPanel) { defaultCompanionWindows.unshift( { - content: config.window.defaultSideBarPanel || config.window.sideBarPanel, + content: options.sideBarPanel + || (options.defaultSearchQuery && 'search') + || config.window.defaultSideBarPanel + || config.window.sideBarPanel, + default: true, id: `cw-${uuid()}`, position: 'left', @@ -70,15 +74,16 @@ export function addWindow({ companionWindows, manifest, ...options }) { rotation: null, selectedAnnotations: {}, sideBarOpen: config.window.sideBarOpenByDefault !== undefined - ? config.window.sideBarOpenByDefault - : config.window.sideBarOpen, - sideBarPanel: config.window.defaultSideBarPanel || config.window.sideBarPanel, + ? config.window.sideBarOpenByDefault || !!options.defaultSearchQuery + : config.window.sideBarOpen || !!options.defaultSearchQuery, + sideBarPanel: options.sideBarPanel + || config.window.defaultSideBarPanel + || config.window.sideBarPanel, thumbnailNavigationId: cwThumbs, }; const elasticLayout = { - height: 400, - width: 400, + ...(config.window.elastic || { height: 400, width: 480 }), x: 200 + (Math.floor(numWindows / 10) * 50 + (numWindows * 30) % 300), y: 200 + ((numWindows * 50) % 300), }; diff --git a/src/state/createPluggableStore.js b/src/state/createPluggableStore.js new file mode 100644 index 0000000000000000000000000000000000000000..3e3975fe1d169338e08f7947863e75b7e5210b5a --- /dev/null +++ b/src/state/createPluggableStore.js @@ -0,0 +1,31 @@ +import deepmerge from 'deepmerge'; +import createStore from './createStore'; +import { importConfig } from './actions/config'; +import { + filterValidPlugins, + getConfigFromPlugins, + getReducersFromPlugins, + getSagasFromPlugins, +} from '../extend/pluginPreprocessing'; + +/** + * Configure Store + */ +function createPluggableStore(config, plugins = []) { + const filteredPlugins = filterValidPlugins(plugins); + + const store = createStore( + getReducersFromPlugins(filteredPlugins), + getSagasFromPlugins(filteredPlugins), + ); + + store.dispatch( + importConfig( + deepmerge(getConfigFromPlugins(filteredPlugins), config), + ), + ); + + return store; +} + +export default createPluggableStore; diff --git a/src/state/createStore.js b/src/state/createStore.js index 4797c59ebfe99ea942b02576f4e16bee8db49222..717b0b8915cc9dd00df4b0652a56a4d291b80c65 100644 --- a/src/state/createStore.js +++ b/src/state/createStore.js @@ -14,7 +14,7 @@ import settings from '../config/settings'; /** * Configure Store */ -export default function (pluginReducers, pluginSagas = []) { +function configureStore(pluginReducers, pluginSagas = []) { const miradorReducer = createRootReducer(pluginReducers); const rootReducer = settings.state.slice @@ -38,3 +38,5 @@ export default function (pluginReducers, pluginSagas = []) { return store; } + +export default configureStore; diff --git a/src/state/index.js b/src/state/index.js index b55ed16a04db290df08cf8067f7d9d47d7cb9036..6c5d5191b914e9c460be4240be8ccdce350b4439 100644 --- a/src/state/index.js +++ b/src/state/index.js @@ -4,12 +4,10 @@ import * as sagas from './sagas'; import * as selectors from './selectors'; import createStore from './createStore'; -const exports = { +export default { actions, createStore, reducers, sagas, selectors, }; - -export default exports; diff --git a/src/state/sagas/auth.js b/src/state/sagas/auth.js index 0ecb94976c362690ffd0360826eb934ce635e7b8..732a482368c8521b09b1970125870d4973580a1d 100644 --- a/src/state/sagas/auth.js +++ b/src/state/sagas/auth.js @@ -1,7 +1,7 @@ import { all, call, put, select, takeEvery, delay, } from 'redux-saga/effects'; -import { Utils } from 'manifesto.js/dist-esmodule/Utils'; +import { Utils } from 'manifesto.js'; import flatten from 'lodash/flatten'; import ActionTypes from '../actions/action-types'; import MiradorCanvas from '../../lib/MiradorCanvas'; diff --git a/src/state/sagas/iiif.js b/src/state/sagas/iiif.js index f0ce4c5371f91f62095e952251dd786e998afec3..41823156bc7b8ac6b423ec9dafe4e8efdb019e53 100644 --- a/src/state/sagas/iiif.js +++ b/src/state/sagas/iiif.js @@ -2,7 +2,7 @@ import { all, call, put, select, takeEvery, } from 'redux-saga/effects'; import fetch from 'isomorphic-unfetch'; -import { Utils } from 'manifesto.js/dist-esmodule/Utils'; +import { Utils } from 'manifesto.js'; import normalizeUrl from 'normalize-url'; import ActionTypes from '../actions/action-types'; import { diff --git a/src/state/sagas/windows.js b/src/state/sagas/windows.js index 8ad878a35124110d53c32ac10cb1e03fe1b87ff3..bc63cdcbb9a072fbdb5f4dd4dd23890d92974823 100644 --- a/src/state/sagas/windows.js +++ b/src/state/sagas/windows.js @@ -27,6 +27,7 @@ import { getElasticLayout, getCanvases, selectInfoResponses, + getWindowConfig, } from '../selectors'; import { fetchManifests } from './iiif'; @@ -202,6 +203,10 @@ export function* updateVisibleCanvases({ windowId }) { /** @private */ export function* setCanvasOfFirstSearchResult({ companionWindowId, windowId }) { + const { switchCanvasOnSearch } = yield select(getWindowConfig, { windowId }); + if (!switchCanvasOnSearch) { + return; + } const selectedIds = yield select(getSelectedContentSearchAnnotationIds, { companionWindowId, windowId, }); diff --git a/src/state/selectors/auth.js b/src/state/selectors/auth.js index 07ce1253686872bdf505c5babda8b7c849098d62..7c290bfca47b82095b693d23e3d518f55db2803b 100644 --- a/src/state/selectors/auth.js +++ b/src/state/selectors/auth.js @@ -1,5 +1,5 @@ import { createSelector } from 'reselect'; -import { Utils } from 'manifesto.js/dist-esmodule/Utils'; +import { Utils } from 'manifesto.js'; import flatten from 'lodash/flatten'; import MiradorCanvas from '../../lib/MiradorCanvas'; import { miradorSlice } from './utils'; diff --git a/src/state/selectors/manifests.js b/src/state/selectors/manifests.js index 63a726680aa8929779c2888c5cc1635b424bdbb5..44abe0435266fe581b624657d5207af2e5076a33 100644 --- a/src/state/selectors/manifests.js +++ b/src/state/selectors/manifests.js @@ -1,8 +1,8 @@ import { createSelector } from 'reselect'; import createCachedSelector from 're-reselect'; -import { PropertyValue } from 'manifesto.js/dist-esmodule/PropertyValue'; -import { Utils } from 'manifesto.js/dist-esmodule/Utils'; +import { PropertyValue, Utils } from 'manifesto.js'; import getThumbnail from '../../lib/ThumbnailFactory'; +import asArray from '../../lib/asArray'; import { getCompanionWindow } from './companionWindows'; import { getManifest } from './getters'; import { getConfig } from './config'; @@ -109,16 +109,6 @@ export const getManifestProvider = createSelector( && PropertyValue.parse(provider[0].label, locale).getValue(), ); -/** - */ -function asArray(value) { - if (!Array.isArray(value)) { - return [value]; - } - - return value; -} - /** * Return the IIIF v3 homepage of a manifest or null * @param {object} state @@ -234,10 +224,13 @@ export const getRights = createSelector( */ export function getManifestThumbnail(state, props) { const manifest = getManifestoInstance(state, props); + const { thumbnails = {} } = getConfig(state); if (!manifest) return undefined; - const thumbnail = getThumbnail(manifest, { maxHeight: 80, maxWidth: 120 }); + const thumbnail = getThumbnail(manifest, { + maxHeight: 80, maxWidth: 120, preferredFormats: thumbnails.preferredFormats, + }); return thumbnail && thumbnail.url; } diff --git a/src/state/selectors/ranges.js b/src/state/selectors/ranges.js index 0a40388874a82176fc62b62320bf212d66510679..a2e312955850fd488421b479a8b6d4f4cecda7ec 100644 --- a/src/state/selectors/ranges.js +++ b/src/state/selectors/ranges.js @@ -1,7 +1,7 @@ import { createSelector } from 'reselect'; import union from 'lodash/union'; import without from 'lodash/without'; -import { Utils } from 'manifesto.js/dist-esmodule/Utils'; +import { Utils } from 'manifesto.js'; import { getVisibleCanvasIds } from './canvases'; import { getCompanionWindow } from './companionWindows'; import { getSequenceTreeStructure } from './sequences'; diff --git a/src/state/selectors/searches.js b/src/state/selectors/searches.js index f679b1b59cf08a12610193dddede306ff1e9ccc0..3e541084e325e3ca41cd962cf119dc91cf15af3a 100644 --- a/src/state/selectors/searches.js +++ b/src/state/selectors/searches.js @@ -1,5 +1,5 @@ import { createSelector } from 'reselect'; -import { PropertyValue } from 'manifesto.js/dist-esmodule/PropertyValue'; +import { PropertyValue } from 'manifesto.js'; import flatten from 'lodash/flatten'; import AnnotationList from '../../lib/AnnotationList'; import { getCanvas, getCanvases } from './canvases'; @@ -57,6 +57,22 @@ export const getSearchIsFetching = createSelector( results => results.some(result => result.isFetching), ); +export const getSearchNumTotal = createSelector( + [ + getSearchForCompanionWindow, + ], + (results) => { + if (!results || !results.data) return undefined; + + const resultWithWithin = Object.values(results.data).find(result => ( + !result.isFetching + && result.json + && result.json.within + )); + return resultWithWithin?.json?.within?.total; + }, +); + export const getNextSearchId = createSelector( [ getSearchForCompanionWindow, diff --git a/src/state/selectors/sequences.js b/src/state/selectors/sequences.js index 6e90dc08b87958722518462470f34626e2321d38..c8014a70c0d3860dba8aaa703c9c13e880937bc8 100644 --- a/src/state/selectors/sequences.js +++ b/src/state/selectors/sequences.js @@ -1,5 +1,5 @@ import { createSelector } from 'reselect'; -import { TreeNode } from 'manifesto.js/dist-esmodule/TreeNode'; +import { TreeNode } from 'manifesto.js'; import { getManifestoInstance, } from './manifests'; diff --git a/webpack.config.js b/webpack.config.js index 292c2e3b62e4cf3933bd0a59d2bee331e9c3b649..3f78b6cad3e2a3339c9801046e1b21ef5bb89b0a 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,8 +1,8 @@ const path = require('path'); +const fs = require('fs'); const webpack = require('webpack'); const TerserPlugin = require('terser-webpack-plugin'); const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); -const paths = require('./config/paths'); /** */ const baseConfig = mode => ({ @@ -10,7 +10,7 @@ const baseConfig = mode => ({ module: { rules: [ { - include: paths.appPath, // CRL + include: path.resolve(fs.realpathSync(process.cwd()), '.'), // CRL loader: require.resolve('babel-loader'), options: { // Save disk space when time isn't as important