diff --git a/docs/viewer.component.md b/docs/viewer.component.md index 461fb87414..ec54d7b5ee 100644 --- a/docs/viewer.component.md +++ b/docs/viewer.component.md @@ -65,6 +65,10 @@ Using with file url: | showSidebar | boolean | false | Toggles sidebar visibility. Requires `allowSidebar` to be set to `true`. | | sidebarPosition | string | right | The position of the sidebar. Can be `left` or `right`. | | sidebarTemplate | TemplateRef | null | The template intended to be used as a sidebar. The template context contains the loaded node data. | +| allowNavigate | boolean | false | Toggle before/next navigation, arrow buttons to navigate between multiple documents in the collection. | +| canNavigateBefore | boolean | true | Toggle the "before" ("<") button. Requires `allowNavigate` to be enabled. | +| canNavigateNext | boolean | true | Toggle the next (">") button. Requires `allowNavigate` to be enabled.| +| allowFullScreen | boolean | true | Toggle the 'Full Screen' feature. | ### Events @@ -74,6 +78,17 @@ Using with file url: | download | any | Yes | Raised when user clicks the 'Download' button. | | print | any | Yes | Raised when user clicks the 'Print' button. | | share | any | Yes | Raised when user clicks the 'Share' button. | +| navigateBefore | any | No | Raised when user clicks 'Navigate Before' ("<") button. | +| navigateNext | any | No | Raised when user clicks 'Navigate Next' (">") button. | + +### Keyboard shortcuts + +| Name | Description | +| --- | --- | +| Esc | Close the viewer (overlay mode only). | +| Left | Invoke 'Navigate before' action. | +| Right | Invoke 'Navigate next' action. | +| Ctrl+F | Activate full-screen mode. | ## Details diff --git a/lib/core/i18n/en.json b/lib/core/i18n/en.json index 9b5f07b361..867c83d8b1 100644 --- a/lib/core/i18n/en.json +++ b/lib/core/i18n/en.json @@ -187,7 +187,8 @@ "PRINT": "Print", "SHARE": "Share", "MORE_ACTIONS": "More actions", - "INFO": "Info" + "INFO": "Info", + "FULLSCREEN": "Activate full-screen mode" }, "ARIA": { "PREVIOUS_PAGE": "Previous page", diff --git a/lib/core/mock/event.mock.ts b/lib/core/mock/event.mock.ts index 2cb9c114cb..a3970d28dd 100644 --- a/lib/core/mock/event.mock.ts +++ b/lib/core/mock/event.mock.ts @@ -24,6 +24,13 @@ export class EventMock { document.dispatchEvent(event); } + static keyUp(key: any) { + let event: any = document.createEvent('Event'); + event.keyCode = key; + event.initEvent('keyup'); + document.dispatchEvent(event); + } + static resizeMobileView() { // todo: no longer compiles with TS 2.0.2 as innerWidth/innerHeight are readonly fields /* diff --git a/lib/core/styles/_index.scss b/lib/core/styles/_index.scss index e26c0ba8ed..a0205d5fbb 100644 --- a/lib/core/styles/_index.scss +++ b/lib/core/styles/_index.scss @@ -17,9 +17,11 @@ @import '../toolbar/toolbar.component'; @import '../userinfo/components/user-info.component'; @import '../viewer/components/viewer.component'; +@import '../viewer/components/pdfViewer.component'; +@import '../viewer/components/txtViewer.component'; +@import '../viewer/components/imgViewer.component'; @import '../form/components/form.component'; @import '../sidebar/sidebar-action-menu.component'; -@import '../viewer/components/pdfViewer.component'; @mixin adf-core-theme($theme) { @include adf-colors-theme($theme); @@ -40,6 +42,8 @@ @include adf-userinfo-theme($theme); @include adf-viewer-theme($theme); @include adf-pdf-viewer-theme($theme); + @include adf-image-viewer-theme($theme); + @include adf-text-viewer-theme($theme); @include adf-form-component-theme($theme); @include adf-sidebar-action-menu-theme($theme); } diff --git a/lib/core/viewer/components/imgViewer.component.html b/lib/core/viewer/components/imgViewer.component.html index f11c0bc495..896b879001 100644 --- a/lib/core/viewer/components/imgViewer.component.html +++ b/lib/core/viewer/components/imgViewer.component.html @@ -1,3 +1,27 @@ -
+
+ +
+ + + + + + + + + + + +
diff --git a/lib/core/viewer/components/imgViewer.component.scss b/lib/core/viewer/components/imgViewer.component.scss index 3c78ff223c..3c414b360e 100644 --- a/lib/core/viewer/components/imgViewer.component.scss +++ b/lib/core/viewer/components/imgViewer.component.scss @@ -1,14 +1,33 @@ -.adf-img-viewer { - .image-container { - display: flex; - flex: 1; - text-align: center; - flex-direction: row; - justify-content: center; - height: 90vh; - img { - width: 100%; - object-fit: contain; +@mixin adf-image-viewer-theme($theme) { + .adf-image-viewer { + .image-container { + display: flex; + flex: 1; + text-align: center; + flex-direction: row; + justify-content: center; + height: 90vh; + img { + width: 100%; + object-fit: contain; + } + } + + &__toolbar { + position: absolute; + bottom: 5px; + + left: 50%; + transform: translateX(-50%); + + .adf-toolbar .mat-toolbar { + max-height: 48px; + background-color: mat-color($primary, default-contrast, 1); + border-width: 0; + border-radius: 2px; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.24), 0 0 2px 0 rgba(0, 0, 0, 0.12); + } + } } } diff --git a/lib/core/viewer/components/imgViewer.component.spec.ts b/lib/core/viewer/components/imgViewer.component.spec.ts index 6f066e2ae2..3366f93d7b 100644 --- a/lib/core/viewer/components/imgViewer.component.spec.ts +++ b/lib/core/viewer/components/imgViewer.component.spec.ts @@ -21,6 +21,8 @@ import { AlfrescoApiService } from '../../services/alfresco-api.service'; import { AuthenticationService } from '../../services/authentication.service'; import { ContentService } from '../../services/content.service'; import { SettingsService } from '../../services/settings.service'; +import { MaterialModule } from '../../material.module'; +import { ToolbarModule } from '../../toolbar/toolbar.module'; import { ImgViewerComponent } from './imgViewer.component'; @@ -38,7 +40,10 @@ describe('Test Img viewer component ', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - + imports: [ + MaterialModule, + ToolbarModule + ], declarations: [ImgViewerComponent], providers: [ SettingsService, diff --git a/lib/core/viewer/components/imgViewer.component.ts b/lib/core/viewer/components/imgViewer.component.ts index f992a2d2e8..d862482f10 100644 --- a/lib/core/viewer/components/imgViewer.component.ts +++ b/lib/core/viewer/components/imgViewer.component.ts @@ -22,11 +22,14 @@ import { ContentService } from '../../services/content.service'; selector: 'adf-img-viewer', templateUrl: './imgViewer.component.html', styleUrls: ['./imgViewer.component.scss'], - host: { 'class': 'adf-img-viewer' }, + host: { 'class': 'adf-image-viewer' }, encapsulation: ViewEncapsulation.None }) export class ImgViewerComponent implements OnChanges { + @Input() + showToolbar = true; + @Input() urlFile: string; @@ -36,6 +39,14 @@ export class ImgViewerComponent implements OnChanges { @Input() nameFile: string; + rotate: number = 0; + scaleX: number = 1.0; + scaleY: number = 1.0; + + get transform(): string { + return `scale(${this.scaleX}, ${this.scaleY}) rotate(${this.rotate}deg)` + } + constructor(private contentService: ContentService) {} ngOnChanges(changes: SimpleChanges) { @@ -48,4 +59,31 @@ export class ImgViewerComponent implements OnChanges { throw new Error('Attribute urlFile or blobFile is required'); } } + + zoomIn() { + const ratio = +((this.scaleX + 0.2).toFixed(1)); + this.scaleX = this.scaleY = ratio; + } + + zoomOut() { + let ratio = +((this.scaleX - 0.2).toFixed(1)); + if (ratio < 0.2) { + ratio = 0.2; + } + this.scaleX = this.scaleY = ratio; + } + + rotateLeft() { + const angle = this.rotate - 90; + this.rotate = Math.abs(angle) < 360 ? angle : 0; + } + + rotateRight() { + const angle = this.rotate + 90; + this.rotate = Math.abs(angle) < 360 ? angle : 0; + } + + flip() { + this.scaleX *= -1; + } } diff --git a/lib/core/viewer/components/pdfViewer.component.html b/lib/core/viewer/components/pdfViewer.component.html index dedb69d260..c68047b923 100644 --- a/lib/core/viewer/components/pdfViewer.component.html +++ b/lib/core/viewer/components/pdfViewer.component.html @@ -44,7 +44,9 @@ {{ 'ADF_VIEWER.PAGE_LABEL.OF' | translate }} {{ totalPages }}
- +
+ {{ currentScaleText }} +
+ + + + +
+
+
@@ -142,10 +158,17 @@ -
+ + + + diff --git a/lib/core/viewer/components/viewer.component.scss b/lib/core/viewer/components/viewer.component.scss index 7104892d4e..2de91b76c1 100644 --- a/lib/core/viewer/components/viewer.component.scss +++ b/lib/core/viewer/components/viewer.component.scss @@ -11,6 +11,30 @@ .adf-viewer { + .navigate-before { + display: flex; + align-items: center; + justify-content: center; + order: 1; + padding-left: 2px; + padding-right: 4px; + background-color: mat-color($background, background); + } + + &-main { + width: 0; + } + + .navigate-next { + display: flex; + align-items: center; + justify-content: center; + order: 3; + padding-left: 4px; + padding-right: 2px; + background-color: mat-color($background, background); + } + &__mimeicon { vertical-align: middle; height: 18px; @@ -19,7 +43,7 @@ &-toolbar { .mat-toolbar { - background-color: mat-color($primary, default-contrast) + background-color: mat-color($primary, default-contrast); } } diff --git a/lib/core/viewer/components/viewer.component.spec.ts b/lib/core/viewer/components/viewer.component.spec.ts index 7a8fa90ffc..6d6957fb91 100644 --- a/lib/core/viewer/components/viewer.component.spec.ts +++ b/lib/core/viewer/components/viewer.component.spec.ts @@ -232,7 +232,7 @@ describe('ViewerComponent', () => { component.sidebarPosition = 'right'; fixture.detectChanges(); let sidebar = element.querySelector('.adf-viewer__sidebar'); - expect(getComputedStyle(sidebar).order).toEqual('2'); + expect(getComputedStyle(sidebar).order).toEqual('4'); }); it('should display sidebar on the right side as fallback', () => { @@ -241,11 +241,71 @@ describe('ViewerComponent', () => { component.sidebarPosition = 'unknown-value'; fixture.detectChanges(); let sidebar = element.querySelector('.adf-viewer__sidebar'); - expect(getComputedStyle(sidebar).order).toEqual('2'); + expect(getComputedStyle(sidebar).order).toEqual('4'); + }); + + describe('Full Screen Mode', () => { + + it('should request only if enabled', () => { + const domElement = jasmine.createSpyObj('el', ['requestFullscreen']); + spyOn(fixture.nativeElement, 'querySelector').and.returnValue(domElement); + + component.allowFullScreen = false; + component.enterFullScreen(); + + expect(domElement.requestFullscreen).not.toHaveBeenCalled(); + }); + + it('should use standard mode', () => { + const domElement = jasmine.createSpyObj('el', ['requestFullscreen']); + spyOn(fixture.nativeElement, 'querySelector').and.returnValue(domElement); + + component.enterFullScreen(); + expect(domElement.requestFullscreen).toHaveBeenCalled(); + }); + + it('should use webkit prefix', () => { + const domElement = jasmine.createSpyObj('el', ['webkitRequestFullscreen']); + spyOn(fixture.nativeElement, 'querySelector').and.returnValue(domElement); + + component.enterFullScreen(); + expect(domElement.webkitRequestFullscreen).toHaveBeenCalled(); + }); + + it('should use moz prefix', () => { + const domElement = jasmine.createSpyObj('el', ['mozRequestFullScreen']); + spyOn(fixture.nativeElement, 'querySelector').and.returnValue(domElement); + + component.enterFullScreen(); + expect(domElement.mozRequestFullScreen).toHaveBeenCalled(); + }); + + it('should use ms prefix', () => { + const domElement = jasmine.createSpyObj('el', ['msRequestFullscreen']); + spyOn(fixture.nativeElement, 'querySelector').and.returnValue(domElement); + + component.enterFullScreen(); + expect(domElement.msRequestFullscreen).toHaveBeenCalled(); + }); + }); describe('Toolbar', () => { + it('should render fullscreen button', () => { + component.allowFullScreen = true; + fixture.detectChanges(); + + expect(element.querySelector('[data-automation-id="toolbar-fullscreen"]')).toBeDefined(); + }); + + it('should not render fullscreen button', () => { + component.allowFullScreen = false; + fixture.detectChanges(); + + expect(element.querySelector('[data-automation-id="toolbar-fullscreen"]')).toBeNull(); + }); + it('should render default download button', () => { component.allowDownload = true; fixture.detectChanges(); @@ -397,7 +457,7 @@ describe('ViewerComponent', () => { }); it('should Esc button hide the viewer', () => { - EventMock.keyDown(27); + EventMock.keyUp(27); fixture.detectChanges(); expect(element.querySelector('.adf-viewer-content')).toBeNull(); }); diff --git a/lib/core/viewer/components/viewer.component.ts b/lib/core/viewer/components/viewer.component.ts index 72b6ffcd39..6cee07923d 100644 --- a/lib/core/viewer/components/viewer.component.ts +++ b/lib/core/viewer/components/viewer.component.ts @@ -17,7 +17,7 @@ import { Location } from '@angular/common'; import { - Component, ContentChild, EventEmitter, HostListener, + Component, ContentChild, EventEmitter, HostListener, ElementRef, Input, OnChanges, Output, SimpleChanges, TemplateRef, ViewEncapsulation } from '@angular/core'; import { MinimalNodeEntryEntity } from 'alfresco-js-api'; @@ -90,6 +90,18 @@ export class ViewerComponent implements OnChanges { @Input() allowShare = false; + @Input() + allowFullScreen = true; + + @Input() + allowNavigate = false; + + @Input() + canNavigateBefore = true; + + @Input() + canNavigateNext = true; + @Input() allowSidebar = false; @@ -129,6 +141,12 @@ export class ViewerComponent implements OnChanges { @Output() extensionChange = new EventEmitter(); + @Output() + navigateBefore = new EventEmitter(); + + @Output() + navigateNext = new EventEmitter(); + viewerType = 'unknown'; isLoading = false; node: MinimalNodeEntryEntity; @@ -143,7 +161,7 @@ export class ViewerComponent implements OnChanges { private extensions = { image: ['png', 'jpg', 'jpeg', 'gif', 'bpm'], media: ['wav', 'mp4', 'mp3', 'webm', 'ogg'], - text: ['txt', 'xml', 'js', 'html', 'json'], + text: ['txt', 'xml', 'js', 'html', 'json', 'ts'], pdf: ['pdf'] }; @@ -155,7 +173,8 @@ export class ViewerComponent implements OnChanges { constructor(private apiService: AlfrescoApiService, private logService: LogService, private location: Location, - private renditionService: RenditionsService) { + private renditionService: RenditionsService, + private el: ElementRef) { } isSourceDefined(): boolean { @@ -354,6 +373,14 @@ export class ViewerComponent implements OnChanges { } } + onNavigateBeforeClick() { + this.navigateBefore.next(); + } + + onNavigateNextClick() { + this.navigateNext.next(); + } + /** * close the viewer */ @@ -409,15 +436,35 @@ export class ViewerComponent implements OnChanges { } /** - * Litener Keyboard Event + * Keyboard event listener * @param {KeyboardEvent} event */ - @HostListener('document:keydown', ['$event']) + @HostListener('document:keyup', ['$event']) handleKeyboardEvent(event: KeyboardEvent) { - let key = event.keyCode; + const key = event.keyCode; + + // Esc if (key === 27 && this.overlayMode) { // esc this.close(); } + + // Left arrow + if (key === 37 && this.canNavigateBefore) { + event.preventDefault(); + this.onNavigateBeforeClick(); + } + + // Right arrow + if (key === 39 && this.canNavigateNext) { + event.preventDefault(); + this.onNavigateNextClick(); + } + + // Ctrl+F + if (key === 70 && event.ctrlKey) { + event.preventDefault(); + this.enterFullScreen(); + } } downloadContent() { @@ -453,6 +500,26 @@ export class ViewerComponent implements OnChanges { } } + /** + * Triggers full screen mode with a main content area displayed. + */ + enterFullScreen(): void { + if (this.allowFullScreen) { + const container = this.el.nativeElement.querySelector('.adf-viewer__fullscreen-container'); + if (container) { + if (container.requestFullscreen) { + container.requestFullscreen(); + } else if (container.webkitRequestFullscreen) { + container.webkitRequestFullscreen(); + } else if (container.mozRequestFullScreen) { + container.mozRequestFullScreen(); + } else if (container.msRequestFullscreen) { + container.msRequestFullscreen(); + } + } + } + } + private async displayNodeRendition(nodeId: string) { this.isLoading = true;