Skip to content

Commit 4315569

Browse files
FrancescoMolinaroatarix83
authored andcommitted
Merged in task/ux-plus-2023_02_x/IIIF-148 (pull request #55)
Task/ux plus 2023 02 x/IIIF-148 Approved-by: Giuseppe Digilio
2 parents c5038f0 + a0757d9 commit 4315569

8 files changed

Lines changed: 202 additions & 18 deletions

File tree

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,8 @@
8484
"@angular/platform-browser-dynamic": "^15.2.10",
8585
"@angular/platform-server": "^15.2.10",
8686
"@angular/router": "^15.2.10",
87-
"@babel/runtime": "7.26.0",
8887
"@asymmetrik/ngx-leaflet": "^15.0.0",
88+
"@babel/runtime": "7.26.0",
8989
"@datadog/browser-rum": "^5.7.0",
9090
"@kolkov/ngx-gallery": "^2.0.1",
9191
"@ng-bootstrap/ng-bootstrap": "^11.0.0",
@@ -146,14 +146,15 @@
146146
"ng2-pdfjs-viewer": "^15.0.0",
147147
"ngx-infinite-scroll": "^15.0.0",
148148
"ngx-pagination": "6.0.3",
149+
"ngx-quill": "21.0.2",
149150
"ngx-skeleton-loader": "^7.0.0",
150151
"nouislider": "^15.8.1",
151152
"pem": "1.14.8",
152-
"ngx-quill": "21.0.2",
153153
"prop-types": "^15.8.1",
154154
"quill": "1.3.7",
155155
"quill-emoji": "^0.2.0",
156156
"react-copy-to-clipboard": "^5.1.0",
157+
"redux-saga": "^1.3.0",
157158
"reflect-metadata": "^0.2.2",
158159
"rxjs": "^7.8.0",
159160
"sanitize-html": "^2.14.0",
@@ -221,8 +222,8 @@
221222
"karma-jasmine": "~4.0.0",
222223
"karma-jasmine-html-reporter": "^1.5.0",
223224
"karma-mocha-reporter": "2.2.5",
224-
"ngx-export-as": "~1.15.1",
225225
"ng-mocks": "^14.13.2",
226+
"ngx-export-as": "~1.15.1",
226227
"ngx-mask": "13.1.7",
227228
"nodemon": "^2.0.22",
228229
"postcss": "^8.5",

src/app/item-page/mirador-viewer/mirador-viewer.component.spec.ts

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
44
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
55
import { BitstreamDataService } from '../../core/data/bitstream-data.service';
66
import { createRelationshipsObservable } from '../simple/item-types/shared/item.component.spec';
7-
import { NO_ERRORS_SCHEMA } from '@angular/core';
7+
import { NO_ERRORS_SCHEMA, PLATFORM_ID } from '@angular/core';
88
import { MetadataMap } from '../../core/shared/metadata.models';
99
import { Item } from '../../core/shared/item.model';
1010
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
@@ -13,6 +13,7 @@ import { of as observableOf } from 'rxjs';
1313
import { MiradorViewerService } from './mirador-viewer.service';
1414
import { HostWindowService } from '../../shared/host-window.service';
1515
import { BundleDataService } from '../../core/data/bundle-data.service';
16+
import { NativeWindowRef, NativeWindowService } from '../../core/services/window.service';
1617

1718

1819
function getItem(metadata: MetadataMap) {
@@ -48,7 +49,9 @@ describe('MiradorViewerComponent with search', () => {
4849
providers: [
4950
{ provide: BitstreamDataService, useValue: {} },
5051
{ provide: BundleDataService, useValue: {} },
51-
{ provide: HostWindowService, useValue: mockHostWindowService }
52+
{ provide: HostWindowService, useValue: mockHostWindowService },
53+
{ provide: NativeWindowService, useValue: new NativeWindowRef() },
54+
{ provide: Location, useValue: {} },
5255
],
5356
schemas: [NO_ERRORS_SCHEMA]
5457
}).overrideComponent(MiradorViewerComponent, {
@@ -111,7 +114,9 @@ describe('MiradorViewerComponent with multiple images', () => {
111114
providers: [
112115
{ provide: BitstreamDataService, useValue: {} },
113116
{ provide: BundleDataService, useValue: {} },
114-
{ provide: HostWindowService, useValue: mockHostWindowService }
117+
{ provide: HostWindowService, useValue: mockHostWindowService },
118+
{ provide: NativeWindowService, useValue: new NativeWindowRef() },
119+
{ provide: Location, useValue: {} },
115120
],
116121
schemas: [NO_ERRORS_SCHEMA]
117122
}).overrideComponent(MiradorViewerComponent, {
@@ -171,7 +176,9 @@ describe('MiradorViewerComponent with a single image', () => {
171176
providers: [
172177
{ provide: BitstreamDataService, useValue: {} },
173178
{ provide: BundleDataService, useValue: {} },
174-
{ provide: HostWindowService, useValue: mockHostWindowService }
179+
{ provide: HostWindowService, useValue: mockHostWindowService },
180+
{ provide: NativeWindowService, useValue: new NativeWindowRef() },
181+
{ provide: Location, useValue: {} },
175182
],
176183
schemas: [NO_ERRORS_SCHEMA]
177184
}).overrideComponent(MiradorViewerComponent, {
@@ -200,7 +207,58 @@ describe('MiradorViewerComponent with a single image', () => {
200207
it('should call mirador service image count', () => {
201208
expect(viewerService.getImageCount).toHaveBeenCalled();
202209
});
210+
});
211+
212+
});
203213

214+
describe('MiradorViewerComponent on browser in prod mode', () => {
215+
let comp: MiradorViewerComponent;
216+
let fixture: ComponentFixture<MiradorViewerComponent>;
217+
const viewerService = jasmine.createSpyObj('MiradorViewerService', ['showEmbeddedViewer', 'getImageCount']);
218+
219+
beforeEach(waitForAsync(() => {
220+
viewerService.showEmbeddedViewer.and.returnValue(true);
221+
viewerService.getImageCount.and.returnValue(observableOf(1));
222+
TestBed.configureTestingModule({
223+
imports: [TranslateModule.forRoot({
224+
loader: {
225+
provide: TranslateLoader,
226+
useClass: TranslateLoaderMock
227+
}
228+
})],
229+
declarations: [MiradorViewerComponent],
230+
providers: [
231+
{ provide: BitstreamDataService, useValue: {} },
232+
{ provide: BundleDataService, useValue: {} },
233+
{ provide: HostWindowService, useValue: mockHostWindowService },
234+
{ provide: NativeWindowService, useValue: new NativeWindowRef() },
235+
{ provide: Location, useValue: {} },
236+
{ provide: PLATFORM_ID, useValue: 'browser' },
237+
],
238+
schemas: [NO_ERRORS_SCHEMA]
239+
}).overrideComponent(MiradorViewerComponent, {
240+
set: {
241+
providers: [
242+
{ provide: MiradorViewerService, useValue: viewerService }
243+
]
244+
}
245+
}).compileComponents();
246+
}));
247+
248+
describe('viewer init', () => {
249+
beforeEach(waitForAsync(() => {
250+
fixture = TestBed.createComponent(MiradorViewerComponent);
251+
comp = fixture.componentInstance;
252+
comp.object = getItem(noMetadata);
253+
fixture.detectChanges();
254+
}));
255+
256+
it('should set iframe listener', () => {
257+
const compAsAny = comp as any;
258+
spyOn(compAsAny._window.nativeWindow, 'addEventListener');
259+
compAsAny.ngOnInit();
260+
expect(compAsAny._window.nativeWindow.addEventListener).toHaveBeenCalled();
261+
});
204262
});
205263

206264
});
@@ -222,15 +280,17 @@ describe('MiradorViewerComponent in development mode', () => {
222280
})],
223281
declarations: [MiradorViewerComponent],
224282
providers: [
225-
{ provide: BitstreamDataService, useValue: {} }
283+
{ provide: BitstreamDataService, useValue: {} },
226284
],
227285
schemas: [NO_ERRORS_SCHEMA]
228286
}).overrideComponent(MiradorViewerComponent, {
229287
set: {
230288
providers: [
231289
{ provide: MiradorViewerService, useValue: viewerService },
232290
{ provide: BundleDataService, useValue: {} },
233-
{ provide: HostWindowService, useValue: mockHostWindowService }
291+
{ provide: HostWindowService, useValue: mockHostWindowService },
292+
{ provide: NativeWindowService, useValue: new NativeWindowRef() },
293+
{ provide: Location, useValue: {} },
234294
]
235295
}
236296
}).compileComponents();

src/app/item-page/mirador-viewer/mirador-viewer.component.ts

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
1-
import { ChangeDetectionStrategy, Component, Inject, Input, OnInit, PLATFORM_ID } from '@angular/core';
1+
import { ChangeDetectionStrategy, Component, Inject, Input, OnInit, OnDestroy, PLATFORM_ID } from '@angular/core';
22
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
33
import { Item } from '../../core/shared/item.model';
44
import { environment } from '../../../environments/environment';
55
import { BitstreamDataService } from '../../core/data/bitstream-data.service';
66
import { Observable, of } from 'rxjs';
77
import { map, take } from 'rxjs/operators';
8-
import { isPlatformBrowser } from '@angular/common';
8+
import { isPlatformBrowser, Location } from '@angular/common';
99
import { MiradorViewerService } from './mirador-viewer.service';
1010
import { HostWindowService, WidthCategory } from '../../shared/host-window.service';
1111
import { BundleDataService } from '../../core/data/bundle-data.service';
12+
import { NativeWindowRef, NativeWindowService } from '../../core/services/window.service';
13+
14+
const IFRAME_UPDATE_URL_MESSAGE = 'update-url';
15+
16+
interface IFrameMessageData {
17+
type: string;
18+
canvasId: string;
19+
canvasIndex: string;
20+
}
1221

1322
@Component({
1423
selector: 'ds-mirador-viewer',
@@ -17,7 +26,7 @@ import { BundleDataService } from '../../core/data/bundle-data.service';
1726
changeDetection: ChangeDetectionStrategy.OnPush,
1827
providers: [ MiradorViewerService ]
1928
})
20-
export class MiradorViewerComponent implements OnInit {
29+
export class MiradorViewerComponent implements OnInit, OnDestroy {
2130

2231
@Input() object: Item;
2332

@@ -36,6 +45,11 @@ export class MiradorViewerComponent implements OnInit {
3645
*/
3746
@Input() canvasId: string;
3847

48+
/**
49+
* Is used as canvas index of the element to show.
50+
*/
51+
@Input() canvasIndex: string;
52+
3953
/**
4054
* Hides embedded viewer in dev mode.
4155
*/
@@ -58,12 +72,17 @@ export class MiradorViewerComponent implements OnInit {
5872

5973
viewerMessage = 'Sorry, the Mirador viewer is not currently available in development mode.';
6074

61-
constructor(private sanitizer: DomSanitizer,
62-
private viewerService: MiradorViewerService,
63-
private bitstreamDataService: BitstreamDataService,
64-
private bundleDataService: BundleDataService,
65-
private hostWindowService: HostWindowService,
66-
@Inject(PLATFORM_ID) private platformId: any) {
75+
constructor(
76+
private sanitizer: DomSanitizer,
77+
private viewerService: MiradorViewerService,
78+
private bitstreamDataService: BitstreamDataService,
79+
private bundleDataService: BundleDataService,
80+
private hostWindowService: HostWindowService,
81+
private location: Location,
82+
@Inject(PLATFORM_ID) private platformId: any,
83+
@Inject(NativeWindowService) protected _window: NativeWindowRef,
84+
) {
85+
6786
}
6887

6988
/**
@@ -95,6 +114,9 @@ export class MiradorViewerComponent implements OnInit {
95114
if (this.canvasId) {
96115
viewerPath += `&canvasId=${this.canvasId}`;
97116
}
117+
if (this.canvasIndex) {
118+
viewerPath += `&canvasIndex=${parseInt(this.canvasIndex, 10) - 1}`;
119+
}
98120
if (environment.mirador.enableDownloadPlugin) {
99121
viewerPath += '&enableDownloadPlugin=true';
100122
}
@@ -108,6 +130,7 @@ export class MiradorViewerComponent implements OnInit {
108130
* Initializes the iframe url observable.
109131
*/
110132
if (isPlatformBrowser(this.platformId)) {
133+
this._window.nativeWindow.addEventListener('message', this.iframeMessageListener);
111134

112135
// Viewer is not currently available in dev mode so hide it in that case.
113136
this.isViewerAvailable = this.viewerService.showEmbeddedViewer();
@@ -150,4 +173,27 @@ export class MiradorViewerComponent implements OnInit {
150173
}
151174
}
152175
}
176+
177+
ngOnDestroy(): void {
178+
this._window.nativeWindow.removeEventListener('message', this.iframeMessageListener);
179+
}
180+
181+
182+
iframeMessageListener = (event: MessageEvent) => {
183+
const data: IFrameMessageData = event.data;
184+
185+
if (data.type === IFRAME_UPDATE_URL_MESSAGE) {
186+
const currentPath = this.location.path();
187+
const canvasId = data.canvasId;
188+
const canvasIndex = data.canvasIndex;
189+
// Use URL API for easier query param manipulation
190+
const url = new URL(window.location.origin + currentPath);
191+
// Set or update the query param
192+
url.searchParams.set('canvasId', canvasId);
193+
url.searchParams.set('canvasIndex', canvasIndex);
194+
const newPathWithQuery = url.pathname + url.search;
195+
// Replace the current state (no reload, no new history entry)
196+
this.location.replaceState(newPathWithQuery);
197+
}
198+
};
153199
}

src/app/item-page/viewer-provider/viewers/item-viewers/iiif-item-viewer/iiif-item-viewer.component.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@
55
[searchable]="isSearchable$ | async"
66
[query]="query$ | async"
77
[canvasId]="canvasId$ | async"
8+
[canvasIndex]="canvasIndex$ | async"
89
></ds-mirador-viewer>
910
</div>

src/app/item-page/viewer-provider/viewers/item-viewers/iiif-item-viewer/iiif-item-viewer.component.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@ import { ActivatedRoute, ParamMap } from '@angular/router';
1414
export class IIIFItemViewerComponent extends BaseItemViewerComponent implements OnInit {
1515

1616
private readonly CANVAS_PARAM: string = 'canvasId';
17+
private readonly CANVASIDX_PARAM: string = 'canvasIndex';
1718
private readonly QUERY_PARAM: string = 'query';
1819

1920
isSearchable$: Observable<boolean>;
2021
query$: Observable<string>;
2122
canvasId$: Observable<string>;
23+
canvasIndex$: Observable<string>;
2224

2325
constructor(
2426
private readonly routeService: RouteService,
@@ -34,6 +36,9 @@ export class IIIFItemViewerComponent extends BaseItemViewerComponent implements
3436
this.canvasId$ = queryParams$.pipe(
3537
this.extractParam(queryMap => queryMap.get(this.CANVAS_PARAM))
3638
);
39+
this.canvasIndex$ = queryParams$.pipe(
40+
this.extractParam(queryMap => queryMap.get(this.CANVASIDX_PARAM))
41+
);
3742
this.isSearchable$ = this.item$.pipe(
3843
map((item) => isIiifSearchEnabled(item))
3944
);

src/mirador-viewer/config.default.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import miradorShareDialogPlugin from 'mirador-share-plugin/es/MiradorShareDialog
1616
import miradorSharePlugin from 'mirador-share-plugin/es/miradorSharePlugin';
1717
import miradorDownloadPlugin from 'mirador-dl-plugin/es/miradorDownloadPlugin';
1818
import miradorDownloadDialog from 'mirador-dl-plugin/es/MiradorDownloadDialog';
19+
import locationPlugin from './locationPlugin';
1920

2021
const MANIFEST_URL_PART = /\/manifest$/;
2122

@@ -28,6 +29,7 @@ const multi = params.get('multi');
2829
const notMobile = params.get('notMobile');
2930
const isDownloadPluginEnabled = (params.get('enableDownloadPlugin') === 'true');
3031
const canvasId = params.get('canvasId');
32+
const canvasIndex = params.get('canvasIndex');
3133

3234
let windowSettings = {};
3335
let sideBarPanel = 'info';
@@ -62,6 +64,8 @@ windowSettings.manifestId = manifest;
6264
if (canvasId && canvasId !== 'null') {
6365
windowSettings.canvasId =
6466
`${(manifest.replace(MANIFEST_URL_PART, ''))}/canvas/${canvasId}`;
67+
} else if (canvasIndex) {
68+
windowSettings.canvasIndex = parseInt(canvasIndex);
6569
}
6670
})();
6771

@@ -182,6 +186,7 @@ let miradorPlugins = [
182186
miradorShareDialogPlugin,
183187
miradorSharePlugin,
184188
miradorDownloadDialog,
189+
locationPlugin
185190
];
186191

187192
(() => {
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { takeEvery, select } from 'redux-saga/effects';
2+
import ActionTypes from 'mirador/dist/es/src/state/actions/action-types';
3+
import { getCanvasIndex } from 'mirador/dist/es/src/state/selectors';
4+
5+
/**
6+
* Extract id from the url.
7+
* URLs are in the format of "https://domain-name/server/iiif/392363fe-015f-41e6-8cdd-b5c754605787/canvas/03c322b7-0182-44fa-8dc3-5d2efcece237"
8+
*/
9+
const extractCanvasId = (canvasUrl) => canvasUrl.split('/').pop();
10+
11+
/** This will be called every time the SET_CANVAS action is dispatched */
12+
const onCanvasChange = function* (action) {
13+
const { windowId, canvasId } = action;
14+
if (windowId && canvasId) {
15+
const canvasIndex = yield select(getCanvasIndex, { windowId });
16+
const canvasId = extractCanvasId(action.canvasId);
17+
18+
const message = {
19+
type: 'update-url',
20+
// index here starts from 0, whilst for setting the index it starts from 1.
21+
canvasIndex: canvasIndex + 1,
22+
canvasId: canvasId
23+
};
24+
25+
const isValidId = canvasId && canvasId !== 'undefined';
26+
const isBrowserEnvironment = typeof window !== 'undefined';
27+
28+
if (isValidId && isBrowserEnvironment) {
29+
window.parent.postMessage(message, '*');
30+
}
31+
}
32+
}
33+
34+
const pluginSaga = function* () {
35+
/* `takeEvery` calls the associated function every time the action is dispatched */
36+
yield takeEvery([
37+
ActionTypes.SET_CANVAS,
38+
], onCanvasChange);
39+
}
40+
41+
const locationPlugin = {
42+
component: () => null,
43+
saga: pluginSaga,
44+
}
45+
46+
export default locationPlugin;

0 commit comments

Comments
 (0)