mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge pull request #1079 from mspalti/iiif-mirador
Angular support for IIIF and Mirador
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
"serve": "ts-node --project ./tsconfig.ts-node.json scripts/serve.ts",
|
||||
"start:dev": "npm-run-all --parallel config:dev:watch serve",
|
||||
"start:prod": "yarn run build:prod && yarn run serve:ssr",
|
||||
"start:mirador:prod": "yarn run build:mirador && yarn run start:prod",
|
||||
"analyze": "webpack-bundle-analyzer dist/browser/stats.json",
|
||||
"build": "ng build",
|
||||
"build:stats": "ng build --stats-json",
|
||||
@@ -44,6 +45,7 @@
|
||||
"clean": "yarn run clean:prod && yarn run clean:env && yarn run clean:node",
|
||||
"clean:env": "rimraf src/environments/environment.ts",
|
||||
"sync-i18n": "yarn run config:dev && ts-node --project ./tsconfig.ts-node.json scripts/sync-i18n-files.ts",
|
||||
"build:mirador": "webpack --config webpack/webpack.mirador.config.ts",
|
||||
"merge-i18n": "yarn run config:dev && ts-node --project ./tsconfig.ts-node.json scripts/merge-i18n-files.ts",
|
||||
"postinstall": "ngcc",
|
||||
"cypress:open": "cypress open",
|
||||
@@ -106,6 +108,9 @@
|
||||
"jsonschema": "1.4.0",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"klaro": "^0.7.10",
|
||||
"mirador": "^3.0.0",
|
||||
"mirador-dl-plugin": "^0.13.0",
|
||||
"mirador-share-plugin": "^0.10.0",
|
||||
"moment": "^2.29.1",
|
||||
"morgan": "^1.10.0",
|
||||
"ng-mocks": "10.5.4",
|
||||
@@ -118,6 +123,8 @@
|
||||
"nouislider": "^14.6.3",
|
||||
"pem": "1.14.4",
|
||||
"postcss-cli": "^8.3.0",
|
||||
"react": "^16.14.0",
|
||||
"react-dom": "^16.14.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "^6.6.3",
|
||||
"rxjs-spy": "^7.5.3",
|
||||
@@ -157,6 +164,7 @@
|
||||
"deep-freeze": "0.0.1",
|
||||
"dotenv": "^8.2.0",
|
||||
"fork-ts-checker-webpack-plugin": "^6.0.3",
|
||||
"html-loader": "^1.3.2",
|
||||
"html-webpack-plugin": "^4.5.0",
|
||||
"http-proxy-middleware": "^1.0.5",
|
||||
"jasmine-core": "^3.6.0",
|
||||
|
@@ -41,6 +41,8 @@ import { UIServerConfig } from './src/config/ui-server-config.interface';
|
||||
* Set path for the browser application's dist folder
|
||||
*/
|
||||
const DIST_FOLDER = join(process.cwd(), 'dist/browser');
|
||||
// Set path fir IIIF viewer.
|
||||
const IIIF_VIEWER = join(process.cwd(), 'dist/iiif');
|
||||
|
||||
const indexHtml = existsSync(join(DIST_FOLDER, 'index.html')) ? 'index.html' : 'index';
|
||||
|
||||
@@ -135,6 +137,10 @@ export function app() {
|
||||
* Serve static resources (images, i18n messages, …)
|
||||
*/
|
||||
server.get('*.*', cacheControl, express.static(DIST_FOLDER, { index: false }));
|
||||
/*
|
||||
* Fallthrough to the IIIF viewer (must be included in the build).
|
||||
*/
|
||||
server.use('/iiif', express.static(IIIF_VIEWER, {index:false}));
|
||||
|
||||
// Register the ngApp callback function to handle incoming requests
|
||||
server.get('*', ngApp);
|
||||
|
@@ -28,6 +28,7 @@ import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-dat
|
||||
import { TruncatableService } from '../../../../shared/truncatable/truncatable.service';
|
||||
import { TruncatePipe } from '../../../../shared/utils/truncate.pipe';
|
||||
import { JournalComponent } from './journal.component';
|
||||
import { RouteService } from '../../../../core/services/route.service';
|
||||
|
||||
let comp: JournalComponent;
|
||||
let fixture: ComponentFixture<JournalComponent>;
|
||||
@@ -86,6 +87,7 @@ describe('JournalComponent', () => {
|
||||
{ provide: NotificationsService, useValue: {} },
|
||||
{ provide: DefaultChangeAnalyzer, useValue: {} },
|
||||
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
|
||||
{ provide: RouteService, useValue: {} }
|
||||
],
|
||||
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
|
@@ -12,7 +12,6 @@ import { ItemPageAbstractFieldComponent } from './simple/field-components/specif
|
||||
import { ItemPageUriFieldComponent } from './simple/field-components/specific-field/uri/item-page-uri-field.component';
|
||||
import { ItemPageTitleFieldComponent } from './simple/field-components/specific-field/title/item-page-title-field.component';
|
||||
import { ItemPageFieldComponent } from './simple/field-components/specific-field/item-page-field.component';
|
||||
import { FileSectionComponent } from './simple/field-components/file-section/file-section.component';
|
||||
import { CollectionsComponent } from './field-components/collections/collections.component';
|
||||
import { FullItemPageComponent } from './full/full-item-page.component';
|
||||
import { FullFileSectionComponent } from './full/field-components/file-section/full-file-section.component';
|
||||
@@ -31,10 +30,12 @@ import { MediaViewerComponent } from './media-viewer/media-viewer.component';
|
||||
import { MediaViewerVideoComponent } from './media-viewer/media-viewer-video/media-viewer-video.component';
|
||||
import { MediaViewerImageComponent } from './media-viewer/media-viewer-image/media-viewer-image.component';
|
||||
import { NgxGalleryModule } from '@kolkov/ngx-gallery';
|
||||
import { MiradorViewerComponent } from './mirador-viewer/mirador-viewer.component';
|
||||
import { VersionPageComponent } from './version-page/version-page/version-page.component';
|
||||
import { VersionedItemComponent } from './simple/item-types/versioned-item/versioned-item.component';
|
||||
import { ThemedFileSectionComponent} from './simple/field-components/file-section/themed-file-section.component';
|
||||
|
||||
|
||||
const ENTRY_COMPONENTS = [
|
||||
// put only entry components that use custom decorator
|
||||
PublicationComponent,
|
||||
@@ -54,7 +55,6 @@ const DECLARATIONS = [
|
||||
ItemPageUriFieldComponent,
|
||||
ItemPageTitleFieldComponent,
|
||||
ItemPageFieldComponent,
|
||||
FileSectionComponent,
|
||||
CollectionsComponent,
|
||||
FullFileSectionComponent,
|
||||
PublicationComponent,
|
||||
@@ -65,6 +65,7 @@ const DECLARATIONS = [
|
||||
MediaViewerComponent,
|
||||
MediaViewerVideoComponent,
|
||||
MediaViewerImageComponent,
|
||||
MiradorViewerComponent,
|
||||
VersionPageComponent,
|
||||
];
|
||||
|
||||
|
@@ -0,0 +1,4 @@
|
||||
<p class="full-text-op">{{'iiifviewer.fullscreen.notice' | translate}}</p>
|
||||
<p *ngIf="!isViewerAvailable" id="viewer-message">{{viewerMessage}}</p>
|
||||
<iframe title="Mirador Viewer" allowtransparency="true" *ngIf="isViewerAvailable" [src]="iframeViewerUrl | async" id="mirador-viewer"></iframe>
|
||||
|
@@ -0,0 +1,13 @@
|
||||
#mirador-viewer {
|
||||
border: 1px solid #dee2e6;
|
||||
height: 660px;
|
||||
width: 100%
|
||||
}
|
||||
.full-text-op {
|
||||
text-align: right;
|
||||
color: #333333;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
p.full-text-op {
|
||||
margin-bottom: 0;
|
||||
}
|
@@ -0,0 +1,255 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { MiradorViewerComponent } from './mirador-viewer.component';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
|
||||
import { BitstreamDataService } from '../../core/data/bitstream-data.service';
|
||||
import { createRelationshipsObservable } from '../simple/item-types/shared/item.component.spec';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { MetadataMap } from '../../core/shared/metadata.models';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { MiradorViewerService } from './mirador-viewer.service';
|
||||
import { HostWindowService } from '../../shared/host-window.service';
|
||||
|
||||
|
||||
function getItem(metadata: MetadataMap) {
|
||||
return Object.assign(new Item(), {
|
||||
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||
metadata: metadata,
|
||||
relationships: createRelationshipsObservable()
|
||||
});
|
||||
}
|
||||
|
||||
const noMetadata = new MetadataMap();
|
||||
|
||||
const mockHostWindowService = {
|
||||
// This isn't really testing mobile status, the return observable just allows the test to run.
|
||||
widthCategory: observableOf(true),
|
||||
};
|
||||
|
||||
describe('MiradorViewerComponent with search', () => {
|
||||
let comp: MiradorViewerComponent;
|
||||
let fixture: ComponentFixture<MiradorViewerComponent>;
|
||||
const viewerService = jasmine.createSpyObj('MiradorViewerService', ['showEmbeddedViewer', 'getImageCount']);
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
viewerService.showEmbeddedViewer.and.returnValue(true);
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
})],
|
||||
declarations: [MiradorViewerComponent],
|
||||
providers: [
|
||||
{ provide: BitstreamDataService, useValue: {} },
|
||||
{ provide: HostWindowService, useValue: mockHostWindowService }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(MiradorViewerComponent, {
|
||||
set: {
|
||||
providers: [
|
||||
{ provide: MiradorViewerService, useValue: viewerService }
|
||||
]
|
||||
}
|
||||
}).compileComponents();
|
||||
}));
|
||||
describe('searchable item', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
fixture = TestBed.createComponent(MiradorViewerComponent);
|
||||
comp = fixture.componentInstance;
|
||||
comp.object = getItem(noMetadata);
|
||||
comp.searchable = true;
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should set multi property to true', (() => {
|
||||
expect(comp.multi).toBe(true);
|
||||
}));
|
||||
|
||||
it('should set url "multi" param to true', (() => {
|
||||
const value = fixture.debugElement
|
||||
.nativeElement.querySelector('#mirador-viewer').src;
|
||||
expect(value).toContain('multi=true');
|
||||
}));
|
||||
|
||||
it('should set url "searchable" param to true', (() => {
|
||||
const value = fixture.debugElement
|
||||
.nativeElement.querySelector('#mirador-viewer').src;
|
||||
expect(value).toContain('searchable=true');
|
||||
}));
|
||||
|
||||
it('should not call mirador service image count', () => {
|
||||
expect(viewerService.getImageCount).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('MiradorViewerComponent with multiple images', () => {
|
||||
|
||||
let comp: MiradorViewerComponent;
|
||||
let fixture: ComponentFixture<MiradorViewerComponent>;
|
||||
const viewerService = jasmine.createSpyObj('MiradorViewerService', ['showEmbeddedViewer', 'getImageCount']);
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
viewerService.showEmbeddedViewer.and.returnValue(true);
|
||||
viewerService.getImageCount.and.returnValue(observableOf(2));
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
})],
|
||||
declarations: [MiradorViewerComponent],
|
||||
providers: [
|
||||
{ provide: BitstreamDataService, useValue: {} },
|
||||
{ provide: HostWindowService, useValue: mockHostWindowService }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(MiradorViewerComponent, {
|
||||
set: {
|
||||
providers: [
|
||||
{ provide: MiradorViewerService, useValue: viewerService }
|
||||
]
|
||||
}
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
describe('non-searchable item with multiple images', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
fixture = TestBed.createComponent(MiradorViewerComponent);
|
||||
comp = fixture.componentInstance;
|
||||
comp.object = getItem(noMetadata);
|
||||
comp.searchable = false;
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should set url "multi" param to true', (() => {
|
||||
const value = fixture.debugElement
|
||||
.nativeElement.querySelector('#mirador-viewer').src;
|
||||
expect(value).toContain('multi=true');
|
||||
}));
|
||||
|
||||
it('should call mirador service image count', () => {
|
||||
expect(viewerService.getImageCount).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should omit "searchable" param from url', (() => {
|
||||
const value = fixture.debugElement
|
||||
.nativeElement.querySelector('#mirador-viewer').src;
|
||||
expect(value).not.toContain('searchable=true');
|
||||
}));
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('MiradorViewerComponent with a single image', () => {
|
||||
let comp: MiradorViewerComponent;
|
||||
let fixture: ComponentFixture<MiradorViewerComponent>;
|
||||
const viewerService = jasmine.createSpyObj('MiradorViewerService', ['showEmbeddedViewer', 'getImageCount']);
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
viewerService.showEmbeddedViewer.and.returnValue(true);
|
||||
viewerService.getImageCount.and.returnValue(observableOf(1));
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
})],
|
||||
declarations: [MiradorViewerComponent],
|
||||
providers: [
|
||||
{ provide: BitstreamDataService, useValue: {} },
|
||||
{ provide: HostWindowService, useValue: mockHostWindowService }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(MiradorViewerComponent, {
|
||||
set: {
|
||||
providers: [
|
||||
{ provide: MiradorViewerService, useValue: viewerService }
|
||||
]
|
||||
}
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
describe('single image viewer', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
fixture = TestBed.createComponent(MiradorViewerComponent);
|
||||
comp = fixture.componentInstance;
|
||||
comp.object = getItem(noMetadata);
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should omit "multi" param', (() => {
|
||||
const value = fixture.debugElement
|
||||
.nativeElement.querySelector('#mirador-viewer').src;
|
||||
expect(value).not.toContain('multi=false');
|
||||
}));
|
||||
|
||||
it('should call mirador service image count', () => {
|
||||
expect(viewerService.getImageCount).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('MiradorViewerComponent in development mode', () => {
|
||||
let comp: MiradorViewerComponent;
|
||||
let fixture: ComponentFixture<MiradorViewerComponent>;
|
||||
const viewerService = jasmine.createSpyObj('MiradorViewerService', ['showEmbeddedViewer', 'getImageCount']);
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
viewerService.showEmbeddedViewer.and.returnValue(false);
|
||||
viewerService.getImageCount.and.returnValue(observableOf(1));
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
})],
|
||||
declarations: [MiradorViewerComponent],
|
||||
providers: [
|
||||
{ provide: BitstreamDataService, useValue: {} }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(MiradorViewerComponent, {
|
||||
set: {
|
||||
providers: [
|
||||
{ provide: MiradorViewerService, useValue: viewerService },
|
||||
{ provide: HostWindowService, useValue: mockHostWindowService }
|
||||
]
|
||||
}
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
describe('embedded viewer', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
fixture = TestBed.createComponent(MiradorViewerComponent);
|
||||
comp = fixture.componentInstance;
|
||||
comp.object = getItem(noMetadata);
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should not embed the viewer', (() => {
|
||||
const value = fixture.debugElement
|
||||
.nativeElement.querySelector('#mirador-viewer');
|
||||
expect(value).toBeNull();
|
||||
}));
|
||||
|
||||
it('should show message', (() => {
|
||||
const value = fixture.debugElement
|
||||
.nativeElement.querySelector('#viewer-message');
|
||||
expect(value).toBeDefined();
|
||||
}));
|
||||
|
||||
});
|
||||
});
|
136
src/app/item-page/mirador-viewer/mirador-viewer.component.ts
Normal file
136
src/app/item-page/mirador-viewer/mirador-viewer.component.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input, OnInit, PLATFORM_ID } from '@angular/core';
|
||||
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { BitstreamDataService } from '../../core/data/bitstream-data.service';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { map, take } from 'rxjs/operators';
|
||||
import { of } from 'rxjs';
|
||||
import { isPlatformBrowser } from '@angular/common';
|
||||
import { MiradorViewerService } from './mirador-viewer.service';
|
||||
import { HostWindowService, WidthCategory } from '../../shared/host-window.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-mirador-viewer',
|
||||
styleUrls: ['./mirador-viewer.component.scss'],
|
||||
templateUrl: './mirador-viewer.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
providers: [ MiradorViewerService ]
|
||||
})
|
||||
export class MiradorViewerComponent implements OnInit {
|
||||
|
||||
@Input() object: Item;
|
||||
|
||||
/**
|
||||
* A previous dspace search query.
|
||||
*/
|
||||
@Input() query: string;
|
||||
|
||||
/**
|
||||
* True if searchable.
|
||||
*/
|
||||
@Input() searchable: boolean;
|
||||
|
||||
/**
|
||||
* Hides embedded viewer in dev mode.
|
||||
*/
|
||||
isViewerAvailable = true;
|
||||
|
||||
/**
|
||||
* The url for the iframe.
|
||||
*/
|
||||
iframeViewerUrl: Observable<SafeResourceUrl>;
|
||||
|
||||
/**
|
||||
* Sets the viewer to show or hide thumbnail side navigation menu.
|
||||
*/
|
||||
multi = false;
|
||||
|
||||
/**
|
||||
* Hides the thumbnail navigation menu on smaller viewports.
|
||||
*/
|
||||
notMobile = false;
|
||||
|
||||
viewerMessage = 'Sorry, the Mirador viewer is not currently available in development mode.';
|
||||
|
||||
constructor(private sanitizer: DomSanitizer,
|
||||
private viewerService: MiradorViewerService,
|
||||
private bitstreamDataService: BitstreamDataService,
|
||||
private hostWindowService: HostWindowService,
|
||||
@Inject(PLATFORM_ID) private platformId: any) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the url for the Mirador iframe. Adds parameters for the displaying the search panel, query results,
|
||||
* or multi-page thumbnail navigation.
|
||||
*/
|
||||
setURL() {
|
||||
// The path to the REST manifest endpoint.
|
||||
const manifestApiEndpoint = encodeURIComponent(environment.rest.baseUrl + '/iiif/'
|
||||
+ this.object.id + '/manifest');
|
||||
// The Express path to Mirador viewer.
|
||||
let viewerPath = '/iiif/mirador/index.html?manifest=' + manifestApiEndpoint;
|
||||
if (this.searchable) {
|
||||
// Tell the viewer add search to menu.
|
||||
viewerPath += '&searchable=' + this.searchable;
|
||||
}
|
||||
if (this.query) {
|
||||
// Tell the viewer to execute a search for the query term.
|
||||
viewerPath += '&query=' + this.query;
|
||||
}
|
||||
if (this.multi) {
|
||||
// Tell the viewer to add thumbnail navigation. If searchable, thumbnail navigation is added by default.
|
||||
viewerPath += '&multi=' + this.multi;
|
||||
}
|
||||
if (this.notMobile) {
|
||||
viewerPath += '¬Mobile=true';
|
||||
}
|
||||
|
||||
// TODO: Should the query term be trusted here?
|
||||
return this.sanitizer.bypassSecurityTrustResourceUrl(viewerPath);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
/**
|
||||
* Initializes the iframe url observable.
|
||||
*/
|
||||
if (isPlatformBrowser(this.platformId)) {
|
||||
|
||||
// Viewer is not currently available in dev mode so hide it in that case.
|
||||
this.isViewerAvailable = this.viewerService.showEmbeddedViewer();
|
||||
|
||||
// The notMobile property affects the thumbnail navigation
|
||||
// menu by hiding it for smaller viewports. This will not be
|
||||
// responsive to resizing.
|
||||
this.hostWindowService.widthCategory
|
||||
.pipe(take(1))
|
||||
.subscribe((category: WidthCategory) => {
|
||||
this.notMobile = !(category === WidthCategory.XS || category === WidthCategory.SM);
|
||||
});
|
||||
|
||||
// We need to set the multi property to true if the
|
||||
// item is searchable or when the ORIGINAL bundle contains more
|
||||
// than 1 image. (The multi property determines whether the
|
||||
// Mirador side thumbnail navigation panel is shown.)
|
||||
if (this.searchable) {
|
||||
this.multi = true;
|
||||
const observable = of('');
|
||||
this.iframeViewerUrl = observable.pipe(
|
||||
map((val) => {
|
||||
return this.setURL();
|
||||
})
|
||||
);
|
||||
} else {
|
||||
// Sets the multi value based on the image count.
|
||||
this.iframeViewerUrl = this.viewerService.getImageCount(this.object, this.bitstreamDataService).pipe(
|
||||
map(c => {
|
||||
if (c > 1) {
|
||||
this.multi = true;
|
||||
}
|
||||
return this.setURL();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
57
src/app/item-page/mirador-viewer/mirador-viewer.service.ts
Normal file
57
src/app/item-page/mirador-viewer/mirador-viewer.service.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Injectable, isDevMode } from '@angular/core';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../core/shared/operators';
|
||||
import { last, map, switchMap } from 'rxjs/operators';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { PaginatedList } from '../../core/data/paginated-list.model';
|
||||
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||
import { BitstreamFormat } from '../../core/shared/bitstream-format.model';
|
||||
import { BitstreamDataService } from '../../core/data/bitstream-data.service';
|
||||
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
|
||||
@Injectable()
|
||||
export class MiradorViewerService {
|
||||
|
||||
LINKS_TO_FOLLOW: FollowLinkConfig<Bitstream>[] = [
|
||||
followLink('format'),
|
||||
];
|
||||
|
||||
/**
|
||||
* Returns boolean to hide viewer when running in development mode.
|
||||
* Needed until it's possible to embed the viewer in development builds.
|
||||
*/
|
||||
showEmbeddedViewer (): boolean {
|
||||
return !isDevMode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns observable of the number of images in the ORIGINAL bundle
|
||||
* @param item
|
||||
* @param bitstreamDataService
|
||||
*/
|
||||
getImageCount(item: Item, bitstreamDataService: BitstreamDataService): Observable<number> {
|
||||
let count = 0;
|
||||
return bitstreamDataService.findAllByItemAndBundleName(item, 'ORIGINAL', {
|
||||
currentPage: 1,
|
||||
elementsPerPage: 10
|
||||
}, true, true, ...this.LINKS_TO_FOLLOW)
|
||||
.pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
map((bitstreamsRD: RemoteData<PaginatedList<Bitstream>>) => bitstreamsRD.payload),
|
||||
map((paginatedList: PaginatedList<Bitstream>) => paginatedList.page),
|
||||
switchMap((bitstreams: Bitstream[]) => bitstreams),
|
||||
switchMap((bitstream: Bitstream) => bitstream.format.pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
map((format: BitstreamFormat) => format)
|
||||
)),
|
||||
map((format: BitstreamFormat) => {
|
||||
if (format.mimetype.includes('image')) {
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}),
|
||||
last()
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,3 +1,12 @@
|
||||
<div class="row" *ngIf="iiifEnabled">
|
||||
<div class="col-12">
|
||||
<ds-mirador-viewer id="iiif-viewer"
|
||||
[object]="object"
|
||||
[searchable]="iiifSearchEnabled"
|
||||
[query]="iiifQuery$ | async">
|
||||
</ds-mirador-viewer>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-row">
|
||||
<h2 class="item-page-title-field mr-auto">
|
||||
{{'publication.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="object?.allMetadata(['dc.title'])"></ds-metadata-values>
|
||||
|
@@ -4,7 +4,7 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service';
|
||||
import { ObjectCacheService } from '../../../../core/cache/object-cache.service';
|
||||
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
|
||||
@@ -25,15 +25,33 @@ import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-dat
|
||||
import { TruncatableService } from '../../../../shared/truncatable/truncatable.service';
|
||||
import { TruncatePipe } from '../../../../shared/utils/truncate.pipe';
|
||||
import { GenericItemPageFieldComponent } from '../../field-components/specific-field/generic/generic-item-page-field.component';
|
||||
import { createRelationshipsObservable } from '../shared/item.component.spec';
|
||||
import {
|
||||
createRelationshipsObservable,
|
||||
iiifEnabled,
|
||||
iiifSearchEnabled, mockRouteService
|
||||
} from '../shared/item.component.spec';
|
||||
import { PublicationComponent } from './publication.component';
|
||||
import { createPaginatedList } from '../../../../shared/testing/utils.test';
|
||||
import { RouteService } from '../../../../core/services/route.service';
|
||||
|
||||
const mockItem: Item = Object.assign(new Item(), {
|
||||
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||
metadata: new MetadataMap(),
|
||||
relationships: createRelationshipsObservable()
|
||||
});
|
||||
const iiifEnabledMap: MetadataMap = {
|
||||
'dspace.iiif.enabled': [iiifEnabled],
|
||||
};
|
||||
|
||||
const iiifEnabledWithSearchMap: MetadataMap = {
|
||||
'dspace.iiif.enabled': [iiifEnabled],
|
||||
'iiif.search.enabled': [iiifSearchEnabled],
|
||||
};
|
||||
|
||||
const noMetadata = new MetadataMap();
|
||||
|
||||
function getItem(metadata: MetadataMap) {
|
||||
return Object.assign(new Item(), {
|
||||
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||
metadata: metadata,
|
||||
relationships: createRelationshipsObservable()
|
||||
});
|
||||
}
|
||||
|
||||
describe('PublicationComponent', () => {
|
||||
let comp: PublicationComponent;
|
||||
@@ -68,6 +86,7 @@ describe('PublicationComponent', () => {
|
||||
{ provide: DSOChangeAnalyzer, useValue: {} },
|
||||
{ provide: DefaultChangeAnalyzer, useValue: {} },
|
||||
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
|
||||
{ provide: RouteService, useValue: mockRouteService }
|
||||
],
|
||||
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
@@ -76,41 +95,81 @@ describe('PublicationComponent', () => {
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
fixture = TestBed.createComponent(PublicationComponent);
|
||||
comp = fixture.componentInstance;
|
||||
comp.object = mockItem;
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
describe('default view', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
fixture = TestBed.createComponent(PublicationComponent);
|
||||
comp = fixture.componentInstance;
|
||||
comp.object = getItem(noMetadata);
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should contain a component to display the date', () => {
|
||||
const fields = fixture.debugElement.queryAll(By.css('ds-item-page-date-field'));
|
||||
expect(fields.length).toBeGreaterThanOrEqual(1);
|
||||
it('should contain a component to display the date', () => {
|
||||
const fields = fixture.debugElement.queryAll(By.css('ds-item-page-date-field'));
|
||||
expect(fields.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('should not contain a metadata only author field', () => {
|
||||
const fields = fixture.debugElement.queryAll(By.css('ds-item-page-author-field'));
|
||||
expect(fields.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should contain a mixed metadata and relationship field for authors', () => {
|
||||
const fields = fixture.debugElement.queryAll(By.css('.ds-item-page-mixed-author-field'));
|
||||
expect(fields.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should contain a component to display the abstract', () => {
|
||||
const fields = fixture.debugElement.queryAll(By.css('ds-item-page-abstract-field'));
|
||||
expect(fields.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('should contain a component to display the uri', () => {
|
||||
const fields = fixture.debugElement.queryAll(By.css('ds-item-page-uri-field'));
|
||||
expect(fields.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('should contain a component to display the collections', () => {
|
||||
const fields = fixture.debugElement.queryAll(By.css('ds-item-page-collections'));
|
||||
expect(fields.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not contain a metadata only author field', () => {
|
||||
const fields = fixture.debugElement.queryAll(By.css('ds-item-page-author-field'));
|
||||
expect(fields.length).toBe(0);
|
||||
describe('with IIIF viewer', () => {
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
fixture = TestBed.createComponent(PublicationComponent);
|
||||
comp = fixture.componentInstance;
|
||||
comp.object = getItem(iiifEnabledMap);
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should contain an iiif viewer component', () => {
|
||||
const fields = fixture.debugElement.queryAll(By.css('ds-mirador-viewer'));
|
||||
expect(fields.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should contain a mixed metadata and relationship field for authors', () => {
|
||||
const fields = fixture.debugElement.queryAll(By.css('.ds-item-page-mixed-author-field'));
|
||||
expect(fields.length).toBe(1);
|
||||
});
|
||||
describe('with IIIF viewer and search', () => {
|
||||
|
||||
it('should contain a component to display the abstract', () => {
|
||||
const fields = fixture.debugElement.queryAll(By.css('ds-item-page-abstract-field'));
|
||||
expect(fields.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
beforeEach(waitForAsync(() => {
|
||||
mockRouteService.getPreviousUrl.and.returnValue(of(['/search?q=bird&motivation=painting','/item']));
|
||||
fixture = TestBed.createComponent(PublicationComponent);
|
||||
comp = fixture.componentInstance;
|
||||
comp.object = getItem(iiifEnabledWithSearchMap);
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should contain a component to display the uri', () => {
|
||||
const fields = fixture.debugElement.queryAll(By.css('ds-item-page-uri-field'));
|
||||
expect(fields.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
it('should contain an iiif viewer component', () => {
|
||||
const fields = fixture.debugElement.queryAll(By.css('ds-mirador-viewer'));
|
||||
expect(fields.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('should call the RouteService getHistory method', () => {
|
||||
expect(mockRouteService.getPreviousUrl).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should contain a component to display the collections', () => {
|
||||
const fields = fixture.debugElement.queryAll(By.css('ds-item-page-collections'));
|
||||
expect(fields.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
@@ -0,0 +1,35 @@
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { Observable } from 'rxjs';
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
import { RouteService } from '../../../../core/services/route.service';
|
||||
|
||||
export const isIiifEnabled = (item: Item) => {
|
||||
return !!item.firstMetadataValue('dspace.iiif.enabled');
|
||||
|
||||
};
|
||||
|
||||
export const isIiifSearchEnabled = (item: Item) => {
|
||||
return !!item.firstMetadataValue('iiif.search.enabled');
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks to see if previous route was a dspace search. If
|
||||
* it was, the search term is extracted and subsequently passed
|
||||
* to the mirador viewer component.
|
||||
* @param item the dspace object
|
||||
* @param routeService
|
||||
*/
|
||||
export const getDSpaceQuery = (item: Item, routeService: RouteService): Observable<string> => {
|
||||
return routeService.getPreviousUrl().pipe(
|
||||
filter(r => {
|
||||
return r.includes('/search');
|
||||
}),
|
||||
map(r => {
|
||||
const arr = r.split('&');
|
||||
const q = arr[1];
|
||||
const v = q.split('=');
|
||||
return v[1];
|
||||
})
|
||||
);
|
||||
};
|
@@ -30,6 +30,26 @@ import { GenericItemPageFieldComponent } from '../../field-components/specific-f
|
||||
import { compareArraysUsing, compareArraysUsingIds } from './item-relationships-utils';
|
||||
import { ItemComponent } from './item.component';
|
||||
import { createPaginatedList } from '../../../../shared/testing/utils.test';
|
||||
import { RouteService } from '../../../../core/services/route.service';
|
||||
import { MetadataValue } from '../../../../core/shared/metadata.models';
|
||||
|
||||
export const iiifEnabled = Object.assign(new MetadataValue(),{
|
||||
'value': 'true',
|
||||
'language': null,
|
||||
'authority': null,
|
||||
'confidence': -1,
|
||||
'place': 0
|
||||
});
|
||||
|
||||
export const iiifSearchEnabled = Object.assign(new MetadataValue(), {
|
||||
'value': 'true',
|
||||
'language': null,
|
||||
'authority': null,
|
||||
'confidence': -1,
|
||||
'place': 0
|
||||
});
|
||||
|
||||
export const mockRouteService = jasmine.createSpyObj('RouteService', ['getPreviousUrl']);
|
||||
|
||||
/**
|
||||
* Create a generic test for an item-page-fields component using a mockItem and the type of component
|
||||
@@ -72,6 +92,7 @@ export function getItemPageFieldsTest(mockItem: Item, component) {
|
||||
{ provide: NotificationsService, useValue: {} },
|
||||
{ provide: DefaultChangeAnalyzer, useValue: {} },
|
||||
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
|
||||
{ provide: RouteService, useValue: {} }
|
||||
],
|
||||
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
|
@@ -2,6 +2,9 @@ import { Component, Input, OnInit } from '@angular/core';
|
||||
import { environment } from '../../../../../environments/environment';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { getItemPageRoute } from '../../../item-page-routing-paths';
|
||||
import { RouteService } from '../../../../core/services/route.service';
|
||||
import { Observable } from 'rxjs';
|
||||
import { getDSpaceQuery, isIiifEnabled, isIiifSearchEnabled } from './item-iiif-utils';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-item',
|
||||
@@ -18,9 +21,33 @@ export class ItemComponent implements OnInit {
|
||||
*/
|
||||
itemPageRoute: string;
|
||||
|
||||
/**
|
||||
* Enables the mirador component.
|
||||
*/
|
||||
iiifEnabled: boolean;
|
||||
|
||||
/**
|
||||
* Used to configure search in mirador.
|
||||
*/
|
||||
iiifSearchEnabled: boolean;
|
||||
|
||||
/**
|
||||
* The query term from the previous dspace search.
|
||||
*/
|
||||
iiifQuery$: Observable<string>;
|
||||
|
||||
mediaViewer = environment.mediaViewer;
|
||||
|
||||
constructor(protected routeService: RouteService) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.itemPageRoute = getItemPageRoute(this.object);
|
||||
// check to see if iiif viewer is required.
|
||||
this.iiifEnabled = isIiifEnabled(this.object);
|
||||
this.iiifSearchEnabled = isIiifSearchEnabled(this.object);
|
||||
if (this.iiifSearchEnabled) {
|
||||
this.iiifQuery$ = getDSpaceQuery(this.object, this.routeService);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,3 +1,12 @@
|
||||
<div class="row" *ngIf="iiifEnabled">
|
||||
<div class="col-12">
|
||||
<ds-mirador-viewer id="iiif-viewer"
|
||||
[object]="object"
|
||||
[searchable]="iiifSearchEnabled"
|
||||
[query]="iiifQuery$ | async">
|
||||
</ds-mirador-viewer>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-row">
|
||||
<h2 class="item-page-title-field mr-auto">
|
||||
<ds-metadata-values [mdValues]="object?.allMetadata(['dc.title'])"></ds-metadata-values>
|
||||
|
@@ -12,14 +12,12 @@ import { CommunityDataService } from '../../../../core/data/community-data.servi
|
||||
import { DefaultChangeAnalyzer } from '../../../../core/data/default-change-analyzer.service';
|
||||
import { DSOChangeAnalyzer } from '../../../../core/data/dso-change-analyzer.service';
|
||||
import { ItemDataService } from '../../../../core/data/item-data.service';
|
||||
import { buildPaginatedList } from '../../../../core/data/paginated-list.model';
|
||||
import { RelationshipService } from '../../../../core/data/relationship.service';
|
||||
import { RemoteData } from '../../../../core/data/remote-data';
|
||||
import { Bitstream } from '../../../../core/shared/bitstream.model';
|
||||
import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { MetadataMap } from '../../../../core/shared/metadata.models';
|
||||
import { PageInfo } from '../../../../core/shared/page-info.model';
|
||||
import { UUIDService } from '../../../../core/shared/uuid.service';
|
||||
import { TranslateLoaderMock } from '../../../../shared/mocks/translate-loader.mock';
|
||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||
@@ -27,8 +25,15 @@ import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-dat
|
||||
import { TruncatableService } from '../../../../shared/truncatable/truncatable.service';
|
||||
import { TruncatePipe } from '../../../../shared/utils/truncate.pipe';
|
||||
import { GenericItemPageFieldComponent } from '../../field-components/specific-field/generic/generic-item-page-field.component';
|
||||
import { createRelationshipsObservable } from '../shared/item.component.spec';
|
||||
import {
|
||||
createRelationshipsObservable,
|
||||
iiifEnabled,
|
||||
iiifSearchEnabled, mockRouteService
|
||||
} from '../shared/item.component.spec';
|
||||
import { UntypedItemComponent } from './untyped-item.component';
|
||||
import { RouteService } from '../../../../core/services/route.service';
|
||||
import { of } from 'rxjs';
|
||||
import { createPaginatedList } from '../../../../shared/testing/utils.test';
|
||||
import { VersionHistoryDataService } from '../../../../core/data/version-history-data.service';
|
||||
import { VersionDataService } from '../../../../core/data/version-data.service';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
@@ -36,11 +41,25 @@ import { WorkspaceitemDataService } from '../../../../core/submission/workspacei
|
||||
import { SearchService } from '../../../../core/shared/search/search.service';
|
||||
import { ItemVersionsSharedService } from '../../../../shared/item/item-versions/item-versions-shared.service';
|
||||
|
||||
const mockItem: Item = Object.assign(new Item(), {
|
||||
bundles: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])),
|
||||
metadata: new MetadataMap(),
|
||||
relationships: createRelationshipsObservable()
|
||||
});
|
||||
|
||||
const iiifEnabledMap: MetadataMap = {
|
||||
'dspace.iiif.enabled': [iiifEnabled],
|
||||
};
|
||||
|
||||
const iiifEnabledWithSearchMap: MetadataMap = {
|
||||
'dspace.iiif.enabled': [iiifEnabled],
|
||||
'iiif.search.enabled': [iiifSearchEnabled],
|
||||
};
|
||||
|
||||
const noMetadata = new MetadataMap();
|
||||
|
||||
function getItem(metadata: MetadataMap) {
|
||||
return Object.assign(new Item(), {
|
||||
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||
metadata: metadata,
|
||||
relationships: createRelationshipsObservable()
|
||||
});
|
||||
}
|
||||
|
||||
describe('UntypedItemComponent', () => {
|
||||
let comp: UntypedItemComponent;
|
||||
@@ -84,48 +103,94 @@ describe('UntypedItemComponent', () => {
|
||||
{ provide: SearchService, useValue: {} },
|
||||
{ provide: ItemDataService, useValue: {} },
|
||||
{ provide: ItemVersionsSharedService, useValue: {} },
|
||||
{ provide: RouteService, useValue: mockRouteService }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(UntypedItemComponent, {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
set: {changeDetection: ChangeDetectionStrategy.Default}
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
fixture = TestBed.createComponent(UntypedItemComponent);
|
||||
comp = fixture.componentInstance;
|
||||
comp.object = mockItem;
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
describe('default view', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
fixture = TestBed.createComponent(UntypedItemComponent);
|
||||
comp = fixture.componentInstance;
|
||||
comp.object = getItem(noMetadata);
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should contain a component to display the date', () => {
|
||||
const fields = fixture.debugElement.queryAll(By.css('ds-item-page-date-field'));
|
||||
expect(fields.length).toBeGreaterThanOrEqual(1);
|
||||
it('should contain a component to display the date', () => {
|
||||
const fields = fixture.debugElement.queryAll(By.css('ds-item-page-date-field'));
|
||||
expect(fields.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('should not contain a metadata only author field', () => {
|
||||
const fields = fixture.debugElement.queryAll(By.css('ds-item-page-author-field'));
|
||||
expect(fields.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should contain a mixed metadata and relationship field for authors', () => {
|
||||
const fields = fixture.debugElement.queryAll(By.css('.ds-item-page-mixed-author-field'));
|
||||
expect(fields.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should contain a component to display the abstract', () => {
|
||||
const fields = fixture.debugElement.queryAll(By.css('ds-item-page-abstract-field'));
|
||||
expect(fields.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('should contain a component to display the uri', () => {
|
||||
const fields = fixture.debugElement.queryAll(By.css('ds-item-page-uri-field'));
|
||||
expect(fields.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('should contain a component to display the collections', () => {
|
||||
const fields = fixture.debugElement.queryAll(By.css('ds-item-page-collections'));
|
||||
expect(fields.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('should not contain an iiif viewer component', () => {
|
||||
const fields = fixture.debugElement.queryAll(By.css('ds-mirador-viewer'));
|
||||
expect(fields.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not contain a metadata only author field', () => {
|
||||
const fields = fixture.debugElement.queryAll(By.css('ds-item-page-author-field'));
|
||||
expect(fields.length).toBe(0);
|
||||
|
||||
describe('with IIIF viewer', () => {
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
fixture = TestBed.createComponent(UntypedItemComponent);
|
||||
comp = fixture.componentInstance;
|
||||
comp.object = getItem(iiifEnabledMap);
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should contain an iiif viewer component', () => {
|
||||
const fields = fixture.debugElement.queryAll(By.css('ds-mirador-viewer'));
|
||||
expect(fields.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should contain a mixed metadata and relationship field for authors', () => {
|
||||
const fields = fixture.debugElement.queryAll(By.css('.ds-item-page-mixed-author-field'));
|
||||
expect(fields.length).toBe(1);
|
||||
});
|
||||
describe('with IIIF viewer and search', () => {
|
||||
|
||||
it('should contain a component to display the abstract', () => {
|
||||
const fields = fixture.debugElement.queryAll(By.css('ds-item-page-abstract-field'));
|
||||
expect(fields.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
beforeEach(waitForAsync(() => {
|
||||
mockRouteService.getPreviousUrl.and.returnValue(of(['/search?q=bird&motivation=painting','/item']));
|
||||
fixture = TestBed.createComponent(UntypedItemComponent);
|
||||
comp = fixture.componentInstance;
|
||||
comp.object = getItem(iiifEnabledWithSearchMap);
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should contain a component to display the uri', () => {
|
||||
const fields = fixture.debugElement.queryAll(By.css('ds-item-page-uri-field'));
|
||||
expect(fields.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
it('should contain an iiif viewer component', () => {
|
||||
const fields = fixture.debugElement.queryAll(By.css('ds-mirador-viewer'));
|
||||
expect(fields.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('should call the RouteService getHistory method', () => {
|
||||
expect(mockRouteService.getPreviousUrl).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should contain a component to display the collections', () => {
|
||||
const fields = fixture.debugElement.queryAll(By.css('ds-item-page-collections'));
|
||||
expect(fields.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -11,13 +11,14 @@ import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-dat
|
||||
import { buildPaginatedList } from '../../../../core/data/paginated-list.model';
|
||||
import { PageInfo } from '../../../../core/shared/page-info.model';
|
||||
import { MetadataMap } from '../../../../core/shared/metadata.models';
|
||||
import { createRelationshipsObservable } from '../shared/item.component.spec';
|
||||
import { createRelationshipsObservable, mockRouteService } from '../shared/item.component.spec';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { Component } from '@angular/core';
|
||||
import { WorkspaceitemDataService } from '../../../../core/submission/workspaceitem-data.service';
|
||||
import { SearchService } from '../../../../core/shared/search/search.service';
|
||||
import { ItemDataService } from '../../../../core/data/item-data.service';
|
||||
import { Version } from '../../../../core/shared/version.model';
|
||||
import { RouteService } from '../../../../core/services/route.service';
|
||||
|
||||
const mockItem: Item = Object.assign(new Item(), {
|
||||
bundles: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])),
|
||||
@@ -66,6 +67,7 @@ describe('VersionedItemComponent', () => {
|
||||
{ provide: WorkspaceitemDataService, useValue: {} },
|
||||
{ provide: SearchService, useValue: {} },
|
||||
{ provide: ItemDataService, useValue: {} },
|
||||
{ provide: RouteService, useValue: mockRouteService }
|
||||
]
|
||||
}).compileComponents();
|
||||
versionService = TestBed.inject(VersionDataService);
|
||||
|
@@ -16,6 +16,7 @@ import { SearchService } from '../../../../core/shared/search/search.service';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { ItemDataService } from '../../../../core/data/item-data.service';
|
||||
import { WorkspaceItem } from '../../../../core/submission/models/workspaceitem.model';
|
||||
import { RouteService } from '../../../../core/services/route.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-versioned-item',
|
||||
@@ -34,8 +35,9 @@ export class VersionedItemComponent extends ItemComponent {
|
||||
private workspaceItemDataService: WorkspaceitemDataService,
|
||||
private searchService: SearchService,
|
||||
private itemService: ItemDataService,
|
||||
protected routeService: RouteService
|
||||
) {
|
||||
super();
|
||||
super(routeService);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -187,6 +187,7 @@ import { MissingTranslationHelper } from './translate/missing-translation.helper
|
||||
import { ItemVersionsNoticeComponent } from './item/item-versions/notice/item-versions-notice.component';
|
||||
import { FileValidator } from './utils/require-file.validator';
|
||||
import { FileValueAccessorDirective } from './utils/file-value-accessor.directive';
|
||||
import { FileSectionComponent } from '../item-page/simple/field-components/file-section/file-section.component';
|
||||
import { ExistingRelationListElementComponent } from './form/builder/ds-dynamic-form-ui/existing-relation-list-element/existing-relation-list-element.component';
|
||||
import { ModifyItemOverviewComponent } from '../item-page/edit-item-page/modify-item-overview/modify-item-overview.component';
|
||||
import { ClaimedTaskActionsLoaderComponent } from './mydspace-actions/claimed-task/switcher/claimed-task-actions-loader.component';
|
||||
@@ -327,6 +328,7 @@ const COMPONENTS = [
|
||||
DsDatePickerInlineComponent,
|
||||
DsSelectComponent,
|
||||
ErrorComponent,
|
||||
FileSectionComponent,
|
||||
FormComponent,
|
||||
LangSwitchComponent,
|
||||
LoadingComponent,
|
||||
|
@@ -2261,6 +2261,28 @@
|
||||
"journalvolume.page.volume": "Volume",
|
||||
|
||||
|
||||
"iiifsearchable.listelement.badge": "Document Media",
|
||||
|
||||
"iiifsearchable.page.titleprefix": "Document: ",
|
||||
|
||||
"iiifsearchable.page.doi": "Permanent Link: ",
|
||||
|
||||
"iiifsearchable.page.issue": "Issue: ",
|
||||
|
||||
"iiifsearchable.page.description": "Description: ",
|
||||
|
||||
"iiifviewer.fullscreen.notice": "Use full screen for better viewing.",
|
||||
|
||||
"iiif.listelement.badge": "Image Media",
|
||||
|
||||
"iiif.page.titleprefix": "Image: ",
|
||||
|
||||
"iiif.page.doi": "Permanent Link: ",
|
||||
|
||||
"iiif.page.issue": "Issue: ",
|
||||
|
||||
"iiif.page.description": "Description: ",
|
||||
|
||||
|
||||
"loading.bitstream": "Loading bitstream...",
|
||||
|
||||
|
166
src/mirador-viewer/index.js
Normal file
166
src/mirador-viewer/index.js
Normal file
@@ -0,0 +1,166 @@
|
||||
import Mirador from 'mirador/dist/es/src/index';
|
||||
import miradorShareDialogPlugin from 'mirador-share-plugin/es/MiradorShareDialog';
|
||||
import miradorSharePlugin from 'mirador-share-plugin/es/miradorSharePlugin';
|
||||
import miradorDownloadPlugin from 'mirador-dl-plugin/es/miradorDownloadPlugin';
|
||||
import miradorDownloadDialog from 'mirador-dl-plugin/es/MiradorDownloadDialog';
|
||||
|
||||
const params = new URLSearchParams(location.search);
|
||||
const manifest = params.get('manifest');
|
||||
const searchOption = params.get('searchable');
|
||||
const query = params.get('query');
|
||||
const multi = params.get('multi');
|
||||
const notMobile = params.get('notMobile');
|
||||
|
||||
let windowSettings = {};
|
||||
let sidbarPanel = 'info';
|
||||
let defaultView = 'single';
|
||||
let multipleItems = false;
|
||||
let thumbNavigation = 'off';
|
||||
|
||||
windowSettings.manifestId = manifest;
|
||||
|
||||
(() => {
|
||||
if (searchOption) {
|
||||
defaultView = 'book';
|
||||
sidbarPanel = 'search';
|
||||
multipleItems = true;
|
||||
if (notMobile) {
|
||||
thumbNavigation = 'far-right';
|
||||
}
|
||||
if (query !== 'null') {
|
||||
windowSettings.defaultSearchQuery = query;
|
||||
}
|
||||
} else {
|
||||
if(multi) {
|
||||
multipleItems = multi;
|
||||
if (notMobile) {
|
||||
thumbNavigation = 'far-right';
|
||||
}
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
(Mirador.viewer(
|
||||
{
|
||||
id: 'mirador',
|
||||
mainMenuSettings: {
|
||||
show: true
|
||||
},
|
||||
thumbnailNavigation: {
|
||||
defaultPosition: thumbNavigation, // 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
|
||||
height: 120, // height of entire ThumbnailNavigation area when position is "far-bottom"
|
||||
width: 100, // width of one canvas (doubled for book view) in ThumbnailNavigation area when position is "far-right"
|
||||
},
|
||||
themes: {
|
||||
light: {
|
||||
palette: {
|
||||
type: 'light',
|
||||
primary: {
|
||||
main: '#266883',
|
||||
},
|
||||
secondary: {
|
||||
main: '#b03727',
|
||||
},
|
||||
shades: { // Shades that can be used to offset color areas of the Workspace / Window
|
||||
dark: '#eeeeee',
|
||||
main: '#ffffff',
|
||||
light: '#ffffff',
|
||||
},
|
||||
highlights: {
|
||||
primary: '#ffff00',
|
||||
secondary: '#00BFFF',
|
||||
},
|
||||
search: {
|
||||
default: { fillStyle: '#00BFFF', globalAlpha: 0.3 },
|
||||
hovered: { fillStyle: '#00FFFF', globalAlpha: 0.3 },
|
||||
selected: { fillStyle: '#ff0900', globalAlpha: 0.3 },
|
||||
},
|
||||
},
|
||||
},
|
||||
dark: {
|
||||
palette: {
|
||||
type: 'dark',
|
||||
primary: {
|
||||
main: '#2790b0',
|
||||
},
|
||||
secondary: {
|
||||
main: '#eeeeee',
|
||||
},
|
||||
highlights: {
|
||||
primary: '#ffff00',
|
||||
secondary: '#00BFFF',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
selectedTheme: 'light',
|
||||
data: [manifest],
|
||||
windows: [
|
||||
windowSettings
|
||||
],
|
||||
miradorSharePlugin: {
|
||||
dragAndDropInfoLink: 'https://iiif.io',
|
||||
embedOption: {
|
||||
enabled: true,
|
||||
embedUrlReplacePattern: [
|
||||
/.*\.edu\/(\w+)\/iiif\/manifest/,
|
||||
manifest
|
||||
],
|
||||
syncIframeDimensions: {
|
||||
height: {param: 'maxheight'},
|
||||
},
|
||||
},
|
||||
shareLink: {
|
||||
enabled: true,
|
||||
manifestIdReplacePattern: [
|
||||
/\/iiif\/manifest/,
|
||||
'',
|
||||
],
|
||||
},
|
||||
},
|
||||
miradorDownloadPlugin: {
|
||||
restrictDownloadOnSizeDefinition: false
|
||||
},
|
||||
window: {
|
||||
allowClose: false,
|
||||
// sideBarOpenByDefault: false,
|
||||
allowFullscreen: true,
|
||||
allowMaximize: false,
|
||||
defaultView: defaultView,
|
||||
sideBarOpen: notMobile,
|
||||
allowTopMenuButton: true,
|
||||
defaultSidebarPanelWidth: 230,
|
||||
switchCanvasOnSearch: true,
|
||||
views: [
|
||||
{ key: 'single', behaviors: ['individuals'] },
|
||||
{ key: 'book', behaviors: ['paged'] },
|
||||
{ key: 'scroll', behaviors: ['continuous'] },
|
||||
{ key: 'gallery' },
|
||||
],
|
||||
panels: {
|
||||
info: true,
|
||||
attribution: false,
|
||||
canvas: true,
|
||||
search: searchOption,
|
||||
layers: false,
|
||||
},
|
||||
sideBarPanel: sidbarPanel
|
||||
},
|
||||
workspace: {
|
||||
allowNewWindows: false,
|
||||
showZoomControls: true,
|
||||
type: 'mosaic'
|
||||
},
|
||||
workspaceControlPanel: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
[
|
||||
miradorShareDialogPlugin,
|
||||
miradorSharePlugin,
|
||||
miradorDownloadDialog,
|
||||
miradorDownloadPlugin
|
||||
]
|
||||
)
|
||||
)(manifest);
|
10
src/mirador-viewer/mirador.html
Normal file
10
src/mirador-viewer/mirador.html
Normal file
@@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Mirador</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="mirador"></div>
|
||||
</body>
|
||||
</html>
|
28
webpack/webpack.mirador.config.ts
Normal file
28
webpack/webpack.mirador.config.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
mode: 'production',
|
||||
entry: {
|
||||
mirador: './src/mirador-viewer/index.js'
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, '..' , 'dist/iiif/mirador'),
|
||||
filename: '[name].js'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.html$/i,
|
||||
loader: 'html-loader',
|
||||
},
|
||||
],
|
||||
},
|
||||
devServer: {
|
||||
contentBase: '../dist/iiif/mirador',
|
||||
},
|
||||
plugins: [new HtmlWebpackPlugin({
|
||||
filename: 'index.html',
|
||||
template: './src/mirador-viewer/mirador.html'
|
||||
})]
|
||||
};
|
Reference in New Issue
Block a user