Dev eromano external viewer (#1636)

* add extensibility point to add a custom viewer

* documentation and tests

* add example in document list
This commit is contained in:
Eugenio Romano
2017-02-16 09:50:12 +00:00
committed by Mario Romano
parent e3ce65880f
commit 59cbbcbe4d
30 changed files with 309 additions and 83 deletions

View File

@@ -0,0 +1,40 @@
.viewer-image-row {
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
-webkit-box-orient: horizontal;
-moz-box-orient: horizontal;
box-orient: horizontal;
flex-direction: row;
-webkit-box-pack: center;
-moz-box-pack: center;
box-pack: center;
justify-content: center;
-webkit-box-align: center;
-moz-box-align: center;
box-align: center;
align-items: center;
}
.viewer-image-cell {
-webkit-box-flex: 1;
-moz-box-flex: 1;
box-flex: 1;
-webkit-flex: 1 1 auto;
flex: 1 1 auto;
padding: 10px;
margin: 10px;
text-align: center;
}
.viewer-image {
height: 80vh;
max-width:100%;
}

View File

@@ -0,0 +1,8 @@
<div class="viewer-image-content">
<div class="viewer-image-row">
<div class="viewer-image-cell">
<img id="viewer-image" src="{{urlFile}}" alt="{{nameFile}}" class="center-element viewer-image"/>
</div>
</div>
</div>

View File

@@ -0,0 +1,76 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { ImgViewerComponent } from './imgViewer.component';
import { DebugElement } from '@angular/core';
import {
AlfrescoAuthenticationService,
AlfrescoSettingsService,
AlfrescoApiService,
CoreModule
} from 'ng2-alfresco-core';
describe('Test ng2-alfresco-viewer Img viewer component ', () => {
let component: ImgViewerComponent;
let fixture: ComponentFixture<ImgViewerComponent>;
let debug: DebugElement;
let element: HTMLElement;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
CoreModule
],
declarations: [ImgViewerComponent],
providers: [
AlfrescoSettingsService,
AlfrescoAuthenticationService,
AlfrescoApiService
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ImgViewerComponent);
debug = fixture.debugElement;
element = fixture.nativeElement;
component = fixture.componentInstance;
fixture.detectChanges();
});
it('If no url is passed should thrown an error', () => {
expect(() => {
component.ngOnChanges(null);
}).toThrow(new Error('Attribute urlFile is required'));
});
it('If url is passed should not thrown an error', () => {
component.urlFile = 'fake-url';
expect(() => {
component.ngOnChanges(null);
}).not.toThrow(new Error('Attribute urlFile is required'));
});
it('The file Name should be present in the alt attribute', () => {
component.nameFile = 'fake-name';
fixture.detectChanges();
expect(element.querySelector('#viewer-image').getAttribute('alt')).toEqual('fake-name');
});
});

View File

@@ -0,0 +1,39 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, Input } from '@angular/core';
@Component({
moduleId: module.id,
selector: 'img-viewer',
templateUrl: './imgViewer.component.html',
styleUrls: ['./imgViewer.component.css']
})
export class ImgViewerComponent {
@Input()
urlFile: string;
@Input()
nameFile: string;
ngOnChanges(changes) {
if (!this.urlFile) {
throw new Error('Attribute urlFile is required');
}
}
}

View File

@@ -0,0 +1,48 @@
.viewer-video-row {
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
-webkit-box-orient: horizontal;
-moz-box-orient: horizontal;
box-orient: horizontal;
flex-direction: row;
-webkit-box-pack: center;
-moz-box-pack: center;
box-pack: center;
justify-content: center;
-webkit-box-align: center;
-moz-box-align: center;
box-align: center;
align-items: center;
}
.viewer-video-cell {
-webkit-box-flex: 1;
-moz-box-flex: 1;
box-flex: 1;
-webkit-flex: 1 1 auto;
flex: 1 1 auto;
padding: 10px;
margin: 10px;
text-align: center;
}
video {
max-height: 80vh;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: center;
align-items: center;
align-content: center;
max-width: 100%;
margin-left: auto;
margin-right: auto;
}

View File

@@ -0,0 +1,10 @@
<div class="viewer-video-content">
<div class="viewer-video-row">
<div class="viewer-video-cell">
<video controls >
<source [src]="urlFile" [type]="mimeType" />
</video>
</div>
</div>
</div>

View File

@@ -0,0 +1,77 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { MediaPlayerComponent } from './mediaPlayer.component';
import { DebugElement } from '@angular/core';
import {
AlfrescoAuthenticationService,
AlfrescoSettingsService,
AlfrescoApiService,
CoreModule
} from 'ng2-alfresco-core';
describe('Test ng2-alfresco-viewer Media player component ', () => {
let component: MediaPlayerComponent;
let fixture: ComponentFixture<MediaPlayerComponent>;
let debug: DebugElement;
let element: HTMLElement;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
CoreModule
],
declarations: [MediaPlayerComponent],
providers: [
AlfrescoSettingsService,
AlfrescoAuthenticationService,
AlfrescoApiService
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MediaPlayerComponent);
debug = fixture.debugElement;
element = fixture.nativeElement;
component = fixture.componentInstance;
fixture.detectChanges();
});
it('If no url is passed should thrown an error', () => {
expect(() => {
component.ngOnChanges(null);
}).toThrow(new Error('Attribute urlFile is required'));
});
it('If url is passed should not thrown an error', () => {
component.urlFile = 'fake-url';
expect(() => {
component.ngOnChanges(null);
}).not.toThrow(new Error('Attribute urlFile is required'));
});
it('If url is passed should not thrown an error', () => {
component.urlFile = 'fake-url';
expect(() => {
component.ngOnChanges(null);
}).not.toThrow(new Error('Attribute urlFile is required'));
});
});

View File

@@ -0,0 +1,43 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, Input } from '@angular/core';
@Component({
moduleId: module.id,
selector: 'media-player',
templateUrl: './mediaPlayer.component.html',
styleUrls: ['./mediaPlayer.component.css']
})
export class MediaPlayerComponent {
@Input()
urlFile: string;
@Input()
mimeType: string;
@Input()
nameFile: string;
ngOnChanges(changes) {
if (!this.urlFile) {
throw new Error('Attribute urlFile is required');
}
}
}

View File

@@ -0,0 +1,23 @@
.viewer-download-text {
text-align: center;
word-wrap: break-word;
}
.viewer-margin-cloud-download{
margin-right: 20px;
}
.viewer-margin {
margin: auto !important;
}
.center-element {
display: flex;
align-items: center;
justify-content: center;
}
.full_width{
width :95% !important;
}

View File

@@ -0,0 +1,12 @@
<section class="section--center mdl-grid mdl-grid--no-spacing">
<div class="viewer-margin mdl-card mdl-cell mdl-cell--9-col-desktop mdl-cell--6-col-tablet mdl-cell--4-col-phone mdl-shadow--2dp">
<div class="viewer-download-text mdl-card__supporting-text viewer-margin">
<h4>File '<span>{{nameFile}}</span>' is of an unsupported format</h4>
</div>
<div class="center-element mdl-card__actions">
<button id="viewer-download-button" aria-label="Download" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--accent" (click)="download()">
<i class="viewer-margin-cloud-download material-icons">cloud_download</i> Download
</button>
</div>
</div>
</section>

View File

@@ -0,0 +1,81 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { NotSupportedFormat } from './notSupportedFormat.component';
import { DebugElement } from '@angular/core';
import {
AlfrescoAuthenticationService,
AlfrescoSettingsService,
AlfrescoApiService,
CoreModule
} from 'ng2-alfresco-core';
describe('Test ng2-alfresco-viewer Not Supported Format View component', () => {
let component: NotSupportedFormat;
let fixture: ComponentFixture<NotSupportedFormat>;
let debug: DebugElement;
let element: HTMLElement;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
CoreModule
],
declarations: [NotSupportedFormat],
providers: [
AlfrescoSettingsService,
AlfrescoAuthenticationService,
AlfrescoApiService
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(NotSupportedFormat);
debug = fixture.debugElement;
element = fixture.nativeElement;
component = fixture.componentInstance;
fixture.detectChanges();
});
describe('View', () => {
it('Download button should be present', () => {
expect(element.querySelector('#viewer-download-button')).not.toBeNull();
});
it('should display the name of the file', () => {
component.nameFile = 'Example Content.xls';
fixture.detectChanges();
expect(element.querySelector('h4 span').innerHTML).toEqual('Example Content.xls');
});
});
describe('User Interaction', () => {
it('Click on Download button should call download method', () => {
spyOn(window, 'open');
let downloadButton: any = element.querySelector('#viewer-download-button');
downloadButton.click();
expect(window.open).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,40 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, Input } from '@angular/core';
@Component({
moduleId: module.id,
selector: 'not-supported-format',
templateUrl: './notSupportedFormat.component.html',
styleUrls: ['./notSupportedFormat.component.css']
})
export class NotSupportedFormat {
@Input()
nameFile: string;
@Input()
urlFile: string;
/**
* Download file opening it in a new window
*/
download() {
window.open(this.urlFile);
}
}

View File

@@ -0,0 +1,96 @@
.loader-container {
display: -webkit-box; /* OLD - iOS 6-, Safari 3.1-6 */
display: -moz-box; /* OLD - Firefox 19- (buggy but mostly works) */
display: -webkit-flex; /* NEW - Chrome */
display: flex; /* NEW, Spec - Opera 12.1, Firefox 20+ */
-webkit-box-flex-direction: row;
-moz-box-flex-direction: row;
-webkit-flex-direction: row;
flex-direction: row;
height:100%;
}
.loader-item {
margin: auto;
max-height:100px;
max-width:300px;
}
.loader-text{
white-space: nowrap;
text-align: center;
position: relative;
color : #fff;
}
.left {
float: left;
}
.viewer-toolbar-pagination{
padding-top: 4px;
top: 80px;
right:35px;
width:auto;
position:absolute;
border-radius: 10px;
background: #3E3E3E;
color: white;
}
.viewer-toolbar-command{
height: 30px;
padding-top: 4px;
top: 80px;
left:35px;
width:auto;
position:absolute;
border-radius: 10px;
background: #3E3E3E;
color: white;
}
.viewer-pagenumber-input {
border: none;
display: block;
font-size: 16px;
padding: 4px 0;
background: 0 0;
text-align: right;
color: inherit;
width: 33px;
margin-right: 4px;
height: 20px;
}
.viewer-total-pages {
border: medium none;
display: flex;
font-size: 16px;
padding: 4px 0px;
background: transparent none repeat scroll 0px 0px;
text-align: right;
color: inherit;
margin-right: 4px;
height: 20px;
align-items: center;
justify-content: center;
}
.viewer-page-counter {
margin-right: 20px;
}
.button-page {
margin-right: 4px;
height: 24px;
width: 24px;
margin-left: 4px;
cursor: pointer;
}
.button-page:hover {
cursor: pointer;
background: grey;
border-radius: 24px;
}

View File

@@ -0,0 +1,45 @@
<!-- Start Pdf Canvas -->
<div id="viewer-pdf-container" class="viewer-pdf-container" (keypress)=eventHandler() (window:resize)="onResize($event)">
<div id="viewer-viewerPdf" class="pdfViewer">
<div id="loader-container" class="loader-container">
<div class="loader-item">
<div id="loader-spin" class="mdl-progress mdl-js-progress mdl-progress__indeterminate"></div>
<div id="loader-text" class="loader-text">Loading <span>{{nameFile}}</span> {{laodingPercent}}%</div>
</div >
</div>
</div>
</div>
<!-- End Pdf Canvas -->
<!-- Pagination toolbar start -->
<div *ngIf="showToolbar" id="viewer-toolbar-pagination" class="viewer-toolbar-pagination mdl-cell--hide-tablet mdl-cell--hide-phone">
<div id="viewer-previous-page-button" aria-label="arrow left" class="button-page left" (click)="previousPage()">
<i class="icon material-icons">keyboard_arrow_left</i>
</div>
<div class="viewer-page-counter left" >
<input id="viewer-pagenumber-input" #page
(keyup.enter)="inputPage(page.value)" class="viewer-pagenumber-input left" type="text" pattern="-?[0-9]*(\.[0-9]+)?" value="{{displayPage}}">
<div id="viewer-total-pages" class="left viewer-total-pages">/ {{totalPages}}</div>
</div>
<div id="viewer-next-page-button" aria-label="arrow right" class="button-page left" (click)="nextPage()" >
<i class="icon material-icons" >keyboard_arrow_right</i>
</div>
</div>
<!-- Pagination toolbar end -->
<!-- Command toolbar start -->
<div *ngIf="showToolbar" id="viewer-toolbar-command" class="viewer-toolbar-command">
<div id="viewer-scale-page-button" aria-label="zoom out map" class="button-page left" (click)="pageFit()">
<i class="icon material-icons">zoom_out_map</i>
</div>
<div id="viewer-zoom-in-button" aria-label="zoom in" class="button-page left" (click)="zoomIn()">
<i class="icon material-icons">zoom_in</i>
</div>
<div id="viewer-zoom-out-button" aria-label="zoom out" class="button-page left" (click)="zoomOut()">
<i class="icon material-icons">zoom_out</i>
</div>
</div>
<!-- Command toolbar end -->

View File

@@ -0,0 +1,275 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { RenderingQueueServices } from '../services/rendering-queue.services';
import { PdfViewerComponent } from './pdfViewer.component';
import { EventMock } from '../assets/event.mock';
import { DebugElement } from '@angular/core';
import {
AlfrescoAuthenticationService,
AlfrescoSettingsService,
AlfrescoApiService,
CoreModule
} from 'ng2-alfresco-core';
describe('Test ng2-alfresco-viewer PdfViewer component', () => {
let component: PdfViewerComponent;
let fixture: ComponentFixture<PdfViewerComponent>;
let debug: DebugElement;
let element: HTMLElement;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
CoreModule
],
declarations: [PdfViewerComponent],
providers: [
AlfrescoSettingsService,
AlfrescoAuthenticationService,
AlfrescoApiService,
RenderingQueueServices
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(PdfViewerComponent);
debug = fixture.debugElement;
element = fixture.nativeElement;
component = fixture.componentInstance;
component.showToolbar = true;
component.urlFile = 'base/src/assets/fake-test-file.pdf';
fixture.detectChanges();
});
describe('View', () => {
it('If urlfile is not present should not be thrown any error ', () => {
component.urlFile = undefined;
fixture.detectChanges();
expect(() => {
component.ngOnChanges(null);
}).toThrow();
});
it('Canvas should be present', () => {
expect(element.querySelector('#viewer-viewerPdf')).not.toBeNull();
expect(element.querySelector('#viewer-pdf-container')).not.toBeNull();
});
it('Loader should be present', () => {
expect(element.querySelector('#loader-container')).not.toBeNull();
});
it('Next an Previous Buttons should be present', () => {
expect(element.querySelector('#viewer-previous-page-button')).not.toBeNull();
expect(element.querySelector('#viewer-next-page-button')).not.toBeNull();
});
it('Input Page elements should be present', () => {
expect(element.querySelector('#viewer-pagenumber-input')).toBeDefined();
expect(element.querySelector('#viewer-total-pages')).toBeDefined();
expect(element.querySelector('#viewer-previous-page-button')).not.toBeNull();
expect(element.querySelector('#viewer-next-page-button')).not.toBeNull();
});
it('Toolbar should be hide if showToolbar is false', () => {
component.showToolbar = false;
fixture.detectChanges();
expect(element.querySelector('#viewer-toolbar-command')).toBeNull();
expect(element.querySelector('#viewer-toolbar-pagination')).toBeNull();
});
});
describe('User interaction', () => {
beforeEach(() => {
component.inputPage('1');
});
it('Total number of pages should be loaded', (done) => {
component.ngOnChanges(null).then(() => {
fixture.detectChanges();
expect(component.totalPages).toEqual(6);
done();
});
}, 5000);
it('right arrow should move to the next page', (done) => {
component.ngOnChanges(null).then(() => {
fixture.detectChanges();
expect(component.displayPage).toBe(1);
EventMock.keyDown(39);
fixture.detectChanges();
expect(component.displayPage).toBe(2);
done();
});
}, 5000);
it('nextPage should move to the next page', (done) => {
let nextPageButton: any = element.querySelector('#viewer-next-page-button');
component.ngOnChanges(null).then(() => {
fixture.detectChanges();
expect(component.displayPage).toBe(1);
nextPageButton.click();
fixture.detectChanges();
expect(component.displayPage).toBe(2);
done();
});
});
it('left arrow should move to the previous page', (done) => {
component.ngOnChanges(null).then(() => {
fixture.detectChanges();
expect(component.displayPage).toBe(1);
EventMock.keyDown(39);
EventMock.keyDown(39);
EventMock.keyDown(37);
fixture.detectChanges();
expect(component.displayPage).toBe(2);
done();
});
});
it('previous page should move to the previous page', (done) => {
let previousPageButton: any = element.querySelector('#viewer-previous-page-button');
let nextPageButton: any = element.querySelector('#viewer-next-page-button');
component.ngOnChanges(null).then(() => {
fixture.detectChanges();
expect(component.displayPage).toBe(1);
nextPageButton.click();
nextPageButton.click();
previousPageButton.click();
fixture.detectChanges();
expect(component.displayPage).toBe(2);
done();
});
});
it('previous page should not move to the previous page if is page 1', (done) => {
component.ngOnChanges(null).then(() => {
fixture.detectChanges();
expect(component.displayPage).toBe(1);
component.previousPage();
fixture.detectChanges();
expect(component.displayPage).toBe(1);
done();
});
});
it('Input page should move to the inserted page', (done) => {
component.ngOnChanges(null).then(() => {
fixture.detectChanges();
expect(component.displayPage).toBe(1);
component.inputPage('2');
fixture.detectChanges();
expect(component.displayPage).toBe(2);
done();
});
});
describe('Zoom', () => {
beforeEach(() => {
component.currentScale = 1;
fixture.detectChanges();
});
it('In should increment the scale value', (done) => {
let zoomInButton: any = element.querySelector('#viewer-zoom-in-button');
component.ngOnChanges(null).then(() => {
let zoomBefore = component.currentScale;
zoomInButton.click();
expect(component.currentScaleMode).toBe('auto');
let currentZoom = component.currentScale;
expect(zoomBefore < currentZoom).toBe(true);
done();
});
});
it('Out should decrement the scale value', (done) => {
let zoomOutButton: any = element.querySelector('#viewer-zoom-out-button');
component.ngOnChanges(null).then(() => {
let zoomBefore = component.currentScale;
zoomOutButton.click();
expect(component.currentScaleMode).toBe('auto');
let currentZoom = component.currentScale;
expect(zoomBefore > currentZoom).toBe(true);
done();
});
});
it('fit-in button should toggle page-fit and auto scale mode', (done) => {
let fitPage: any = element.querySelector('#viewer-scale-page-button');
component.ngOnChanges(null).then(() => {
expect(component.currentScaleMode).toBe('auto');
fitPage.click();
expect(component.currentScaleMode).toBe('page-fit');
fitPage.click();
expect(component.currentScaleMode).toBe('auto');
done();
});
}, 5000);
});
});
describe('Resize interaction', () => {
it('resize event should trigger setScaleUpdatePages', (done) => {
component.ngOnChanges(null).then(() => {
fixture.detectChanges();
spyOn(component, 'onResize');
EventMock.resizeMobileView();
expect(component.onResize).toHaveBeenCalled();
done();
});
});
});
describe('scroll interaction', () => {
it('scroll page should return the current page', (done) => {
component.ngOnChanges(null).then(() => {
fixture.detectChanges();
expect(component.displayPage).toBe(1);
component.inputPage('2');
fixture.detectChanges();
expect(component.displayPage).toBe(2);
let documentContainer = element.querySelector('#viewer-pdf-container');
documentContainer.scrollTop = 100000;
component.watchScroll(documentContainer);
fixture.detectChanges();
expect(component.displayPage).toBe(6);
done();
});
});
});
});

View File

@@ -0,0 +1,381 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, Input, HostListener } from '@angular/core';
import { LogService } from 'ng2-alfresco-core';
import { RenderingQueueServices } from '../services/rendering-queue.services';
declare let PDFJS: any;
@Component({
moduleId: module.id,
selector: 'pdf-viewer',
templateUrl: './pdfViewer.component.html',
styleUrls: ['./pdfViewer.component.css', './pdfViewerHost.component.css'],
providers: [RenderingQueueServices]
})
export class PdfViewerComponent {
@Input()
urlFile: string;
@Input()
nameFile: string;
@Input()
showToolbar: boolean = true;
currentPdfDocument: any;
page: number;
displayPage: number;
totalPages: number;
laodingPercent: number;
pdfViewer: any;
currentScaleMode: string = 'auto';
currentScale: number;
MAX_AUTO_SCALE: number = 1.25;
DEFAULT_SCALE_DELTA: number = 1.1;
MIN_SCALE: number = 0.25;
MAX_SCALE: number = 10.0;
constructor(private renderingQueueServices: RenderingQueueServices,
private logService: LogService) {
}
ngOnChanges(changes) {
if (!this.urlFile) {
throw new Error('Attribute urlFile is required');
}
if (this.urlFile) {
return new Promise((resolve, reject) => {
let loadingTask = this.getPDFJS().getDocument(this.urlFile);
loadingTask.onProgress = (progressData) => {
let level = progressData.loaded / progressData.total;
this.laodingPercent = Math.round(level * 100);
};
loadingTask.then((pdfDocument) => {
this.currentPdfDocument = pdfDocument;
this.totalPages = pdfDocument.numPages;
this.page = 1;
this.displayPage = 1;
this.initPDFViewer(this.currentPdfDocument);
this.currentPdfDocument.getPage(1).then(() => {
this.scalePage('auto');
resolve();
}, (error) => {
reject(error);
});
}, (error) => {
reject(error);
});
});
}
}
/**
* return the PDFJS global object (exist to facilitate the mock of PDFJS in the test)
*
* @returns {PDFJS}
*/
getPDFJS() {
return PDFJS;
}
initPDFViewer(pdfDocument: any) {
PDFJS.verbosity = 1;
PDFJS.disableWorker = false;
let documentContainer = document.getElementById('viewer-pdf-container');
let viewer: any = document.getElementById('viewer-viewerPdf');
window.document.addEventListener('scroll', (event) => {
this.watchScroll(event.target);
}, true);
this.pdfViewer = new PDFJS.PDFViewer({
container: documentContainer,
viewer: viewer,
renderingQueue: this.renderingQueueServices
});
this.renderingQueueServices.setViewer(this.pdfViewer);
this.pdfViewer.setDocument(pdfDocument);
}
/**
* Method to scale the page current support implementation
*
* @param {string} scaleMode - new scale mode
*/
scalePage(scaleMode) {
this.currentScaleMode = scaleMode;
if (this.pdfViewer) {
let viewerContainer = document.getElementById('viewer-main-container');
let documentContainer = document.getElementById('viewer-pdf-container');
let widthContainer;
let heigthContainer;
if (viewerContainer && viewerContainer.clientWidth <= documentContainer.clientWidth) {
widthContainer = viewerContainer.clientWidth;
heigthContainer = viewerContainer.clientHeight;
} else {
widthContainer = documentContainer.clientWidth;
heigthContainer = documentContainer.clientHeight;
}
let currentPage = this.pdfViewer._pages[this.pdfViewer._currentPageNumber];
let padding = 20;
let pageWidthScale = (widthContainer - padding) / currentPage.width * currentPage.scale;
let pageHeightScale = (heigthContainer - padding) / currentPage.width * currentPage.scale;
let scale;
switch (this.currentScaleMode) {
case 'page-actual':
scale = 1;
break;
case 'page-width':
scale = pageWidthScale;
break;
case 'page-height':
scale = pageHeightScale;
break;
case 'page-fit':
scale = Math.min(pageWidthScale, pageHeightScale);
break;
case 'auto':
let horizontalScale;
if (this.isLandscape) {
horizontalScale = Math.min(pageHeightScale, pageWidthScale);
} else {
horizontalScale = pageWidthScale;
}
scale = Math.min(this.MAX_AUTO_SCALE, horizontalScale);
break;
default:
this.logService.error('pdfViewSetScale: \'' + scaleMode + '\' is an unknown zoom value.');
return;
}
this.setScaleUpdatePages(scale);
}
}
/**
* Update all the pages with the newScale scale
*
* @param {number} newScale - new scale page
*/
setScaleUpdatePages(newScale: number) {
if (!this.isSameScale(this.currentScale, newScale)) {
this.currentScale = newScale;
this.pdfViewer._pages.forEach(function (currentPage) {
currentPage.update(newScale);
});
this.pdfViewer.update();
}
}
/**
* method to check if the request scale of the page is the same for avoid unuseful re-rendering
*
* @param {number} oldScale - old scale page
* @param {number} newScale - new scale page
*
* @returns {boolean}
*/
isSameScale(oldScale: number, newScale: number) {
return (newScale === oldScale);
}
/**
* method to check if is a land scape view
*
* @param {number} width
* @param {number} height
*
* @returns {boolean}
*/
isLandscape(width: number, height: number) {
return (width > height);
}
/**
* Method triggered when the page is resized
*/
onResize() {
this.scalePage(this.currentScaleMode);
}
/**
* toggle the fit page pdf
*/
pageFit() {
if (this.currentScaleMode !== 'page-fit') {
this.scalePage('page-fit');
} else {
this.scalePage('auto');
}
}
/**
* zoom in page pdf
*
* @param {number} ticks
*/
zoomIn(ticks: number) {
let newScale: any = this.currentScale;
do {
newScale = (newScale * this.DEFAULT_SCALE_DELTA).toFixed(2);
newScale = Math.ceil(newScale * 10) / 10;
newScale = Math.min(this.MAX_SCALE, newScale);
} while (--ticks > 0 && newScale < this.MAX_SCALE);
this.currentScaleMode = 'auto';
this.setScaleUpdatePages(newScale);
}
/**
* zoom out page pdf
*
* @param {number} ticks
*/
zoomOut(ticks: number) {
let newScale: any = this.currentScale;
do {
newScale = (newScale / this.DEFAULT_SCALE_DELTA).toFixed(2);
newScale = Math.floor(newScale * 10) / 10;
newScale = Math.max(this.MIN_SCALE, newScale);
} while (--ticks > 0 && newScale > this.MIN_SCALE);
this.currentScaleMode = 'auto';
this.setScaleUpdatePages(newScale);
}
/**
* load the previous page
*/
previousPage() {
if (this.pdfViewer && this.page > 1) {
this.page--;
this.displayPage = this.page;
this.pdfViewer.currentPageNumber = this.page;
}
}
/**
* load the next page
*/
nextPage() {
if (this.pdfViewer && this.page < this.totalPages) {
this.page++;
this.displayPage = this.page;
this.pdfViewer.currentPageNumber = this.page;
}
}
/**
* load the page in input
*
* @param {string} page - page to load
*/
inputPage(page: string) {
let pageInput = parseInt(page, 10);
if (!isNaN(pageInput) && pageInput > 0 && pageInput <= this.totalPages) {
this.page = pageInput;
this.displayPage = this.page;
this.pdfViewer.currentPageNumber = this.page;
} else {
this.displayPage = this.page;
}
}
/**
* Litener Scroll Event
*
* @param {any} target
*/
watchScroll(target) {
let outputPage = this.getVisibleElement(target);
if (outputPage) {
this.page = outputPage.id;
this.displayPage = this.page;
}
}
/**
* find out what elements are visible within a scroll pane
*
* @param {any} target
*
* @returns {Object} page
*/
getVisibleElement(target) {
return this.pdfViewer._pages.find((page) => {
return this.isOnScreen(page, target);
});
}
/**
* check if a page is visible
*
* @param {any} page
* @param {any} target
*
* @returns {boolean}
*/
isOnScreen(page: any, target: any) {
let viewport: any = {};
viewport.top = target.scrollTop;
viewport.bottom = viewport.top + target.scrollHeight;
let bounds: any = {};
bounds.top = page.div.offsetTop;
bounds.bottom = bounds.top + page.viewport.height;
return ((bounds.top <= viewport.bottom) && (bounds.bottom >= viewport.top));
};
/**
* Litener Keyboard Event
* @param {KeyboardEvent} event
*/
@HostListener('document:keydown', ['$event'])
handleKeyboardEvent(event: KeyboardEvent) {
let key = event.keyCode;
if (key === 39) { // right arrow
this.nextPage();
} else if (key === 37) {// left arrow
this.previousPage();
}
}
}

View File

@@ -0,0 +1,265 @@
:host >>> .textLayer {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
overflow: hidden;
opacity: 0.2;
line-height: 1.0;
}
:host >>> .textLayer > div {
color: transparent;
position: absolute;
white-space: pre;
cursor: text;
-webkit-transform-origin: 0% 0%;
-moz-transform-origin: 0% 0%;
-o-transform-origin: 0% 0%;
-ms-transform-origin: 0% 0%;
transform-origin: 0% 0%;
}
:host >>> .textLayer .highlight {
margin: -1px;
padding: 1px;
background-color: rgb(180, 0, 170);
border-radius: 4px;
}
:host >>> .textLayer .highlight.begin {
border-radius: 4px 0px 0px 4px;
}
:host >>> .textLayer .highlight.end {
border-radius: 0px 4px 4px 0px;
}
:host >>> .textLayer .highlight.middle {
border-radius: 0px;
}
:host >>> .textLayer .highlight.selected {
background-color: rgb(0, 100, 0);
}
:host >>> .textLayer ::selection { background: rgb(0,0,255); }
:host >>> .textLayer ::-moz-selection { background: rgb(0,0,255); }
:host >>> .textLayer .endOfContent {
display: block;
position: absolute;
left: 0px;
top: 100%;
right: 0px;
bottom: 0px;
z-index: -1;
cursor: default;
-webkit-user-select: none;
-ms-user-select: none;
-moz-user-select: none;
}
:host >>> .textLayer .endOfContent.active {
top: 0px;
}
:host >>> .annotationLayer section {
position: absolute;
}
:host >>> .annotationLayer .linkAnnotation > a {
position: absolute;
font-size: 1em;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
:host >>> .annotationLayer .linkAnnotation > a /* -ms-a */ {
background: url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7") 0 0 repeat;
}
:host >>> .annotationLayer .linkAnnotation > a:hover {
opacity: 0.2;
background: #ff0;
box-shadow: 0px 2px 10px #ff0;
}
:host >>> .annotationLayer .textAnnotation img {
position: absolute;
cursor: pointer;
}
:host >>> .annotationLayer .popupWrapper {
position: absolute;
width: 20em;
}
:host >>> .annotationLayer .popup {
position: absolute;
z-index: 200;
max-width: 20em;
background-color: #FFFF99;
box-shadow: 0px 2px 5px #333;
border-radius: 2px;
padding: 0.6em;
margin-left: 5px;
cursor: pointer;
word-wrap: break-word;
}
:host >>> .annotationLayer .popup h1 {
font-size: 1em;
border-bottom: 1px solid #000000;
padding-bottom: 0.2em;
}
:host >>> .annotationLayer .popup p {
padding-top: 0.2em;
}
:host >>> .annotationLayer .highlightAnnotation,
.annotationLayer .underlineAnnotation,
.annotationLayer .squigglyAnnotation,
.annotationLayer .strikeoutAnnotation,
.annotationLayer .fileAttachmentAnnotation {
cursor: pointer;
}
:host >>> .pdfViewer .canvasWrapper {
overflow: hidden;
}
:host >>> .pdfViewer .page {
direction: ltr;
width: 816px;
height: 1056px;
margin: 1px auto -8px auto;
position: relative;
overflow: visible;
border: 9px solid transparent;
background-clip: content-box;
background-color: white;
}
:host >>> .pdfViewer.removePageBorders .page {
margin: 0px auto 10px auto;
border: none;
}
:host >>> .pdfViewer .page canvas {
margin: 0;
display: block;
}
:host >>> .pdfViewer .page .loadingIcon {
position: absolute;
display: block;
left: 0;
top: 0;
right: 0;
bottom: 0;
}
:host >>> .loadingIcon {
width: 100px;
height: 100px;
left: 50% !important;
top: 50% !important;
margin-top: -50px;
margin-left: -50px;
font-size: 5px;
text-indent: -9999em;
border-top: 1.1em solid rgba(3,0,2, 0.2);
border-right: 1.1em solid rgba(3,0,2, 0.2);
border-bottom: 1.1em solid rgba(3,0,2, 0.2);
border-left: 1.1em solid #030002;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
-webkit-animation: load8 1.1s infinite linear;
animation: load8 1.1s infinite linear;
}
:host >>> .loadingIcon,
:host >>> .loadingIcon:after {
border-radius: 50%;
}
@-webkit-keyframes load8 {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes load8 {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
:host >>> * {
padding: 0;
margin: 0;
}
:host >>> html {
height: 100%;
width: 100%;
/* Font size is needed to make the activity bar the correct size. */
font-size: 10px;
}
:host >>> body {
height: 100%;
width: 100%;
background-color: #404040;
background-image: url(images/texture.png);
}
:host >>> body,
input,
button,
select {
font: message-box;
outline: none;
}
:host >>> .hidden {
display: none !important;
}
:host >>> [hidden] {
display: none !important;
}
#viewer-pdf-container {
overflow: auto;
-webkit-overflow-scrolling: touch;
position: absolute;
top: 32px;
right: 0;
bottom: 0;
left: 0;
outline: none;
}
html[dir='ltr'] #viewer-pdf-container {
box-shadow: inset 1px 0 0 hsla(0,0%,100%,.05);
}
html[dir='rtl'] #viewer-pdf-container {
box-shadow: inset -1px 0 0 hsla(0,0%,100%,.05);
}

View File

@@ -0,0 +1,63 @@
.button-container {
padding: 0 40px;
}
.left {
float: left;
}
#page-content {
display: flex;
flex-direction: row;
flex-wrap: wrap;
flex: 1;
}
.mdl-grid {
width: 100vw;
padding: 0px !important;
}
.viewer-name-file {
width: 100%;
height: 21px;
overflow: hidden !important;
text-overflow: ellipsis;
white-space: nowrap;
padding-right: 60px;
margin-right: -40px;
}
.viewer-shadow-transparent {
z-index: 1000;
background-color: #3E3E3E;
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
opacity: .90;
}
.viewer-overlay-view {
position: fixed;
top: 0px;
left: 0px;
z-index: 1000;
}
img-viewer {
height: 100%;
}
.center-element {
display: flex;
align-items: center;
justify-content: center;
}
.all-space{
width: 100%;
height: 100%;
background-color: #515151;
}

View File

@@ -0,0 +1,72 @@
<div id="viewer" *ngIf="showViewer" [ngClass]="{'all-space': !overlayMode }">
<div *ngIf="overlayMode">
<div id="viewer-shadow-transparent" class="viewer-shadow-transparent"></div>
</div>
<div id="viewer-main-container" class="all-space" [ngClass]="{'viewer-overlay-view': overlayMode }">
<!-- Start Layout -->
<div [ngClass]="{'mdl-layout mdl-js-layout mdl-layout--fixed-header': overlayMode, 'all-space': !overlayMode}">
<header *ngIf="overlayMode" class="mdl-layout__header">
<div class="mdl-layout__header-row">
<!-- File Title -->
<span id="viewer-name-file" class="mdl-layout-title viewer-name-file">{{displayName}}</span>
<span class="vertical-divider"></span>
<div class="mdl-layout-spacer"></div>
<!-- Start Navigation -->
<nav class="mdl-navigation">
<div id="viewer-toolbar-view-options">
<button *ngIf="overlayMode"
class="mdl-color--black mdl-button mdl-js-button mdl-button--fab mdl-button--mini-fab mdl-button--colored"
(click)="close()" aria-label="Close">
<i id="viewer-close-button" class="icon material-icons">close</i>
</button>
</div>
</nav>
<!-- End Navigation -->
</div>
</header>
<main id="page-content" class="mdl-layout__content" [ngClass]="{'all-space': !overlayMode }">
<div class="mdl-grid">
<div id="viewer-content-container" *ngIf="isLoaded()"
class="center-element mdl-cell mdl-cell--12-col">
<!-- Start View Switch-->
<div *ngIf="isPdf()">
<pdf-viewer [showToolbar]="showToolbar" [urlFile]="urlFileContent"
[nameFile]="displayName"></pdf-viewer>
</div>
<div class="center-element" *ngIf="isImage()">
<img-viewer [urlFile]="urlFileContent" [nameFile]="displayName"></img-viewer>
</div>
<div class="center-element" *ngIf="isMedia()">
<media-player [urlFile]="urlFileContent" [mimeType]="mimeType"
[nameFile]="displayName"></media-player>
</div>
<span *ngFor="let extensionTemplate of extensionTemplates">
<template [ngTemplateOutlet]="extensionTemplate.template" *ngIf="extensionTemplate.isVisible"
[ngOutletContext]="{ urlFileContent: urlFileContent, extension:extension }"></template>
</span>
<div *ngIf="!supportedExtension()">
<not-supported-format *ngIf="!extensionTemplate" [urlFile]="urlFileContent" [nameFile]="displayName"></not-supported-format>
</div>
<!-- End View Switch -->
</div>
</div>
</main>
</div>
<!-- End Layout -->
</div>
</div>

View File

@@ -0,0 +1,334 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { PdfViewerComponent } from './pdfViewer.component';
import { NotSupportedFormat } from './notSupportedFormat.component';
import { MediaPlayerComponent } from './mediaPlayer.component';
import { ImgViewerComponent } from './imgViewer.component';
import { RenderingQueueServices } from '../services/rendering-queue.services';
import { ViewerComponent } from './viewer.component';
import { EventMock } from '../assets/event.mock';
import { DebugElement } from '@angular/core';
import {
AlfrescoAuthenticationService,
AlfrescoSettingsService,
AlfrescoApiService,
CoreModule
} from 'ng2-alfresco-core';
declare let jasmine: any;
describe('Test ng2-alfresco-viewer ViewerComponent', () => {
let component: ViewerComponent;
let fixture: ComponentFixture<ViewerComponent>;
let debug: DebugElement;
let element: HTMLElement;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
CoreModule
],
declarations: [
ViewerComponent,
PdfViewerComponent,
NotSupportedFormat,
MediaPlayerComponent,
ImgViewerComponent
],
providers: [
AlfrescoSettingsService,
AlfrescoAuthenticationService,
AlfrescoApiService,
RenderingQueueServices
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ViewerComponent);
debug = fixture.debugElement;
element = fixture.nativeElement;
component = fixture.componentInstance;
jasmine.Ajax.install();
component.showToolbar = true;
component.urlFile = 'base/src/assets/fake-test-file.pdf';
fixture.detectChanges();
fixture.detectChanges();
});
afterEach(() => {
jasmine.Ajax.uninstall();
});
describe('View', () => {
describe('Overlay mode true', () => {
beforeEach(() => {
component.overlayMode = true;
fixture.detectChanges();
});
it('shadow overlay should be present if is overlay mode', () => {
expect(element.querySelector('#viewer-shadow-transparent')).not.toBeNull();
});
it('header should be present if is overlay mode', () => {
expect(element.querySelector('header')).not.toBeNull();
});
it('Name File should be present if is overlay mode ', () => {
component.ngOnChanges(null).then(() => {
fixture.detectChanges();
expect(element.querySelector('#viewer-name-file').innerHTML).toEqual('fake-test-file.pdf');
});
});
it('Close button should be present if overlay mode', () => {
expect(element.querySelector('#viewer-close-button')).not.toBeNull();
});
it('Click on close button should hide the viewer', () => {
let closebutton: any = element.querySelector('#viewer-close-button');
closebutton.click();
fixture.detectChanges();
expect(element.querySelector('#viewer-main-container')).toBeNull();
});
it('Esc button should hide the viewer', () => {
EventMock.keyDown(27);
fixture.detectChanges();
expect(element.querySelector('#viewer-main-container')).toBeNull();
});
it('all-space class should not be present if is in overlay mode', () => {
expect(element.querySelector('#viewer').getAttribute('class')).toEqual('');
});
});
describe('Overlay mode false', () => {
beforeEach(() => {
component.overlayMode = false;
fixture.detectChanges();
});
it('header should be NOT be present if is not overlay mode', () => {
expect(element.querySelector('header')).toBeNull();
});
it('Close button should be not present if is not overlay mode', () => {
expect(element.querySelector('#viewer-close-button')).toBeNull();
});
it('Esc button should not hide the viewer if is not overlay mode', () => {
EventMock.keyDown(27);
fixture.detectChanges();
expect(element.querySelector('#viewer-main-container')).not.toBeNull();
});
it('all-space class should be present if is not overlay mode', () => {
expect(element.querySelector('#viewer').getAttribute('class')).toEqual('all-space');
});
});
});
describe('Attribute', () => {
it('Url or fileNodeId File should be mandatory', () => {
component.showViewer = true;
component.fileNodeId = undefined;
component.urlFile = undefined;
expect(() => {
component.ngOnChanges(null);
}).toThrow();
});
it('If FileNodeId is present should not be thrown any error ', () => {
component.showViewer = true;
component.fileNodeId = 'file-node-id';
component.urlFile = undefined;
expect(() => {
component.ngOnChanges(null);
}).not.toThrow();
});
it('If urlFile is present should not be thrown any error ', () => {
component.showViewer = true;
component.fileNodeId = undefined;
expect(() => {
component.ngOnChanges(null);
}).not.toThrow();
});
it('showViewer default value should be true', () => {
expect(component.showViewer).toBe(true);
});
it('if showViewer value is false the viewer should be hide', () => {
component.showViewer = false;
fixture.detectChanges();
expect(element.querySelector('#viewer-main-container')).toBeNull();
});
});
describe('Extension Type Test', () => {
it('if extension file is a pdf the pdf viewer should be loaded', (done) => {
component.urlFile = 'base/src/assets/fake-test-file.pdf';
component.ngOnChanges(null).then(() => {
fixture.detectChanges();
expect(element.querySelector('pdf-viewer')).not.toBeNull();
done();
});
});
it('if extension file is a image the img viewer should be loaded', (done) => {
component.urlFile = 'fake-url-file.png';
component.ngOnChanges(null).then(() => {
fixture.detectChanges();
expect(element.querySelector('#viewer-image')).not.toBeNull();
done();
});
});
it('if extension file is a video the the media player should be loaded', (done) => {
component.urlFile = 'fake-url-file.mp4';
component.ngOnChanges(null).then(() => {
fixture.detectChanges();
expect(element.querySelector('media-player')).not.toBeNull();
done();
});
});
it('if extension file is a not supported the not supported div should be loaded', (done) => {
component.urlFile = 'fake-url-file.unsupported';
component.ngOnChanges(null).then(() => {
fixture.detectChanges();
expect(element.querySelector('not-supported-format')).not.toBeNull();
done();
});
});
});
describe('MimeType handling', () => {
it('should display a PDF file identified by mimetype when the filename has no extension', (done) => {
component.urlFile = 'content';
component.mimeType = 'application/pdf';
component.ngOnChanges(null).then(() => {
fixture.detectChanges();
expect(element.querySelector('pdf-viewer')).not.toBeNull();
done();
});
});
it('should display a PDF file identified by mimetype when the file extension is wrong', (done) => {
component.urlFile = 'content.bin';
component.mimeType = 'application/pdf';
component.ngOnChanges(null).then(() => {
fixture.detectChanges();
expect(element.querySelector('pdf-viewer')).not.toBeNull();
done();
});
});
it('should display an image file identified by mimetype when the filename has no extension', (done) => {
component.urlFile = 'content';
component.mimeType = 'image/png';
component.ngOnChanges(null).then(() => {
fixture.detectChanges();
expect(element.querySelector('#viewer-image')).not.toBeNull();
done();
});
});
it('should display a image file identified by mimetype when the file extension is wrong', (done) => {
component.urlFile = 'content.bin';
component.mimeType = 'image/png';
component.ngOnChanges(null).then(() => {
fixture.detectChanges();
expect(element.querySelector('#viewer-image')).not.toBeNull();
done();
});
});
it('should display the media player if the file identified by mimetype is a media when the filename has wrong extension', (done) => {
component.urlFile = 'content.bin';
component.mimeType = 'video/mp4';
component.ngOnChanges(null).then(() => {
fixture.detectChanges();
expect(element.querySelector('media-player')).not.toBeNull();
done();
});
});
it('should display the media player if the file identified by mimetype is a media when the filename has no extension', (done) => {
component.urlFile = 'content';
component.mimeType = 'video/mp4';
component.ngOnChanges(null).then(() => {
fixture.detectChanges();
expect(element.querySelector('media-player')).not.toBeNull();
done();
});
});
it('should not display the media player if the file identified by mimetype is a media but with not supported extension', (done) => {
component.urlFile = 'content';
component.mimeType = 'video/avi';
component.ngOnChanges(null).then(() => {
fixture.detectChanges();
expect(element.querySelector('media-player')).toBeNull();
done();
});
});
});
describe('Events', () => {
it('if extension change exextensionChange event should be fired ', (done) => {
component.extensionChange.subscribe((fileExtension) => {
expect(fileExtension).toEqual('png');
done();
});
component.urlFile = 'fake-url-file.png';
component.ngOnChanges(null);
});
});
});

View File

@@ -0,0 +1,337 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, ElementRef, Input, Output, HostListener, EventEmitter, Inject, TemplateRef } from '@angular/core';
import { DOCUMENT } from '@angular/platform-browser';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { AlfrescoApiService, LogService } from 'ng2-alfresco-core';
@Component({
moduleId: module.id,
selector: 'alfresco-viewer',
templateUrl: './viewer.component.html',
styleUrls: ['./viewer.component.css']
})
export class ViewerComponent {
@Input()
urlFile: string = '';
@Input()
fileNodeId: string = null;
@Input()
overlayMode: boolean = false;
@Input()
showViewer: boolean = true;
@Input()
showToolbar: boolean = true;
@Output()
showViewerChange: EventEmitter<boolean> = new EventEmitter<boolean>();
@Output()
extensionChange: EventEmitter<String> = new EventEmitter<String>();
extensionTemplates: { template: TemplateRef<any>, isVisible: boolean }[] = [];
externalExtensions: string[] = [];
urlFileContent: string;
otherMenu: any;
displayName: string;
extension: string;
mimeType: string;
loaded: boolean = false;
constructor(private apiService: AlfrescoApiService,
private element: ElementRef,
@Inject(DOCUMENT) private document,
private logService: LogService) {
}
ngOnChanges(changes) {
if (this.showViewer) {
this.hideOtherHeaderBar();
this.blockOtherScrollBar();
if (!this.urlFile && !this.fileNodeId) {
throw new Error('Attribute urlFile or fileNodeId is required');
}
return new Promise((resolve, reject) => {
let alfrescoApi = this.apiService.getInstance();
if (this.urlFile) {
let filenameFromUrl = this.getFilenameFromUrl(this.urlFile);
this.displayName = filenameFromUrl ? filenameFromUrl : '';
this.extension = this.getFileExtension(filenameFromUrl);
this.extensionChange.emit(this.extension);
this.urlFileContent = this.urlFile;
resolve();
} else if (this.fileNodeId) {
alfrescoApi.nodes.getNodeInfo(this.fileNodeId).then((data: MinimalNodeEntryEntity) => {
this.mimeType = data.content.mimeType;
this.displayName = data.name;
this.urlFileContent = alfrescoApi.content.getContentUrl(data.id);
this.extension = this.getFileExtension(data.name);
this.extensionChange.emit(this.extension);
this.loaded = true;
resolve();
}, function (error) {
reject(error);
this.logService.error('This node does not exist');
});
}
});
}
}
/**
* close the viewer
*/
close() {
this.unblockOtherScrollBar();
if (this.otherMenu) {
this.otherMenu.hidden = false;
}
this.cleanup();
this.showViewer = false;
this.showViewerChange.emit(this.showViewer);
}
/**
* cleanup before the close
*/
cleanup() {
this.urlFileContent = '';
this.displayName = '';
this.fileNodeId = null;
this.loaded = false;
this.extension = null;
this.mimeType = null;
}
ngOnDestroy() {
this.cleanup();
}
/**
* get File name from url
*
* @param {string} url - url file
* @returns {string} name file
*/
getFilenameFromUrl(url: string) {
let anchor = url.indexOf('#');
let query = url.indexOf('?');
let end = Math.min(
anchor > 0 ? anchor : url.length,
query > 0 ? query : url.length);
return url.substring(url.lastIndexOf('/', end) + 1, end);
}
/**
* Get the token from the local storage
*
* @param {string} fileName - file name
* @returns {string} file name extension
*/
private getFileExtension(fileName: string) {
return fileName.split('.').pop().toLowerCase();
}
/**
* Check if the content is an image through the extension or mime type
*
* @returns {boolean}
*/
private isImage() {
return this.isImageExtension() || this.isImageMimeType();
}
/**
* Check if the content is a media through the extension or mime type
*
* @returns {boolean}
*/
private isMedia() {
return this.isMediaExtension(this.extension) || this.isMediaMimeType();
}
/**
* check if the current file is a supported image extension
*
* @returns {boolean}
*/
private isImageExtension() {
return this.extension === 'png' || this.extension === 'jpg' ||
this.extension === 'jpeg' || this.extension === 'gif' || this.extension === 'bmp';
}
/**
* check if the current file has an image-based mimetype
*
* @returns {boolean}
*/
private isMediaMimeType() {
let mimeExtension;
if (this.mimeType && this.mimeType.indexOf('/')) {
mimeExtension = this.mimeType.substr(this.mimeType.indexOf('/') + 1, this.mimeType.length);
}
return this.mimeType && this.mimeType.indexOf('video/') === 0 && this.isMediaExtension(mimeExtension);
}
/**
* check if the current file is a supported media extension
* @param {string} extension
*
* @returns {boolean}
*/
private isMediaExtension(extension: string) {
return extension === 'mp4' || extension === 'WebM' || extension === 'Ogg';
}
/**
* check if the current file has an image-based mimetype
*
* @returns {boolean}
*/
private isImageMimeType() {
return this.mimeType && this.mimeType.indexOf('image/') === 0;
}
/**
* check if the current file is a supported pdf extension
*
* @returns {boolean}
*/
private isPdf() {
return this.extension === 'pdf' || this.mimeType === 'application/pdf';
}
/**
* check if the current file is a supported extension
*
* @returns {boolean}
*/
supportedExtension() {
return this.isImage() || this.isPdf() || this.isMedia() || this.isExternalSupportedExtension();
}
/**
* Check if the file is compatible with one of the extension
*
* @returns {boolean}
*/
isExternalSupportedExtension() {
let externalType: string;
if (this.externalExtensions && (this.externalExtensions instanceof Array)) {
externalType = this.externalExtensions.find((externalExtension) => {
return externalExtension.toLowerCase() === this.extension;
});
}
return !!externalType;
}
/**
* Litener Keyboard Event
* @param {KeyboardEvent} event
*/
@HostListener('document:keydown', ['$event'])
handleKeyboardEvent(event: KeyboardEvent) {
let key = event.keyCode;
if (key === 27 && this.overlayMode) { // esc
this.close();
}
}
/**
* Check if in the document there are scrollable main area and disable it
*
* @returns {boolean}
*/
private blockOtherScrollBar() {
let mainElements: any = document.getElementsByTagName('main');
for (let i = 0; i < mainElements.length; i++) {
mainElements[i].style.overflow = 'hidden';
}
}
/**
* Check if in the document there are scrollable main area and renable it
*
* @returns {boolean}
*/
private unblockOtherScrollBar() {
let mainElements: any = document.getElementsByTagName('main');
for (let i = 0; i < mainElements.length; i++) {
mainElements[i].style.overflow = '';
}
}
/**
* Check if the viewer is used inside and header element
*
* @returns {boolean}
*/
private isParentElementHeaderBar() {
return !!this.closestElement(this.element.nativeElement, 'header');
}
/**
* Check if the viewer is used inside and header element
* @param {HTMLElement} elelemnt
* @param {string} nodeName
* @returns {HTMLElement}
*/
private closestElement(element: HTMLElement, nodeName: string) {
let parent = element.parentElement;
if (parent) {
if (parent.nodeName.toLowerCase() === nodeName) {
return parent;
} else {
return this.closestElement(parent, nodeName);
}
} else {
return null;
}
}
/**
* Hide the other possible menu in the application
*/
private hideOtherHeaderBar() {
if (this.overlayMode && !this.isParentElementHeaderBar()) {
this.otherMenu = document.querySelector('header');
if (this.otherMenu) {
this.otherMenu.hidden = true;
}
}
}
/**
* return true if the data about the node in the ecm are loaded
*/
isLoaded() {
return this.fileNodeId ? this.loaded : true;
}
}