From 93a87af4a5515ffd94da6627d2a8ff64353aaf60 Mon Sep 17 00:00:00 2001 From: Denys Vuika Date: Tue, 3 Oct 2017 11:57:23 +0100 Subject: [PATCH] [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 --- demo-shell-ng2/app/app.component.html | 96 ----- demo-shell-ng2/app/app.component.ts | 13 +- demo-shell-ng2/app/app.module.ts | 6 +- demo-shell-ng2/app/app.routes.ts | 6 + .../app/components/about/about.component.html | 2 + .../app/components/about/about.component.ts | 7 +- .../activiti/activiti-demo.component.html | 2 + .../app/components/activiti/apps.view.ts | 1 + .../app-menu/app-menu.component.html | 96 +++++ .../app-menu/app-menu.component.scss} | 1 - .../components/app-menu/app-menu.component.ts | 34 ++ .../datatable/datatable-demo.component.html | 2 + .../file-view/file-view.component.html | 9 + .../file-view/file-view.component.ts | 36 ++ .../files/custom-sources.component.html | 1 + .../app/components/files/files.component.html | 36 +- .../app/components/files/files.component.ts | 23 +- .../components/form/form-demo.component.html | 2 + .../form/form-list-demo.component.ts | 1 + .../app/components/home/home.component.html | 2 + .../components/social/social.component.html | 2 + .../app/components/tag/tag.component.html | 2 + .../webscript/webscript.component.ts | 1 + docs/docassets/images/renditions.png | Bin 56771 -> 0 bytes docs/viewer.component.md | 17 +- ng2-components/ng2-alfresco-viewer/index.ts | 33 +- .../src/components/imgViewer.component.scss | 1 + .../notSupportedFormat.component.html | 47 --- .../notSupportedFormat.component.scss | 5 - .../notSupportedFormat.component.spec.ts | 271 -------------- .../notSupportedFormat.component.ts | 133 ------- .../components/pdfViewerHost.component.scss | 1 + .../unknown-format.component.html | 6 + .../unknown-format.component.scss | 8 + .../unknown-format.component.ts | 26 ++ .../src/components/viewer.component.html | 173 ++++++--- .../src/components/viewer.component.scss | 34 +- .../src/components/viewer.component.spec.ts | 32 +- .../src/components/viewer.component.ts | 347 +++++++++++------- .../extension-viewer.directive.spec.ts | 5 +- 40 files changed, 674 insertions(+), 846 deletions(-) create mode 100644 demo-shell-ng2/app/components/app-menu/app-menu.component.html rename demo-shell-ng2/app/{app.component.scss => components/app-menu/app-menu.component.scss} (99%) create mode 100644 demo-shell-ng2/app/components/app-menu/app-menu.component.ts create mode 100644 demo-shell-ng2/app/components/file-view/file-view.component.html create mode 100644 demo-shell-ng2/app/components/file-view/file-view.component.ts delete mode 100644 docs/docassets/images/renditions.png delete mode 100644 ng2-components/ng2-alfresco-viewer/src/components/notSupportedFormat.component.html delete mode 100644 ng2-components/ng2-alfresco-viewer/src/components/notSupportedFormat.component.scss delete mode 100644 ng2-components/ng2-alfresco-viewer/src/components/notSupportedFormat.component.spec.ts delete mode 100644 ng2-components/ng2-alfresco-viewer/src/components/notSupportedFormat.component.ts create mode 100644 ng2-components/ng2-alfresco-viewer/src/components/unknown-format/unknown-format.component.html create mode 100644 ng2-components/ng2-alfresco-viewer/src/components/unknown-format/unknown-format.component.scss create mode 100644 ng2-components/ng2-alfresco-viewer/src/components/unknown-format/unknown-format.component.ts diff --git a/demo-shell-ng2/app/app.component.html b/demo-shell-ng2/app/app.component.html index 6ecf0aadbc..0680b43f9c 100644 --- a/demo-shell-ng2/app/app.component.html +++ b/demo-shell-ng2/app/app.component.html @@ -1,97 +1 @@ - - - - ADF Demo Application - -
- - - - Home - Content Services - Process Services - Login - - - - - - - - - - - - - - home - Home - - - folder_open - Content Services - - - device_hub - Process Services - - - vpn_key - Login - - - extension - DL: Custom Sources - - - view_module - DataTable - - - poll - Form - - - library_books - Form List - - - file_upload - Uploader - - - extension - Webscript - - - local_offer - Tag - - - thumb_up - Social - - - settings - Settings - - - info_outline - About - - - -
- diff --git a/demo-shell-ng2/app/app.component.ts b/demo-shell-ng2/app/app.component.ts index 3ad93d556f..f1747695ce 100644 --- a/demo-shell-ng2/app/app.component.ts +++ b/demo-shell-ng2/app/app.component.ts @@ -16,33 +16,24 @@ */ import { Component, ViewEncapsulation } from '@angular/core'; -import { AlfrescoSettingsService, AlfrescoTranslationService, PageTitle, StorageService } from 'ng2-alfresco-core'; +import { AlfrescoSettingsService, PageTitle, StorageService } from 'ng2-alfresco-core'; @Component({ selector: 'adf-app', templateUrl: './app.component.html', - styleUrls: ['./app.component.scss', './theme.scss'], + styleUrls: ['./theme.scss'], encapsulation: ViewEncapsulation.None }) export class AppComponent { searchTerm: string = ''; constructor(private settingsService: AlfrescoSettingsService, - private translateService: AlfrescoTranslationService, private storage: StorageService, pageTitle: PageTitle) { this.setProvider(); pageTitle.setTitle(); } - isAPageWithHeaderBar(): boolean { - return location.pathname === '/login' || location.pathname === '/settings'; - } - - changeLanguage(lang: string) { - this.translateService.use(lang); - } - private setProvider() { if (this.storage.hasItem(`providers`)) { this.settingsService.setProviders(this.storage.getItem(`providers`)); diff --git a/demo-shell-ng2/app/app.module.ts b/demo-shell-ng2/app/app.module.ts index e01a6ca653..fcafb18ef4 100644 --- a/demo-shell-ng2/app/app.module.ts +++ b/demo-shell-ng2/app/app.module.ts @@ -41,6 +41,8 @@ import { ChartsModule } from 'ng2-charts'; import { AppComponent } from './app.component'; import { routing } from './app.routes'; import { CustomEditorsModule } from './components/activiti/custom-editor/custom-editor.component'; +import { AppMenuComponent } from './components/app-menu/app-menu.component'; +import { FileViewComponent } from './components/file-view/file-view.component'; import { FormListDemoComponent } from './components/form/form-list-demo.component'; import { ThemePickerModule } from './components/theme-picker/theme-picker'; import { MaterialModule } from './material.module'; @@ -119,7 +121,9 @@ import { SettingsComponent, FormDemoComponent, FormListDemoComponent, - CustomSourcesComponent + CustomSourcesComponent, + FileViewComponent, + AppMenuComponent ], providers: [ { provide: AppConfigService, useClass: DebugAppConfigService }, diff --git a/demo-shell-ng2/app/app.routes.ts b/demo-shell-ng2/app/app.routes.ts index ecc6bea0a9..c23380ae81 100644 --- a/demo-shell-ng2/app/app.routes.ts +++ b/demo-shell-ng2/app/app.routes.ts @@ -39,6 +39,7 @@ import { } from './components/index'; import { UploadButtonComponent } from 'ng2-alfresco-upload'; +import { FileViewComponent } from './components/file-view/file-view.component'; import { CustomSourcesComponent } from './components/files/custom-sources.component'; import { FormListDemoComponent } from './components/form/form-list-demo.component'; @@ -64,6 +65,11 @@ export const appRoutes: Routes = [ component: FilesComponent, canActivate: [AuthGuardEcm] }, + { + path: 'files/:nodeId/view', + component: FileViewComponent, + canActivate: [ AuthGuardEcm ] + }, { path: 'dl-custom-sources', component: CustomSourcesComponent, diff --git a/demo-shell-ng2/app/components/about/about.component.html b/demo-shell-ng2/app/components/about/about.component.html index 132925dfc1..213a40fbb0 100644 --- a/demo-shell-ng2/app/components/about/about.component.html +++ b/demo-shell-ng2/app/components/about/about.component.html @@ -1,3 +1,5 @@ + +

Server settings

diff --git a/demo-shell-ng2/app/components/about/about.component.ts b/demo-shell-ng2/app/components/about/about.component.ts index 7fb556da91..e62061e748 100644 --- a/demo-shell-ng2/app/components/about/about.component.ts +++ b/demo-shell-ng2/app/components/about/about.component.ts @@ -17,7 +17,7 @@ import { Component, OnInit } from '@angular/core'; import { Http } from '@angular/http'; -import { AlfrescoAuthenticationService, AppConfigService, BpmProductVersionModel, DiscoveryApiService, EcmProductVersionModel, LogService } from 'ng2-alfresco-core'; +import { AlfrescoAuthenticationService, AppConfigService, BpmProductVersionModel, DiscoveryApiService, EcmProductVersionModel } from 'ng2-alfresco-core'; import { ObjectDataTableAdapter } from 'ng2-alfresco-datatable'; @Component({ @@ -40,7 +40,6 @@ export class AboutComponent implements OnInit { constructor(private http: Http, private appConfig: AppConfigService, private authService: AlfrescoAuthenticationService, - private logService: LogService, private discovery: DiscoveryApiService) { } @@ -62,13 +61,11 @@ export class AboutComponent implements OnInit { let regexp = new RegExp('^(ng2-activiti|ng2-alfresco|alfresco-)'); let alfrescoPackages = Object.keys(response.json().dependencies).filter((val) => { - this.logService.log(val); return regexp.test(val); }); let alfrescoPackagesTableRepresentation = []; alfrescoPackages.forEach((val) => { - this.logService.log(response.json().dependencies[val]); alfrescoPackagesTableRepresentation.push({ name: val, version: response.json().dependencies[val].version @@ -77,8 +74,6 @@ export class AboutComponent implements OnInit { this.gitHubLinkCreation(alfrescoPackagesTableRepresentation); - this.logService.log(alfrescoPackagesTableRepresentation); - this.data = new ObjectDataTableAdapter(alfrescoPackagesTableRepresentation, [ {type: 'text', key: 'name', title: 'Name', sortable: true}, {type: 'text', key: 'version', title: 'Version', sortable: true} diff --git a/demo-shell-ng2/app/components/activiti/activiti-demo.component.html b/demo-shell-ng2/app/components/activiti/activiti-demo.component.html index 0d551a24e6..f84f4435b9 100644 --- a/demo-shell-ng2/app/components/activiti/activiti-demo.component.html +++ b/demo-shell-ng2/app/components/activiti/activiti-demo.component.html @@ -1,3 +1,5 @@ + +
diff --git a/demo-shell-ng2/app/components/activiti/apps.view.ts b/demo-shell-ng2/app/components/activiti/apps.view.ts index 04898acd6d..b73ae25f3e 100644 --- a/demo-shell-ng2/app/components/activiti/apps.view.ts +++ b/demo-shell-ng2/app/components/activiti/apps.view.ts @@ -22,6 +22,7 @@ import { AppDefinitionRepresentationModel } from 'ng2-activiti-tasklist'; @Component({ selector: 'activiti-apps-view', template: ` + ` }) diff --git a/demo-shell-ng2/app/components/app-menu/app-menu.component.html b/demo-shell-ng2/app/components/app-menu/app-menu.component.html new file mode 100644 index 0000000000..4bfcb76628 --- /dev/null +++ b/demo-shell-ng2/app/components/app-menu/app-menu.component.html @@ -0,0 +1,96 @@ + + + + ADF Demo Application + +
+ + + + Home + Content Services + Process Services + Login + + + + + + + + + + + + + + + home + Home + + + folder_open + Content Services + + + device_hub + Process Services + + + vpn_key + Login + + + extension + DL: Custom Sources + + + view_module + DataTable + + + poll + Form + + + library_books + Form List + + + file_upload + Uploader + + + extension + Webscript + + + local_offer + Tag + + + thumb_up + Social + + + settings + Settings + + + info_outline + About + + + +
diff --git a/demo-shell-ng2/app/app.component.scss b/demo-shell-ng2/app/components/app-menu/app-menu.component.scss similarity index 99% rename from demo-shell-ng2/app/app.component.scss rename to demo-shell-ng2/app/components/app-menu/app-menu.component.scss index 35a7d0c51f..016d5d4a01 100644 --- a/demo-shell-ng2/app/app.component.scss +++ b/demo-shell-ng2/app/components/app-menu/app-menu.component.scss @@ -1,6 +1,5 @@ @import '~@angular/material/theming'; - .adf-app-user-profile { margin-right: 10px; } diff --git a/demo-shell-ng2/app/components/app-menu/app-menu.component.ts b/demo-shell-ng2/app/components/app-menu/app-menu.component.ts new file mode 100644 index 0000000000..278d336db5 --- /dev/null +++ b/demo-shell-ng2/app/components/app-menu/app-menu.component.ts @@ -0,0 +1,34 @@ +/*! + * @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'; +import { AlfrescoTranslationService } from 'ng2-alfresco-core'; + +@Component({ + selector: 'adf-app-menu', + templateUrl: 'app-menu.component.html', + styleUrls: ['app-menu.component.scss'] +}) +export class AppMenuComponent { + + constructor(private translateService: AlfrescoTranslationService) { + } + + changeLanguage(lang: string) { + this.translateService.use(lang); + } +} diff --git a/demo-shell-ng2/app/components/datatable/datatable-demo.component.html b/demo-shell-ng2/app/components/datatable/datatable-demo.component.html index 1feceb707b..813816fe34 100644 --- a/demo-shell-ng2/app/components/datatable/datatable-demo.component.html +++ b/demo-shell-ng2/app/components/datatable/datatable-demo.component.html @@ -1,3 +1,5 @@ + +
+ + + + + + + + diff --git a/demo-shell-ng2/app/components/file-view/file-view.component.ts b/demo-shell-ng2/app/components/file-view/file-view.component.ts new file mode 100644 index 0000000000..ba713f6aaf --- /dev/null +++ b/demo-shell-ng2/app/components/file-view/file-view.component.ts @@ -0,0 +1,36 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { AlfrescoApiService } from 'ng2-alfresco-core'; + +@Component({ + selector: 'adf-file-view', + templateUrl: 'file-view.component.html' +}) +export class FileViewComponent implements OnInit { + + nodeId: string = null; + + constructor( + private router: Router, + private route: ActivatedRoute, + private apiService: AlfrescoApiService) {} + + ngOnInit() { + this.route.params.subscribe(params => { + const id = params.nodeId; + if (id) { + this.apiService.getInstance().nodes.getNodeInfo(id).then( + (node) => { + if (node && node.isFile) { + this.nodeId = id; + return; + } + this.router.navigate(['/files', id]); + }, + () => this.router.navigate(['/files', id]) + ); + } + }); + } + +} diff --git a/demo-shell-ng2/app/components/files/custom-sources.component.html b/demo-shell-ng2/app/components/files/custom-sources.component.html index cff8d59ede..7996cc2bc4 100644 --- a/demo-shell-ng2/app/components/files/custom-sources.component.html +++ b/demo-shell-ng2/app/components/files/custom-sources.component.html @@ -1,3 +1,4 @@ + diff --git a/demo-shell-ng2/app/components/files/files.component.html b/demo-shell-ng2/app/components/files/files.component.html index 14450ea3b6..fa1edea705 100644 --- a/demo-shell-ng2/app/components/files/files.component.html +++ b/demo-shell-ng2/app/components/files/files.component.html @@ -1,3 +1,5 @@ + +
@@ -225,14 +227,6 @@
-
- Use inline viewer (no overlay) -
- -
- Use File Viewer dialog -
-
Multiselect (with checkboxes)
@@ -309,29 +303,3 @@
- -
- - - - - - - - - - - - - -
- - -
-
-
diff --git a/demo-shell-ng2/app/components/files/files.component.ts b/demo-shell-ng2/app/components/files/files.component.ts index d6c40ce37c..a74bcdef67 100644 --- a/demo-shell-ng2/app/components/files/files.component.ts +++ b/demo-shell-ng2/app/components/files/files.component.ts @@ -27,8 +27,6 @@ import { import { DataColumn, DataRow } from 'ng2-alfresco-datatable'; import { DocumentListComponent, PermissionStyleModel } from 'ng2-alfresco-documentlist'; -import { ViewerService } from 'ng2-alfresco-viewer'; - const DEFAULT_FOLDER_TO_SHOW = '-my-'; @Component({ @@ -46,8 +44,6 @@ export class FilesComponent implements OnInit { toolbarColor = 'default'; useDropdownBreadcrumb = false; - useViewerDialog = true; - useInlineViewer = false; selectionModes = [ { value: 'none', viewValue: 'None' }, @@ -91,27 +87,14 @@ export class FilesComponent implements OnInit { private contentService: AlfrescoContentService, private dialog: MdDialog, private translateService: AlfrescoTranslationService, - private viewerService: ViewerService, private router: Router, @Optional() private route: ActivatedRoute) { } showFile(event) { - if (this.useViewerDialog) { - if (event.value.entry.isFile) { - this.viewerService - .showViewerForNode(event.value.entry) - .then(result => { - console.log(result); - }); - } - } else { - if (event.value.entry.isFile) { - this.fileNodeId = event.value.entry.id; - this.showViewer = true; - } else { - this.showViewer = false; - } + const entry = event.value.entry; + if (entry && entry.isFile) { + this.router.navigate(['/files', entry.id, 'view']); } } diff --git a/demo-shell-ng2/app/components/form/form-demo.component.html b/demo-shell-ng2/app/components/form/form-demo.component.html index 8b41dfbe7c..973f8b78f7 100644 --- a/demo-shell-ng2/app/components/form/form-demo.component.html +++ b/demo-shell-ng2/app/components/form/form-demo.component.html @@ -1,3 +1,5 @@ + +
diff --git a/demo-shell-ng2/app/components/form/form-list-demo.component.ts b/demo-shell-ng2/app/components/form/form-list-demo.component.ts index 162b04084b..8ace89ac05 100644 --- a/demo-shell-ng2/app/components/form/form-list-demo.component.ts +++ b/demo-shell-ng2/app/components/form/form-list-demo.component.ts @@ -22,6 +22,7 @@ import { ActivitiForm } from 'ng2-activiti-form'; @Component({ selector: 'form-list-demo', template: ` +
diff --git a/demo-shell-ng2/app/components/home/home.component.html b/demo-shell-ng2/app/components/home/home.component.html index af4342dcdc..cf97241a9c 100644 --- a/demo-shell-ng2/app/components/home/home.component.html +++ b/demo-shell-ng2/app/components/home/home.component.html @@ -1,3 +1,5 @@ + + diff --git a/demo-shell-ng2/app/components/social/social.component.html b/demo-shell-ng2/app/components/social/social.component.html index 7bd1e17bff..0b727f6cd0 100644 --- a/demo-shell-ng2/app/components/social/social.component.html +++ b/demo-shell-ng2/app/components/social/social.component.html @@ -1,3 +1,5 @@ + +

diff --git a/demo-shell-ng2/app/components/tag/tag.component.html b/demo-shell-ng2/app/components/tag/tag.component.html index 11d44c424a..1099f23c7a 100644 --- a/demo-shell-ng2/app/components/tag/tag.component.html +++ b/demo-shell-ng2/app/components/tag/tag.component.html @@ -1,3 +1,5 @@ + +

diff --git a/demo-shell-ng2/app/components/webscript/webscript.component.ts b/demo-shell-ng2/app/components/webscript/webscript.component.ts index 4587498c80..bd4296d559 100644 --- a/demo-shell-ng2/app/components/webscript/webscript.component.ts +++ b/demo-shell-ng2/app/components/webscript/webscript.component.ts @@ -21,6 +21,7 @@ import { LogService } from 'ng2-alfresco-core'; @Component({ selector: 'alfresco-webscript-demo', template: ` +


diff --git a/docs/docassets/images/renditions.png b/docs/docassets/images/renditions.png deleted file mode 100644 index 02198cbf856139126f17242ebc2abffde57189bb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 56771 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 sEv&n)170Cr9pdH?_b diff --git a/docs/viewer.component.md b/docs/viewer.component.md index 82169a7dcb..96bfa555ff 100644 --- a/docs/viewer.component.md +++ b/docs/viewer.component.md @@ -7,7 +7,7 @@ See it live: [Viewer Quickstart](https://embed.plnkr.co/iTuG1lFIXfsP95l6bDW6/) - [Basic usage](#basic-usage) - * [Properties](#properties) +- [Properties](#properties) - [Details](#details) * [Supported file formats](#supported-file-formats) * [PDF Conversion](#pdf-conversion) @@ -39,9 +39,9 @@ Using with file url: ``` -### Properties +## Properties -| Attribute | Options | Default | Description | +| Name | Type | Default | Description | | --- | --- | --- | --- | | fileNodeId | string | | Node Id of the file to load | | urlFile | string | | If you want to load an external file that does not come from ECM you can use this Url where to load the file | @@ -50,6 +50,13 @@ Using with file url: | showViewer | boolean | true | Hide or show the viewer | | showToolbar | boolean | true | Hide or show the toolbars | | displayName | string | | You can specify the name of the file | +| allowGoBack | boolean | true | Allow `back` navigation | +| allowOpenWith | boolean | true | Toggle `Open With` options | +| allowDownload | boolean | true | Toggle download feature | +| allowPrint | boolean | true | Toggle printing feature | +| allowShare | boolean | true | Toggle sharing feature | +| allowInfoDrawer | boolean | true | Toogle info drawer feature | +| showInfoDrawer | boolean | false | Toggles info drawer visibility. Requires `allowInfoDrawer` to be set to `true`. | ## Details @@ -63,9 +70,7 @@ Using with file url: ### PDF Conversion -![Rendition](docassets/images/renditions.png) - -Note for unsupported extensions the viewer will offer the possibility to convert to PDF if that kind of extension is supported by the [content service renditions service](https://community.alfresco.com/docs/DOC-5879-rendition-service) +For unsupported extensions or mime types the viewer will try to fetch PDF rendition utilising the [renditions service api](https://community.alfresco.com/docs/DOC-5879-rendition-service) ### Configuring PDF.js library diff --git a/ng2-components/ng2-alfresco-viewer/index.ts b/ng2-components/ng2-alfresco-viewer/index.ts index 276bee5c2f..8aa27e7eb6 100644 --- a/ng2-components/ng2-alfresco-viewer/index.ts +++ b/ng2-components/ng2-alfresco-viewer/index.ts @@ -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 diff --git a/ng2-components/ng2-alfresco-viewer/src/components/imgViewer.component.scss b/ng2-components/ng2-alfresco-viewer/src/components/imgViewer.component.scss index b62e87f509..3c78ff223c 100644 --- a/ng2-components/ng2-alfresco-viewer/src/components/imgViewer.component.scss +++ b/ng2-components/ng2-alfresco-viewer/src/components/imgViewer.component.scss @@ -7,6 +7,7 @@ justify-content: center; height: 90vh; img { + width: 100%; object-fit: contain; } } diff --git a/ng2-components/ng2-alfresco-viewer/src/components/notSupportedFormat.component.html b/ng2-components/ng2-alfresco-viewer/src/components/notSupportedFormat.component.html deleted file mode 100644 index 01d5009114..0000000000 --- a/ng2-components/ng2-alfresco-viewer/src/components/notSupportedFormat.component.html +++ /dev/null @@ -1,47 +0,0 @@ - - - Unknown format - - -

File '{{nameFile}}' is of an unsupported format

- - -
- - - - - -
- - - diff --git a/ng2-components/ng2-alfresco-viewer/src/components/notSupportedFormat.component.scss b/ng2-components/ng2-alfresco-viewer/src/components/notSupportedFormat.component.scss deleted file mode 100644 index b6bae84645..0000000000 --- a/ng2-components/ng2-alfresco-viewer/src/components/notSupportedFormat.component.scss +++ /dev/null @@ -1,5 +0,0 @@ -.adf-not-supported-format { - .mat-card { - max-width: 400px; - } -} diff --git a/ng2-components/ng2-alfresco-viewer/src/components/notSupportedFormat.component.spec.ts b/ng2-components/ng2-alfresco-viewer/src/components/notSupportedFormat.component.spec.ts deleted file mode 100644 index 5670e79a9b..0000000000 --- a/ng2-components/ng2-alfresco-viewer/src/components/notSupportedFormat.component.spec.ts +++ /dev/null @@ -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; - let debug: DebugElement; - let element: HTMLElement; - let renditionsService: RenditionsService; - - let renditionSubject: Subject; - let conversionSubject: Subject; - - 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(); - conversionSubject = new Subject(); - 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.'); - }); - }); - }); -}); diff --git a/ng2-components/ng2-alfresco-viewer/src/components/notSupportedFormat.component.ts b/ng2-components/ng2-alfresco-viewer/src/components/notSupportedFormat.component.ts deleted file mode 100644 index 0560f52d23..0000000000 --- a/ng2-components/ng2-alfresco-viewer/src/components/notSupportedFormat.component.ts +++ /dev/null @@ -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(); - } - } -} diff --git a/ng2-components/ng2-alfresco-viewer/src/components/pdfViewerHost.component.scss b/ng2-components/ng2-alfresco-viewer/src/components/pdfViewerHost.component.scss index 17684164df..20817fce7c 100644 --- a/ng2-components/ng2-alfresco-viewer/src/components/pdfViewerHost.component.scss +++ b/ng2-components/ng2-alfresco-viewer/src/components/pdfViewerHost.component.scss @@ -8,6 +8,7 @@ overflow: hidden; opacity: 0.2; line-height: 1.0; + border: 1px solid gray; & > div { color: transparent; diff --git a/ng2-components/ng2-alfresco-viewer/src/components/unknown-format/unknown-format.component.html b/ng2-components/ng2-alfresco-viewer/src/components/unknown-format/unknown-format.component.html new file mode 100644 index 0000000000..74de545144 --- /dev/null +++ b/ng2-components/ng2-alfresco-viewer/src/components/unknown-format/unknown-format.component.html @@ -0,0 +1,6 @@ +
+
+ wifi_tethering +
Document preview could not be loaded.
+
+
diff --git a/ng2-components/ng2-alfresco-viewer/src/components/unknown-format/unknown-format.component.scss b/ng2-components/ng2-alfresco-viewer/src/components/unknown-format/unknown-format.component.scss new file mode 100644 index 0000000000..8499afb75f --- /dev/null +++ b/ng2-components/ng2-alfresco-viewer/src/components/unknown-format/unknown-format.component.scss @@ -0,0 +1,8 @@ +.adf-viewer__unknown-format-view { + height: 90vh; + text-align: center; + display: flex; + flex: 1; + flex-direction: column; + justify-content: center; +} diff --git a/ng2-components/ng2-alfresco-viewer/src/components/unknown-format/unknown-format.component.ts b/ng2-components/ng2-alfresco-viewer/src/components/unknown-format/unknown-format.component.ts new file mode 100644 index 0000000000..95c2af1199 --- /dev/null +++ b/ng2-components/ng2-alfresco-viewer/src/components/unknown-format/unknown-format.component.ts @@ -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 { +} diff --git a/ng2-components/ng2-alfresco-viewer/src/components/viewer.component.html b/ng2-components/ng2-alfresco-viewer/src/components/viewer.component.html index d3dd4a5c74..37bc165ef7 100644 --- a/ng2-components/ng2-alfresco-viewer/src/components/viewer.component.html +++ b/ng2-components/ng2-alfresco-viewer/src/components/viewer.component.html @@ -4,80 +4,151 @@ [class.adf-viewer-inline-container]="!overlayMode">
- + + - {{ displayName }} + + + {{ displayName }} - + + + + + + + + + + + + + + + + + + + + + + + + + + + + -
+ +
+

Loading

+
+ +
+
+
+ +
-
-
+
+
- - - + + - - - + + - - - + + - + - - - - + + + + + + -
- - -
+ + +
+
+ +
+ + + + DETAILS + + + + + Activity + + + +
+
diff --git a/ng2-components/ng2-alfresco-viewer/src/components/viewer.component.scss b/ng2-components/ng2-alfresco-viewer/src/components/viewer.component.scss index ea3471a69c..84750c3812 100644 --- a/ng2-components/ng2-alfresco-viewer/src/components/viewer.component.scss +++ b/ng2-components/ng2-alfresco-viewer/src/components/viewer.component.scss @@ -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; + } + } } diff --git a/ng2-components/ng2-alfresco-viewer/src/components/viewer.component.spec.ts b/ng2-components/ng2-alfresco-viewer/src/components/viewer.component.spec.ts index 3a159eac6c..cc04ae154b 100644 --- a/ng2-components/ng2-alfresco-viewer/src/components/viewer.component.spec.ts +++ b/ng2-components/ng2-alfresco-viewer/src/components/viewer.component.spec.ts @@ -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', () => { diff --git a/ng2-components/ng2-alfresco-viewer/src/components/viewer.component.ts b/ng2-components/ng2-alfresco-viewer/src/components/viewer.component.ts index 4e087bd1c6..7f14a7e7e7 100644 --- a/ng2-components/ng2-alfresco-viewer/src/components/viewer.component.ts +++ b/ng2-components/ng2-alfresco-viewer/src/components/viewer.component.ts @@ -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 = new EventEmitter(); + goBack = new EventEmitter>(); @Output() - extensionChange: EventEmitter = new EventEmitter(); + showViewerChange = new EventEmitter(); + + @Output() + extensionChange = new EventEmitter(); + + viewerType: string = 'unknown'; + downloadUrl: string = null; + fileName: string = 'document'; + isLoading: boolean = false; extensionTemplates: { template: TemplateRef, 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(); + 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'); + } } } diff --git a/ng2-components/ng2-alfresco-viewer/src/directives/extension-viewer.directive.spec.ts b/ng2-components/ng2-alfresco-viewer/src/directives/extension-viewer.directive.spec.ts index 398406e3ce..0dfa24656c 100644 --- a/ng2-components/ng2-alfresco-viewer/src/directives/extension-viewer.directive.spec.ts +++ b/ng2-components/ng2-alfresco-viewer/src/directives/extension-viewer.directive.spec.ts @@ -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