[ADF-1623] routing integration for Viewer (#2404)

* routed viewer (demo app)

* toolbar support

* app menu component for demo shell

* navigate back button

* fix unit tests

* improve viewer type detection and rendering

* download button

* automatic pdf rendition, spinners, ui tweaks

* border for pdf pages

* scroll top support

* docs update

* info drawer placeholder
This commit is contained in:
Denys Vuika
2017-10-03 11:57:23 +01:00
committed by Maurizio Vitale
parent 55a999b492
commit 93a87af4a5
40 changed files with 674 additions and 846 deletions

View File

@@ -23,12 +23,13 @@ import { MaterialModule } from './src/material.module';
export { ViewerComponent } from './src/components/viewer.component';
import { ImgViewerComponent } from './src/components/imgViewer.component';
import { MediaPlayerComponent } from './src/components/mediaPlayer.component';
import { NotSupportedFormatComponent } from './src/components/notSupportedFormat.component';
import { PdfViewerComponent } from './src/components/pdfViewer.component';
import { TxtViewerComponent } from './src/components/txtViewer.component';
import { UnknownFormatComponent } from './src/components/unknown-format/unknown-format.component';
import { PdfViewComponent } from './src/components/viewer-dialog/pdf-view/pdf-view.component';
import { ViewerDialogComponent } from './src/components/viewer-dialog/viewer-dialog.component';
import { ViewerComponent } from './src/components/viewer.component';
import { ExtensionViewerDirective } from './src/directives/extension-viewer.directive';
import { RenderingQueueServices } from './src/services/rendering-queue.services';
@@ -38,33 +39,33 @@ export { ViewerDialogComponent } from './src/components/viewer-dialog/viewer-dia
export { ViewerDialogSettings } from './src/components/viewer-dialog/viewer-dialog.settings';
export { ViewerService } from './src/services/viewer.service';
export const VIEWER_DIRECTIVES: any[] = [
ViewerComponent,
ImgViewerComponent,
TxtViewerComponent,
MediaPlayerComponent,
NotSupportedFormatComponent,
PdfViewerComponent,
ExtensionViewerDirective,
ViewerDialogComponent,
PdfViewComponent
];
export function declarations() {
return [
ViewerComponent,
ImgViewerComponent,
TxtViewerComponent,
MediaPlayerComponent,
PdfViewerComponent,
ExtensionViewerDirective,
ViewerDialogComponent,
PdfViewComponent,
UnknownFormatComponent
];
}
@NgModule({
imports: [
CoreModule,
MaterialModule
],
declarations: [
...VIEWER_DIRECTIVES
],
declarations: declarations(),
providers: [
RenderingQueueServices,
ViewerService
],
exports: [
MaterialModule,
...VIEWER_DIRECTIVES
...declarations()
],
entryComponents: [
ViewerDialogComponent

View File

@@ -7,6 +7,7 @@
justify-content: center;
height: 90vh;
img {
width: 100%;
object-fit: contain;
}
}

View File

@@ -1,47 +0,0 @@
<md-card *ngIf="!isConversionFinished">
<md-card-title>
Unknown format
</md-card-title>
<md-card-content>
<h4>File '<span>{{nameFile}}</span>' is of an unsupported format</h4>
<md-progress-bar
*ngIf="isConversionStarted"
mode="indeterminate"
data-automation-id="viewer-conversion-spinner">
</md-progress-bar>
</md-card-content>
<md-card-actions>
<button
md-button
data-automation-id="viewer-download-button"
(click)="download()">
<md-icon>cloud_download</md-icon>
Download
</button>
<button
md-button
*ngIf="convertible"
[disabled]="isConversionStarted"
data-automation-id="viewer-convert-button"
(click)="convertToPdf()">
<md-icon>insert_drive_file</md-icon>
Convert to PDF
</button>
<button
md-button
*ngIf="displayable"
data-automation-id="viewer-display-button"
(click)="showPDF()">
<md-icon>insert_drive_file</md-icon>
Show PDF
</button>
</md-card-actions>
</md-card>
<adf-pdf-viewer
*ngIf="isConversionFinished"
[showToolbar]="showToolbar"
[urlFile]="renditionUrl"
[nameFile]="nameFile"
data-automation-id="pdf-rendition-viewer">
</adf-pdf-viewer>

View File

@@ -1,5 +0,0 @@
.adf-not-supported-format {
.mat-card {
max-width: 400px;
}
}

View File

@@ -1,271 +0,0 @@
/*!
* @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 { DebugElement } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { ContentService, CoreModule, RenditionsService } from 'ng2-alfresco-core';
import { Subject } from 'rxjs/Subject';
import { MaterialModule } from './../material.module';
import { NotSupportedFormatComponent } from './notSupportedFormat.component';
import { PdfViewerComponent } from './pdfViewer.component';
interface RenditionResponse {
entry: {
status: string
};
}
describe('NotSupportedFormatComponent', () => {
const nodeId = 'not-supported-node-id';
let component: NotSupportedFormatComponent;
let service: ContentService;
let fixture: ComponentFixture<NotSupportedFormatComponent>;
let debug: DebugElement;
let element: HTMLElement;
let renditionsService: RenditionsService;
let renditionSubject: Subject<RenditionResponse>;
let conversionSubject: Subject<any>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
CoreModule,
MaterialModule
],
declarations: [
NotSupportedFormatComponent,
PdfViewerComponent
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(NotSupportedFormatComponent);
service = fixture.debugElement.injector.get(ContentService);
debug = fixture.debugElement;
element = fixture.nativeElement;
component = fixture.componentInstance;
component.nodeId = nodeId;
renditionSubject = new Subject<RenditionResponse>();
conversionSubject = new Subject<any>();
renditionsService = TestBed.get(RenditionsService);
spyOn(renditionsService, 'getRendition').and.returnValue(renditionSubject);
spyOn(renditionsService, 'convert').and.returnValue(conversionSubject);
});
describe('View', () => {
beforeEach(() => {
fixture.detectChanges();
});
it('should display the Download button', () => {
expect(element.querySelector('[data-automation-id="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');
});
it('should NOT show loading spinner by default', () => {
expect(element.querySelector('[data-automation-id="viewer-conversion-spinner"]')).toBeNull('Conversion spinner should NOT be shown by default');
});
});
describe('Convertibility to pdf', () => {
it('should not show the "Convert to PDF" button by default', () => {
fixture.detectChanges();
expect(element.querySelector('[data-automation-id="viewer-convert-button"]')).toBeNull();
});
it('should be checked on ngInit', () => {
fixture.detectChanges();
expect(renditionsService.getRendition).toHaveBeenCalledWith(nodeId, 'pdf');
});
it('should NOT be checked on ngInit if nodeId is not set', () => {
component.nodeId = null;
fixture.detectChanges();
expect(renditionsService.getRendition).not.toHaveBeenCalled();
});
it('should show the "Convert to PDF" button if the node is convertible', async(() => {
fixture.detectChanges();
renditionSubject.next({ entry: { status: 'NOT_CREATED' } });
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(element.querySelector('[data-automation-id="viewer-convert-button"]')).not.toBeNull();
});
}));
it('should NOT show the "Convert to PDF" button if the node is NOT convertible', async(() => {
component.convertible = true;
fixture.detectChanges();
renditionSubject.error(new Error('Mocked error'));
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(element.querySelector('[data-automation-id="viewer-convert-button"]')).toBeNull();
});
}));
it('should NOT show the "Convert to PDF" button if the node is already converted', async(() => {
renditionSubject.next({ entry: { status: 'CREATED' } });
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(element.querySelector('[data-automation-id="viewer-convert-button"]')).toBeNull();
});
}));
it('should start the conversion when clicking on the "Convert to PDF" button', () => {
component.convertible = true;
fixture.detectChanges();
const convertButton = debug.query(By.css('[data-automation-id="viewer-convert-button"]'));
convertButton.triggerEventHandler('click', null);
fixture.detectChanges();
const conversionSpinner = debug.query(By.css('[data-automation-id="viewer-conversion-spinner"]'));
expect(renditionsService.convert).toHaveBeenCalled();
expect(conversionSpinner).not.toBeNull();
});
it('should remove the spinner if an error happens during conversion', () => {
component.convertToPdf();
conversionSubject.error('whatever');
fixture.detectChanges();
const conversionSpinner = debug.query(By.css('[data-automation-id="viewer-conversion-spinner"]'));
expect(conversionSpinner).toBeNull();
});
it('should remove the spinner and show the pdf if conversion has finished', () => {
component.convertToPdf();
conversionSubject.complete();
fixture.detectChanges();
const conversionSpinner = debug.query(By.css('[data-automation-id="viewer-conversion-spinner"]'));
const pdfRenditionViewer = debug.query(By.css('[data-automation-id="pdf-rendition-viewer"]'));
expect(conversionSpinner).toBeNull();
expect(pdfRenditionViewer).not.toBeNull();
});
it('should unsubscribe from the conversion subscription on ngOnDestroy', () => {
component.convertToPdf();
component.ngOnDestroy();
conversionSubject.complete();
fixture.detectChanges();
const pdfRenditionViewer = debug.query(By.css('[data-automation-id="pdf-rendition-viewer"]'));
expect(pdfRenditionViewer).toBeNull();
});
it('should not throw error on ngOnDestroy if the conversion hasn\'t started at all' , () => {
const callNgOnDestroy = () => {
component.ngOnDestroy();
};
expect(callNgOnDestroy).not.toThrowError();
});
});
describe('User Interaction', () => {
beforeEach(() => {
fixture.detectChanges();
});
describe('Download', () => {
it('should call download method if Click on Download button', () => {
spyOn(window, 'open');
component.urlFile = 'test';
let downloadButton: any = element.querySelector('[data-automation-id="viewer-download-button"]');
downloadButton.click();
expect(window.open).toHaveBeenCalled();
});
it('should call content service download method if Click on Download button', () => {
spyOn(service, 'downloadBlob');
component.blobFile = new Blob();
let downloadButton: any = element.querySelector('[data-automation-id="viewer-download-button"]');
downloadButton.click();
expect(service.downloadBlob).toHaveBeenCalled();
});
});
describe('Conversion', () => {
function clickOnConvertButton() {
renditionSubject.next({ entry: { status: 'NOT_CREATED' } });
fixture.detectChanges();
let convertButton: any = element.querySelector('[data-automation-id="viewer-convert-button"]');
convertButton.click();
fixture.detectChanges();
}
it('should show loading spinner and disable the "Convert to PDF button" after the button was clicked', () => {
clickOnConvertButton();
let convertButton: any = element.querySelector('[data-automation-id="viewer-convert-button"]');
expect(element.querySelector('[data-automation-id="viewer-conversion-spinner"]')).not.toBeNull('Conversion spinner should be shown');
expect(convertButton.disabled).toBe(true);
});
it('should re-enable the "Convert to PDF button" and hide spinner after unsuccessful conversion and hide loading spinner', () => {
clickOnConvertButton();
conversionSubject.error(new Error());
fixture.detectChanges();
let convertButton: any = element.querySelector('[data-automation-id="viewer-convert-button"]');
expect(element.querySelector('[data-automation-id="viewer-conversion-spinner"]')).toBeNull('Conversion spinner should be shown');
expect(convertButton.disabled).toBe(false);
});
it('should show the pdf rendition after successful conversion', () => {
clickOnConvertButton();
conversionSubject.next();
conversionSubject.complete();
fixture.detectChanges();
fixture.detectChanges();
expect(element.querySelector('[data-automation-id="pdf-rendition-viewer"]')).not.toBeNull('Pdf rendition should be shown.');
});
});
});
});

View File

@@ -1,133 +0,0 @@
/*!
* @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, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { ContentService, RenditionsService } from 'ng2-alfresco-core';
const DEFAULT_CONVERSION_ENCODING = 'pdf';
@Component({
selector: 'adf-not-supported-format',
templateUrl: './notSupportedFormat.component.html',
styleUrls: ['./notSupportedFormat.component.scss'],
host: { 'class': 'adf-not-supported-format' },
encapsulation: ViewEncapsulation.None
})
export class NotSupportedFormatComponent implements OnInit, OnDestroy {
@Input()
nameFile: string;
@Input()
urlFile: string;
@Input()
blobFile: Blob;
@Input()
nodeId: string|null = null;
@Input()
showToolbar: boolean = true;
convertible: boolean = false;
displayable: boolean = false;
isConversionStarted: boolean = false;
isConversionFinished: boolean = false;
renditionUrl: string|null = null;
conversionsubscription: any = null;
constructor(
private contentService: ContentService,
private renditionsService: RenditionsService) {}
/**
* Checks for available renditions if the nodeId is present
*/
ngOnInit() {
if (this.nodeId) {
this.checkRendition();
}
}
/**
* Download file opening it in a new window
*/
download() {
if (this.urlFile) {
window.open(this.urlFile);
} else {
this.contentService.downloadBlob(this.blobFile, this.nameFile);
}
}
/**
* Update component's button according to the given rendition's availability
*
* @param {string} encoding - the rendition id
*/
checkRendition(encoding: string = DEFAULT_CONVERSION_ENCODING): void {
this.renditionsService.getRendition(this.nodeId, encoding)
.subscribe(
(response) => {
const status = response.entry.status.toString();
if (status === 'NOT_CREATED') {
this.convertible = true;
this.displayable = false;
} else if (status === 'CREATED') {
this.convertible = false;
this.displayable = true;
}
},
() => {
this.convertible = false;
this.displayable = false;
}
);
}
/**
* Set the component to loading state and send the conversion starting signal to parent component
*/
convertToPdf(): void {
this.isConversionStarted = true;
this.conversionsubscription = this.renditionsService.convert(this.nodeId, DEFAULT_CONVERSION_ENCODING)
.subscribe({
error: (error) => { this.isConversionStarted = false; },
complete: () => { this.showPDF(); }
});
}
/**
* Show the PDF rendition of the node
*/
showPDF(): void {
this.renditionUrl = this.renditionsService.getRenditionUrl(this.nodeId, DEFAULT_CONVERSION_ENCODING);
this.isConversionStarted = false;
this.isConversionFinished = true;
}
/**
* Kills the subscription polling if it has been started
*/
ngOnDestroy(): void {
if (this.isConversionStarted) {
this.conversionsubscription.unsubscribe();
}
}
}

View File

@@ -8,6 +8,7 @@
overflow: hidden;
opacity: 0.2;
line-height: 1.0;
border: 1px solid gray;
& > div {
color: transparent;

View File

@@ -0,0 +1,6 @@
<div class="adf-viewer__unknown-format-view">
<div>
<md-icon class="icon">wifi_tethering</md-icon>
<div class="label">Document preview could not be loaded.</div>
</div>
</div>

View File

@@ -0,0 +1,8 @@
.adf-viewer__unknown-format-view {
height: 90vh;
text-align: center;
display: flex;
flex: 1;
flex-direction: column;
justify-content: center;
}

View File

@@ -0,0 +1,26 @@
/*!
* @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 } from '@angular/core';
@Component({
selector: 'adf-viewer-unknown-format',
templateUrl: 'unknown-format.component.html',
styleUrls: ['unknown-format.component.scss']
})
export class UnknownFormatComponent {
}

View File

@@ -4,80 +4,151 @@
[class.adf-viewer-inline-container]="!overlayMode">
<div class="adf-viewer-content">
<ng-container *ngIf="overlayMode">
<ng-container *ngIf="showToolbar">
<adf-toolbar color="default" class="adf-viewer-toolbar">
<adf-toolbar-title>
<span class="adf-viewer-filename">{{ displayName }}</span>
<button *ngIf="allowGoBack"
class="adf-viewer-close-button"
md-icon-button
mdTooltip="Back"
(click)="onBackButtonClick()">
<md-icon>arrow_back</md-icon>
</button>
<img [src]="mimeType | adfMimeTypeIcon">
<span>{{ displayName }}</span>
</adf-toolbar-title>
<button
md-icon-button
class="adf-viewer-close-button"
mdTooltip="Close and go back"
mdTooltipPosition="before"
(click)="close()"
aria-label="Close">
<md-icon>close</md-icon>
<ng-container *ngIf="allowOpenWith">
<button md-button [mdMenuTriggerFor]="mnuOpenWith">
<span>Open with</span>
<md-icon>arrow_drop_down</md-icon>
</button>
<md-menu #mnuOpenWith="mdMenu" [overlapTrigger]="false">
<button md-menu-item>
<md-icon>dialpad</md-icon>
<span>Option 1</span>
</button>
<button md-menu-item disabled>
<md-icon>voicemail</md-icon>
<span>Option 2</span>
</button>
<button md-menu-item>
<md-icon>notifications_off</md-icon>
<span>Option 3</span>
</button>
</md-menu>
</ng-container>
<adf-toolbar-divider></adf-toolbar-divider>
<button *ngIf="allowDownload" md-icon-button mdTooltip="Download" (click)="download()">
<md-icon>file_download</md-icon>
</button>
<button *ngIf="allowPrint" md-icon-button mdTooltip="Print">
<md-icon>print</md-icon>
</button>
<button *ngIf="allowShare" md-icon-button mdTooltip="Share">
<md-icon>share</md-icon>
</button>
<button md-icon-button [mdMenuTriggerFor]="mnuMoreActions" mdTooltip="More actions">
<md-icon>more_vert</md-icon>
</button>
<md-menu #mnuMoreActions="mdMenu">
<button md-menu-item>
<md-icon>dialpad</md-icon>
<span>Action One</span>
</button>
<button md-menu-item disabled>
<md-icon>voicemail</md-icon>
<span>Action Two</span>
</button>
<button md-menu-item>
<md-icon>notifications_off</md-icon>
<span>Action Three</span>
</button>
</md-menu>
<ng-container *ngIf="allowInfoDrawer">
<adf-toolbar-divider></adf-toolbar-divider>
<button md-icon-button mdTooltip="Info"
[color]="showInfoDrawer ? 'accent' : 'default'"
(click)="showInfoDrawer = !showInfoDrawer">
<md-icon>info_outline</md-icon>
</button>
</ng-container>
</adf-toolbar>
</ng-container>
<div class="adf-viewer-layout">
<ng-container *ngIf="isLoading">
<div class="adf-viewer__loading-screen">
<h2>Loading</h2>
<div>
<md-spinner></md-spinner>
</div>
</div>
</ng-container>
<div *ngIf="!isLoading" class="adf-viewer-layout">
<div class="adf-viewer-layout-content">
<div *ngIf="isLoaded()">
<div class="adf-viewer-content-container">
<div>
<div class="adf-viewer-content-container" [ngSwitch]="viewerType">
<ng-container *ngIf="isPdf()">
<adf-pdf-viewer
[showToolbar]="showToolbar"
[blobFile]="blobFile"
[urlFile]="urlFileContent"
[nameFile]="displayName">
</adf-pdf-viewer>
<ng-container *ngSwitchCase="'pdf'">
<adf-pdf-viewer [blobFile]="blobFile" [urlFile]="urlFileContent" [nameFile]="displayName"></adf-pdf-viewer>
</ng-container>
<ng-container *ngIf="isImage()">
<adf-img-viewer
[urlFile]="urlFileContent"
[nameFile]="displayName"
[blobFile]="blobFile">
</adf-img-viewer>
<ng-container *ngSwitchCase="'image'">
<adf-img-viewer [urlFile]="urlFileContent" [nameFile]="displayName" [blobFile]="blobFile"></adf-img-viewer>
</ng-container>
<ng-container *ngIf="isMedia()">
<adf-media-player
[urlFile]="urlFileContent"
[mimeType]="mimeType"
[blobFile]="blobFile"
[nameFile]="displayName">
</adf-media-player>
<ng-container *ngSwitchCase="'media'">
<adf-media-player [urlFile]="urlFileContent" [mimeType]="mimeType" [blobFile]="blobFile" [nameFile]="displayName"></adf-media-player>
</ng-container>
<ng-container *ngIf="isText() && !isExternalSupportedExtension()">
<ng-container *ngSwitchCase="'text'">
<adf-txt-viewer [urlFile]="urlFileContent" [blobFile]="blobFile"></adf-txt-viewer>
</ng-container>
<span *ngFor="let extensionTemplate of extensionTemplates">
<ng-template
*ngIf="extensionTemplate.isVisible"
[ngTemplateOutlet]="extensionTemplate.template"
[ngOutletContext]="{ urlFileContent: urlFileContent, extension:extension }">
</ng-template>
</span>
<ng-container *ngSwitchCase="'custom'">
<span *ngFor="let extensionTemplate of extensionTemplates">
<ng-template
*ngIf="extensionTemplate.isVisible"
[ngTemplateOutlet]="extensionTemplate.template"
[ngOutletContext]="{ urlFileContent: urlFileContent, extension:extension }">
</ng-template>
</span>
</ng-container>
<div *ngIf="!supportedExtension()" class="adf-viewer-unknown-content">
<adf-not-supported-format
*ngIf="!extensionTemplate"
[urlFile]="urlFileContent"
[blobFile]="blobFile"
[nameFile]="displayName"
[showToolbar]="showToolbar"
[nodeId]="fileNodeId">
</adf-not-supported-format>
</div>
<ng-container *ngSwitchDefault>
<adf-viewer-unknown-format></adf-viewer-unknown-format>
</ng-container>
</div>
</div>
</div>
<ng-container *ngIf="showInfoDrawer">
<div class="adf-viewer__info-drawer">
<md-tab-group md-stretch-tabs>
<md-tab label="Details">
<md-card>
DETAILS
</md-card>
</md-tab>
<md-tab label="Activity">
<md-card>
Activity
</md-card>
</md-tab>
</md-tab-group>
</div>
</ng-container>
</div>
</div>
</div>

View File

@@ -1,4 +1,4 @@
$adf-viewer-background-color: #515151;
$adf-viewer-background-color: #f5f5f5;
@mixin full-screen() {
width: 100%;
@@ -33,7 +33,7 @@ $adf-viewer-background-color: #515151;
@include full-screen();
display: flex;
flex-direction: column;
flex-direction: row;
overflow-y: auto;
overflow-x: hidden;
position: relative;
@@ -41,6 +41,7 @@ $adf-viewer-background-color: #515151;
.adf-viewer-content {
@include full-screen();
flex: 1;
}
}
@@ -66,4 +67,33 @@ $adf-viewer-background-color: #515151;
align-items: center;
display: flex;
}
&__loading-screen {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
height: 85vh;
.md-spinner {
margin: 0 auto;
}
}
&__info-drawer {
width: 350px;
display: block;
padding: 8px 0;
background-color: #fafafa;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.27);
border-left: 1px solid rgba(0, 0, 0, 0.07);
.mat-tab-label {
text-transform: uppercase;
}
.mat-card {
margin: 6px;
}
}
}

View File

@@ -15,6 +15,8 @@
* limitations under the License.
*/
import { Location } from '@angular/common';
import { SpyLocation } from '@angular/common/testing';
import { DebugElement } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
@@ -25,9 +27,9 @@ import { EventMock } from '../assets/event.mock';
import { RenderingQueueServices } from '../services/rendering-queue.services';
import { ImgViewerComponent } from './imgViewer.component';
import { MediaPlayerComponent } from './mediaPlayer.component';
import { NotSupportedFormatComponent } from './notSupportedFormat.component';
import { PdfViewerComponent } from './pdfViewer.component';
import { TxtViewerComponent } from './txtViewer.component';
import { UnknownFormatComponent } from './unknown-format/unknown-format.component';
import { ViewerComponent } from './viewer.component';
declare let jasmine: any;
@@ -49,12 +51,13 @@ describe('ViewerComponent', () => {
ViewerComponent,
PdfViewerComponent,
TxtViewerComponent,
NotSupportedFormatComponent,
MediaPlayerComponent,
ImgViewerComponent
ImgViewerComponent,
UnknownFormatComponent
],
providers: [
RenderingQueueServices
RenderingQueueServices,
{ provide: Location, useClass: SpyLocation }
]
}).compileComponents();
}));
@@ -128,10 +131,6 @@ describe('ViewerComponent', () => {
expect(element.querySelector('header')).toBeNull();
});
it('should Close button be not present if is not overlay mode', () => {
expect(element.querySelector('.adf-viewer-close-button')).toBeNull();
});
it('should Esc button not hide the viewer if is not overlay mode', () => {
EventMock.keyDown(27);
fixture.detectChanges();
@@ -182,7 +181,7 @@ describe('ViewerComponent', () => {
});
});
describe('Exteznsion Type Test', () => {
describe('Extension Type Test', () => {
it('should extension file pdf be loaded', (done) => {
component.urlFile = 'base/src/assets/fake-test-file.pdf';
@@ -243,13 +242,13 @@ describe('ViewerComponent', () => {
});
});
it('should the not supported div be loaded if the file is a not supported extension', (done) => {
it('should display [unknown format] for unsupported extensions', (done) => {
component.urlFile = 'fake-url-file.unsupported';
component.mimeType = '';
component.ngOnChanges(null).then(() => {
fixture.detectChanges();
expect(element.querySelector('adf-not-supported-format')).not.toBeNull();
expect(element.querySelector('adf-viewer-unknown-format')).toBeDefined();
done();
});
});
@@ -333,17 +332,6 @@ describe('ViewerComponent', () => {
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('adf-media-player')).toBeNull();
done();
});
});
});
describe('Events', () => {

View File

@@ -15,9 +15,10 @@
* limitations under the License.
*/
import { Location } from '@angular/common';
import { Component, EventEmitter, HostListener, Input, OnChanges, OnDestroy, Output, TemplateRef, ViewEncapsulation } from '@angular/core';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { AlfrescoApiService, LogService } from 'ng2-alfresco-core';
import { AlfrescoApiService, BaseEvent, LogService, RenditionsService } from 'ng2-alfresco-core';
@Component({
selector: 'adf-viewer, alfresco-viewer',
@@ -44,30 +45,70 @@ export class ViewerComponent implements OnDestroy, OnChanges {
showViewer: boolean = true;
@Input()
showToolbar: boolean = true;
showToolbar = true;
@Input()
displayName: string;
displayName: string = 'Unknown';
@Input()
allowGoBack = true;
@Input()
allowOpenWith = true;
@Input()
allowDownload = true;
@Input()
allowPrint = true;
@Input()
allowShare = true;
@Input()
allowInfoDrawer = true;
@Input()
showInfoDrawer = false;
@Output()
showViewerChange: EventEmitter<boolean> = new EventEmitter<boolean>();
goBack = new EventEmitter<BaseEvent<any>>();
@Output()
extensionChange: EventEmitter<String> = new EventEmitter<String>();
showViewerChange = new EventEmitter<boolean>();
@Output()
extensionChange = new EventEmitter<string>();
viewerType: string = 'unknown';
downloadUrl: string = null;
fileName: string = 'document';
isLoading: boolean = false;
extensionTemplates: { template: TemplateRef<any>, isVisible: boolean }[] = [];
externalExtensions: string[] = [];
urlFileContent: string;
otherMenu: any;
extension: string;
mimeType: string;
loaded: boolean = false;
constructor(private apiService: AlfrescoApiService,
private logService: LogService) {
}
private extensions = {
image: ['png', 'jpg', 'jpeg', 'gif', 'bpm'],
media: ['wav', 'mp4', 'mp3', 'webm', 'ogg'],
text: ['txt', 'xml', 'js', 'html'],
pdf: ['pdf']
};
private mimeTypes = [
{ mimeType: 'application/x-javascript', type: 'text' },
{ mimeType: 'application/pdf', type: 'pdf' }
];
constructor(
private apiService: AlfrescoApiService,
private logService: LogService,
private location: Location,
private renditionService: RenditionsService) {}
ngOnChanges(changes) {
if (this.showViewer) {
@@ -77,28 +118,66 @@ export class ViewerComponent implements OnDestroy, OnChanges {
return new Promise((resolve, reject) => {
if (this.blobFile) {
this.isLoading = true;
this.mimeType = this.blobFile.type;
this.viewerType = this.getViewerTypeByMimeType(this.mimeType);
this.allowDownload = false;
// TODO: wrap blob into the data url and allow downloading
this.extensionChange.emit(this.mimeType);
this.isLoading = false;
this.scrollTop();
resolve();
} else if (this.urlFile) {
this.isLoading = true;
let filenameFromUrl = this.getFilenameFromUrl(this.urlFile);
this.displayName = filenameFromUrl ? filenameFromUrl : '';
this.displayName = filenameFromUrl || 'Unknown';
this.extension = this.getFileExtension(filenameFromUrl);
this.extensionChange.emit(this.extension);
this.urlFileContent = this.urlFile;
this.downloadUrl = this.urlFile;
this.fileName = this.displayName;
this.viewerType = this.getViewerTypeByExtension(this.extension);
if (this.viewerType === 'unknown') {
this.viewerType = this.getViewerTypeByMimeType(this.mimeType);
}
this.extensionChange.emit(this.extension);
this.isLoading = false;
this.scrollTop();
resolve();
} else if (this.fileNodeId) {
this.isLoading = true;
this.apiService.getInstance().nodes.getNodeInfo(this.fileNodeId).then(
(data: MinimalNodeEntryEntity) => {
this.mimeType = data.content.mimeType;
this.displayName = data.name;
this.urlFileContent = this.apiService.getInstance().content.getContentUrl(data.id);
this.extension = this.getFileExtension(data.name);
this.fileName = data.name;
this.downloadUrl = this.apiService.getInstance().content.getContentUrl(data.id, true);
this.viewerType = this.getViewerTypeByExtension(this.extension);
if (this.viewerType === 'unknown') {
this.viewerType = this.getViewerTypeByMimeType(this.mimeType);
}
if (this.viewerType === 'unknown') {
this.displayAsPdf(data.id);
} else {
this.isLoading = false;
}
this.extensionChange.emit(this.extension);
this.loaded = true;
this.scrollTop();
resolve();
},
(error) => {
this.isLoading = false;
reject(error);
this.logService.error('This node does not exist');
}
@@ -108,6 +187,79 @@ export class ViewerComponent implements OnDestroy, OnChanges {
}
}
scrollTop() {
window.scrollTo(0, 1);
}
getViewerTypeByMimeType(mimeType: string) {
if (mimeType) {
mimeType = mimeType.toLowerCase();
if (mimeType.startsWith('image/')) {
return 'image';
}
if (mimeType.startsWith('text/')) {
return 'text';
}
if (mimeType.startsWith('video/')) {
return 'media';
}
if (mimeType.startsWith('audio/')) {
return 'media';
}
const registered = this.mimeTypes.find(t => t.mimeType === mimeType);
if (registered) {
return registered.type;
}
}
return 'unknown';
}
getViewerTypeByExtension(extension: string) {
if (extension) {
extension = extension.toLowerCase();
}
if (this.isCustomViewerExtension(extension)) {
return 'custom';
}
if (this.extensions.image.indexOf(extension) >= 0) {
return 'image';
}
if (this.extensions.media.indexOf(extension) >= 0) {
return 'media';
}
if (this.extensions.text.indexOf(extension) >= 0) {
return 'text';
}
if (this.extensions.pdf.indexOf(extension) >= 0) {
return 'pdf';
}
return 'unknown';
}
onBackButtonClick() {
if (this.overlayMode) {
this.close();
} else {
const event = new BaseEvent<any>();
this.goBack.next(event);
if (!event.defaultPrevented) {
this.location.back();
}
}
}
/**
* close the viewer
*/
@@ -127,7 +279,6 @@ export class ViewerComponent implements OnDestroy, OnChanges {
this.urlFileContent = '';
this.displayName = '';
this.fileNodeId = null;
this.loaded = false;
this.extension = null;
this.mimeType = null;
}
@@ -157,113 +308,19 @@ export class ViewerComponent implements OnDestroy, OnChanges {
* @param {string} fileName - file name
* @returns {string} file name extension
*/
private getFileExtension(fileName: string): string {
getFileExtension(fileName: string): string {
return fileName.split('.').pop().toLowerCase();
}
/**
* Check if the content is an image through the extension or mime type
*
* @returns {boolean}
*/
public isImage(): boolean {
return this.isImageExtension() || this.isImageMimeType();
}
isCustomViewerExtension(extension: string): boolean {
const extensions = this.externalExtensions || [];
/**
* Check if the content is a media through the extension or mime type
*
* @returns {boolean}
*/
public isMedia(): boolean {
return this.isMediaExtension(this.extension) || this.isMediaMimeType();
}
/**
* check if the current file is a supported image extension
*
* @returns {boolean}
*/
private isImageExtension(): boolean {
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(): boolean {
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.mimeType.indexOf('audio/') === 0)) && this.isMediaExtension(mimeExtension);
}
/**
* check if the current file is a supported media extension
* @param {string} extension
*
* @returns {boolean}
*/
private isMediaExtension(extension: string): boolean {
return extension === 'wav' || extension === 'mp4' || extension === 'mp3' || extension === 'WebM' || extension === 'Ogg';
}
/**
* check if the current file has an image-based mimetype
*
* @returns {boolean}
*/
private isImageMimeType(): boolean {
return this.mimeType && this.mimeType.indexOf('image/') === 0;
}
/**
* check if the current file is a supported pdf extension
*
* @returns {boolean}
*/
public isPdf(): boolean {
return this.extension === 'pdf' || this.mimeType === 'application/pdf';
}
/**
* check if the current file is a supported txt extension
*
* @returns {boolean}
*/
public isText(): boolean {
return this.extension === 'txt' || this.mimeType === 'text/txt' || this.mimeType === 'text/plain';
}
/**
* check if the current file is a supported extension
*
* @returns {boolean}
*/
supportedExtension(): boolean {
return this.isImage() || this.isPdf() || this.isMedia() || this.isText() || this.isExternalSupportedExtension();
}
/**
* Check if the file is compatible with one of the extension
*
* @returns {boolean}
*/
isExternalSupportedExtension(): boolean {
let externalType: string;
if (this.externalExtensions && (this.externalExtensions instanceof Array)) {
externalType = this.externalExtensions.find((externalExtension) => {
return externalExtension.toLowerCase() === this.extension;
});
if (extension && extensions.length > 0) {
extension = extension.toLowerCase();
return extensions.indexOf(extension) >= 0;
}
return !!externalType;
return false;
}
/**
@@ -278,12 +335,56 @@ export class ViewerComponent implements OnDestroy, OnChanges {
}
}
/**
* return true if the data about the node in the ecm are loaded
*
* @returns {boolean}
*/
isLoaded(): boolean {
return this.fileNodeId ? this.loaded : true;
download() {
if (this.allowDownload && this.downloadUrl && this.fileName) {
const link = document.createElement('a');
link.style.display = 'none';
link.download = this.fileName;
link.href = this.downloadUrl;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
}
private displayAsPdf(nodeId: string) {
this.isLoading = true;
this.renditionService.getRendition(nodeId, 'pdf').subscribe(
(response) => {
const status = response.entry.status.toString();
if (status === 'CREATED') {
this.isLoading = false;
this.showPdfRendition(nodeId);
} else if (status === 'NOT_CREATED') {
this.renditionService.convert(nodeId, 'pdf').subscribe({
complete: () => {
this.isLoading = false;
this.showPdfRendition(nodeId);
},
error: (error) => {
this.isLoading = false;
console.log(error);
}
});
} else {
this.isLoading = false;
}
},
(err) => {
this.isLoading = false;
console.log(err);
}
);
}
private showPdfRendition(nodeId: string) {
if (nodeId) {
this.viewerType = 'pdf';
this.urlFileContent = this.renditionService.getRenditionUrl(nodeId, 'pdf');
}
}
}

View File

@@ -15,6 +15,8 @@
* limitations under the License.
*/
import { Location } from '@angular/common';
import { SpyLocation } from '@angular/common/testing';
import { ElementRef } from '@angular/core';
import { Injector } from '@angular/core';
import { async, getTestBed, TestBed } from '@angular/core/testing';
@@ -28,7 +30,7 @@ export class MockElementRef extends ElementRef {
}
}
describe('ExtensionViewerComponent', () => {
describe('ExtensionViewerDirective', () => {
let injector: Injector;
let extensionViewerDirective: ExtensionViewerDirective;
let viewerComponent: ViewerComponent;
@@ -37,6 +39,7 @@ describe('ExtensionViewerComponent', () => {
TestBed.configureTestingModule({
imports: [CoreModule],
providers: [
{ provide: Location, useClass: SpyLocation },
ExtensionViewerDirective,
{provide: ElementRef, useClass: MockElementRef},
ViewerComponent