diff --git a/frontend/app/app.module.ts b/frontend/app/app.module.ts index 26a6312..2d9268f 100644 --- a/frontend/app/app.module.ts +++ b/frontend/app/app.module.ts @@ -30,6 +30,7 @@ import {Config} from "./config/Config"; import {GalleryMapComponent} from "./gallery/map/map.gallery.component"; import {GalleryMapLightboxComponent} from "./gallery/map/lightbox/lightbox.map.gallery.component"; import {ThumbnailManagerService} from "./gallery/thumnailManager.service"; +import {OverlayService} from "./gallery/overlay.service"; @NgModule({ imports: [ @@ -68,7 +69,8 @@ import {ThumbnailManagerService} from "./gallery/thumnailManager.service"; AuthenticationService, ThumbnailLoaderService, ThumbnailManagerService, - FullScreenService], + FullScreenService, + OverlayService], bootstrap: [AppComponent] }) diff --git a/frontend/app/gallery/fullscreen.service.ts b/frontend/app/gallery/fullscreen.service.ts index ae45ced..9e89b1c 100644 --- a/frontend/app/gallery/fullscreen.service.ts +++ b/frontend/app/gallery/fullscreen.service.ts @@ -1,9 +1,12 @@ import {Injectable} from "@angular/core"; +import {Event} from "../../../common/event/Event"; @Injectable() export class FullScreenService { + OnFullScreenChange = new Event(); + public isFullScreenEnabled(): boolean { return !!(document.fullscreenElement || document['mozFullScreenElement'] || document.webkitFullscreenElement); } @@ -22,6 +25,7 @@ export class FullScreenService { } else if (element.msRequestFullscreen) { element.msRequestFullscreen(); } + this.OnFullScreenChange.trigger(true); } public exitFullScreen() { @@ -36,6 +40,7 @@ export class FullScreenService { } else if (document.webkitExitFullscreen) { document.webkitExitFullscreen(); } + this.OnFullScreenChange.trigger(false); } } diff --git a/frontend/app/gallery/grid/grid.gallery.component.ts b/frontend/app/gallery/grid/grid.gallery.component.ts index fb88908..11f1c03 100644 --- a/frontend/app/gallery/grid/grid.gallery.component.ts +++ b/frontend/app/gallery/grid/grid.gallery.component.ts @@ -1,13 +1,14 @@ import { - Component, - Input, - ElementRef, - OnChanges, - ViewChild, - ViewChildren, - QueryList, AfterViewInit, - HostListener + ChangeDetectorRef, + Component, + ElementRef, + HostListener, + Input, + OnChanges, + QueryList, + ViewChild, + ViewChildren } from "@angular/core"; import {PhotoDTO} from "../../../../common/entities/PhotoDTO"; import {GridRowBuilder} from "./GridRowBuilder"; @@ -15,31 +16,33 @@ import {GalleryLightboxComponent} from "../lightbox/lightbox.gallery.component"; import {GridPhoto} from "./GridPhoto"; import {GalleryPhotoComponent} from "./photo/photo.grid.gallery.component"; import {Config} from "../../config/Config"; +import {OverlayService} from "../overlay.service"; @Component({ selector: 'gallery-grid', templateUrl: 'app/gallery/grid/grid.gallery.component.html', styleUrls: ['app/gallery/grid/grid.gallery.component.css'], }) -export class GalleryGridComponent implements OnChanges,AfterViewInit { +export class GalleryGridComponent implements OnChanges, AfterViewInit { - @ViewChild('gridContainer') gridContainer:ElementRef; - @ViewChildren(GalleryPhotoComponent) gridPhotoQL:QueryList; + @ViewChild('gridContainer') gridContainer: ElementRef; + @ViewChildren(GalleryPhotoComponent) gridPhotoQL: QueryList; @Input() photos: Array; - @Input() lightbox:GalleryLightboxComponent; + @Input() lightbox: GalleryLightboxComponent; - photosToRender:Array = []; - containerWidth:number = 0; + photosToRender: Array = []; + containerWidth: number = 0; private IMAGE_MARGIN = 2; private TARGET_COL_COUNT = 5; private MIN_ROW_COUNT = 2; private MAX_ROW_COUNT = 5; - onScrollFired = false; + private onScrollFired = false; + private scrollbarWidth = 0; - constructor() { + constructor(private overlayService: OverlayService, private changeDetector: ChangeDetectorRef) { } ngOnChanges() { @@ -61,16 +64,17 @@ export class GalleryGridComponent implements OnChanges,AfterViewInit { } this.updateContainerWidth(); this.sortPhotos(); + //render the same amount of images on resize + let renderedIndex = this.renderedPhotoIndex; this.clearRenderedPhotos(); - setImmediate(() => { - this.renderPhotos(); - }); + this.renderPhotos(renderedIndex); } - isAfterViewInit:boolean = false; + + isAfterViewInit: boolean = false; ngAfterViewInit() { - this.lightbox.gridPhotoQL = this.gridPhotoQL; + this.lightbox.setGridPhotoQL(this.gridPhotoQL); //TODO: implement scroll detection @@ -96,6 +100,7 @@ export class GalleryGridComponent implements OnChanges,AfterViewInit { private clearRenderedPhotos() { this.photosToRender = []; this.renderedPhotoIndex = 0; + this.changeDetector.detectChanges(); } private mergeNewPhotos() { @@ -123,9 +128,9 @@ export class GalleryGridComponent implements OnChanges,AfterViewInit { } - private renderedPhotoIndex:number = 0; + private renderedPhotoIndex: number = 0; - private renderPhotos() { + private renderPhotos(numberOfPhotos: number = 0) { if (this.containerWidth == 0 || this.renderedPhotoIndex >= this.photos.length || !this.shouldRenderMore()) { return; } @@ -133,7 +138,7 @@ export class GalleryGridComponent implements OnChanges,AfterViewInit { let renderedContentHeight = 0; - while (this.renderedPhotoIndex < this.photos.length && this.shouldRenderMore(renderedContentHeight) === true) { + while (this.renderedPhotoIndex < this.photos.length && (this.shouldRenderMore(renderedContentHeight) === true || this.renderedPhotoIndex < numberOfPhotos)) { let ret = this.renderARow(); if (ret === null) { throw new Error("Gridphotos rendering failed"); @@ -149,7 +154,7 @@ export class GalleryGridComponent implements OnChanges,AfterViewInit { * @param offset Add height to the client height (conent is not yet added to the dom, but calculate with it) * @returns {boolean} */ - private shouldRenderMore(offset:number = 0):boolean { + private shouldRenderMore(offset: number = 0): boolean { return Config.Client.enableOnScrollRendering === false || window.scrollY >= (document.body.clientHeight + offset - window.innerHeight) * 0.7 || (document.body.clientHeight + offset) * 0.85 < window.innerHeight; @@ -164,7 +169,7 @@ export class GalleryGridComponent implements OnChanges,AfterViewInit { this.renderPhotos(); if (Config.Client.enableOnScrollThumbnailPrioritising === true) { - this.gridPhotoQL.toArray().forEach((pc:GalleryPhotoComponent) => { + this.gridPhotoQL.toArray().forEach((pc: GalleryPhotoComponent) => { pc.onScroll(); }); } @@ -174,15 +179,16 @@ export class GalleryGridComponent implements OnChanges,AfterViewInit { } } - public renderARow():number { + public renderARow(): number { if (this.renderedPhotoIndex >= this.photos.length) { return null; } + let maxRowHeight = window.innerHeight / this.MIN_ROW_COUNT; let minRowHeight = window.innerHeight / this.MAX_ROW_COUNT; - let photoRowBuilder = new GridRowBuilder(this.photos, this.renderedPhotoIndex, this.IMAGE_MARGIN, this.containerWidth); + let photoRowBuilder = new GridRowBuilder(this.photos, this.renderedPhotoIndex, this.IMAGE_MARGIN, this.containerWidth - this.overlayService.getPhantomScrollbarWidth()); photoRowBuilder.addPhotos(this.TARGET_COL_COUNT); photoRowBuilder.adjustRowHeightBetween(minRowHeight, maxRowHeight); @@ -198,7 +204,7 @@ export class GalleryGridComponent implements OnChanges,AfterViewInit { return rowHeight; } - private updateContainerWidth():number { + private updateContainerWidth(): number { if (!this.gridContainer) { return; } diff --git a/frontend/app/gallery/lightbox/lightbox.gallery.component.html b/frontend/app/gallery/lightbox/lightbox.gallery.component.html index 6ed5280..f5dcb50 100644 --- a/frontend/app/gallery/lightbox/lightbox.gallery.component.html +++ b/frontend/app/gallery/lightbox/lightbox.gallery.component.html @@ -37,9 +37,11 @@ - - diff --git a/frontend/app/gallery/lightbox/lightbox.gallery.component.ts b/frontend/app/gallery/lightbox/lightbox.gallery.component.ts index 4c3e1a0..742ccf3 100644 --- a/frontend/app/gallery/lightbox/lightbox.gallery.component.ts +++ b/frontend/app/gallery/lightbox/lightbox.gallery.component.ts @@ -1,8 +1,19 @@ -import {Component, QueryList, Output, EventEmitter, HostListener, ElementRef, ViewChild} from "@angular/core"; +import { + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + HostListener, + Output, + QueryList, + ViewChild +} from "@angular/core"; import {PhotoDTO} from "../../../../common/entities/PhotoDTO"; import {GalleryPhotoComponent} from "../grid/photo/photo.grid.gallery.component"; import {Dimension} from "../../model/IRenderable"; import {FullScreenService} from "../fullscreen.service"; +import {OverlayService} from "../overlay.service"; +import {Subscription} from "rxjs"; @Component({ selector: 'gallery-lightbox', @@ -19,62 +30,80 @@ export class GalleryLightboxComponent { public blackCanvasOpacity: any = 0; private activePhoto: GalleryPhotoComponent; - public gridPhotoQL: QueryList; + private gridPhotoQL: QueryList; private visible = false; + private changeSubscription: Subscription = null; @ViewChild("root") elementRef: ElementRef; - constructor(private fullScreenService: FullScreenService) { + constructor(private fullScreenService: FullScreenService, private changeDetector: ChangeDetectorRef, private overlayService: OverlayService) { + } + //noinspection JSUnusedGlobalSymbols + @HostListener('window:resize', ['$event']) + onResize() { + if (this.activePhoto) { + this.disableAnimation(); + this.lightboxDimension.width = this.getScreenWidth(); + this.lightboxDimension.height = this.getScreenHeight(); + this.updateActivePhoto(this.activePhotoId); + } } public nextImage() { - this.disableAnimation(); - let pcList = this.gridPhotoQL.toArray(); - for (let i = 0; i < pcList.length; i++) { - if (pcList[i] === this.activePhoto) { - if (i + 1 < pcList.length) { - this.showPhoto(pcList[i + 1]); - - if (i + 3 === pcList.length) { - this.onLastElement.emit({}); //trigger to render more photos if there are - } - } - return; + if (this.activePhotoId + 1 < this.gridPhotoQL.length) { + this.showPhoto(this.activePhotoId + 1); + if (this.activePhotoId + 3 >= this.gridPhotoQL.length) { + this.onLastElement.emit({}); //trigger to render more photos if there are } + return; } + console.warn("can't find photo to show next"); } public prevImage() { this.disableAnimation(); - let pcList = this.gridPhotoQL.toArray(); - for (let i = 0; i < pcList.length; i++) { - if (pcList[i] === this.activePhoto) { - if (i > 0) { - this.showPhoto(pcList[i - 1]); - } - return; - } + if (this.activePhotoId > 0) { + this.showPhoto(this.activePhotoId - 1); + return; } + console.warn("can't find photo to show prev"); } - private showPhoto(photoComponent: GalleryPhotoComponent) { + activePhotoId: number = null; + + private showPhoto(photoIndex: number) { + this.activePhoto = null; + this.changeDetector.detectChanges(); + this.updateActivePhoto(photoIndex); + } + + private updateActivePhoto(photoIndex: number) { let pcList = this.gridPhotoQL.toArray(); - let index = pcList.indexOf(photoComponent); - if (index == -1) { + + if (photoIndex < 0 || photoIndex > this.gridPhotoQL.length) { throw new Error("Can't find the photo"); } + this.activePhotoId = photoIndex; + this.activePhoto = pcList[photoIndex]; + + this.photoDimension = this.calcLightBoxPhotoDimension(this.activePhoto.gridPhoto.photo); + this.navigation.hasPrev = photoIndex > 0; + this.navigation.hasNext = photoIndex + 1 < pcList.length; + + let to = this.activePhoto.getDimension(); + + //if target image out of screen -> scroll to there + if (this.getBodyScrollTop() > to.top || this.getBodyScrollTop() + this.getScreenHeight() < to.top) { + this.setBodyScrollTop(to.top); + } - this.photoDimension = this.calcLightBoxPhotoDimension(photoComponent.gridPhoto.photo); - this.navigation.hasPrev = index > 0; - this.navigation.hasNext = index + 1 < pcList.length; - this.activePhoto = photoComponent; } public show(photo: PhotoDTO) { @@ -90,8 +119,8 @@ export class GalleryLightboxComponent { this.blackCanvasOpacity = 0; this.photoDimension = selectedPhoto.getDimension(); - document.getElementsByTagName('body')[0].style.overflow = 'hidden'; - + //disable scroll + this.overlayService.showOverlay(); setImmediate(() => { this.lightboxDimension = { top: 0, @@ -100,19 +129,13 @@ export class GalleryLightboxComponent { height: this.getScreenHeight() }; this.blackCanvasOpacity = 1.0; - this.showPhoto(selectedPhoto); + this.showPhoto(this.gridPhotoQL.toArray().indexOf(selectedPhoto)); }); } public hide() { this.enableAnimation(); this.fullScreenService.exitFullScreen(); - let to = this.activePhoto.getDimension(); - - //iff target image out of screen -> scroll to there - if (this.getBodyScrollTop() > to.top || this.getBodyScrollTop() + this.getScreenHeight() < to.top) { - this.setBodyScrollTop(to.top); - } this.lightboxDimension = this.activePhoto.getDimension(); this.lightboxDimension.top -= this.getBodyScrollTop(); @@ -121,13 +144,24 @@ export class GalleryLightboxComponent { setTimeout(() => { this.visible = false; this.activePhoto = null; - document.getElementsByTagName('body')[0].style.overflow = 'scroll'; + this.overlayService.hideOverlay(); }, 500); - } + setGridPhotoQL(value: QueryList) { + if (this.changeSubscription != null) { + this.changeSubscription.unsubscribe(); + } + this.gridPhotoQL = value; + this.changeSubscription = this.gridPhotoQL.changes.subscribe(() => { + if (this.activePhotoId != null && this.gridPhotoQL.length > this.activePhotoId) { + this.updateActivePhoto(this.activePhotoId); + } + }); + } + private findPhotoComponent(photo: any) { let galleryPhotoComponents = this.gridPhotoQL.toArray(); for (let i = 0; i < galleryPhotoComponents.length; i++) { @@ -138,15 +172,26 @@ export class GalleryLightboxComponent { return null; } + //noinspection JSUnusedGlobalSymbols @HostListener('window:keydown', ['$event']) onKeyPress(e: KeyboardEvent) { + if (this.visible != true) { + return; + } let event: KeyboardEvent = window.event ? window.event : e; switch (event.keyCode) { case 37: - this.prevImage(); + if (this.activePhotoId > 0) { + this.prevImage(); + } break; case 39: - this.nextImage(); + if (this.activePhotoId < this.gridPhotoQL.length - 1) { + this.nextImage(); + } + break; + case 27: //escape + this.hide(); break; } } diff --git a/frontend/app/gallery/lightbox/photo/photo.lightbox.gallery.component.html b/frontend/app/gallery/lightbox/photo/photo.lightbox.gallery.component.html index 274e31c..03355ee 100644 --- a/frontend/app/gallery/lightbox/photo/photo.lightbox.gallery.component.html +++ b/frontend/app/gallery/lightbox/photo/photo.lightbox.gallery.component.html @@ -4,12 +4,13 @@ [style.height.%]="imageSize.height" [src]="thumbnailPath()"/> - + diff --git a/frontend/app/gallery/lightbox/photo/photo.lightbox.gallery.component.ts b/frontend/app/gallery/lightbox/photo/photo.lightbox.gallery.component.ts index e84af2f..aee4443 100644 --- a/frontend/app/gallery/lightbox/photo/photo.lightbox.gallery.component.ts +++ b/frontend/app/gallery/lightbox/photo/photo.lightbox.gallery.component.ts @@ -1,4 +1,4 @@ -import {Component, OnChanges, Input, ViewChild, ElementRef} from "@angular/core"; +import {Component, Input, OnChanges} from "@angular/core"; import {GridPhoto} from "../../grid/GridPhoto"; @Component({ @@ -8,12 +8,11 @@ import {GridPhoto} from "../../grid/GridPhoto"; }) export class GalleryLightboxPhotoComponent implements OnChanges { - @Input() gridPhoto:GridPhoto; + @Input() gridPhoto: GridPhoto; public imageSize = {width: "auto", height: "100"}; - @ViewChild('imgContainer') nativeElement:ElementRef; - imageLoaded:boolean = false; + imageLoaded: boolean = false; constructor() { } @@ -48,12 +47,12 @@ export class GalleryLightboxPhotoComponent implements OnChanges { console.error("cant load image"); } - public showThumbnail():boolean { + public showThumbnail(): boolean { return this.gridPhoto && !this.imageLoaded && (this.gridPhoto.isThumbnailAvailable() || this.gridPhoto.isReplacementThumbnailAvailable()); } - public thumbnailPath():string { + public thumbnailPath(): string { if (this.gridPhoto.isThumbnailAvailable() === true) return this.gridPhoto.getThumbnailPath(); diff --git a/frontend/app/gallery/map/lightbox/lightbox.map.gallery.component.ts b/frontend/app/gallery/map/lightbox/lightbox.map.gallery.component.ts index 6488f88..da62e7a 100644 --- a/frontend/app/gallery/map/lightbox/lightbox.map.gallery.component.ts +++ b/frontend/app/gallery/map/lightbox/lightbox.map.gallery.component.ts @@ -1,9 +1,9 @@ -import {Component, Input, OnChanges, ElementRef, ViewChild} from "@angular/core"; +import {Component, ElementRef, HostListener, Input, OnChanges, ViewChild} from "@angular/core"; import {PhotoDTO} from "../../../../../common/entities/PhotoDTO"; import {Dimension} from "../../../model/IRenderable"; import {FullScreenService} from "../../fullscreen.service"; import {SebmGoogleMap} from "angular2-google-maps/core"; -import {ThumbnailManagerService, IconThumbnail} from "../../thumnailManager.service"; +import {IconThumbnail, ThumbnailManagerService} from "../../thumnailManager.service"; import {IconPhoto} from "../../IconPhoto"; @Component({ @@ -138,6 +138,20 @@ export class GalleryMapLightboxComponent implements OnChanges { return window.innerHeight; } + //noinspection JSUnusedGlobalSymbols + @HostListener('window:keydown', ['$event']) + onKeyPress(e: KeyboardEvent) { + if (this.visible != true) { + return; + } + let event: KeyboardEvent = window.event ? window.event : e; + switch (event.keyCode) { + case 27: //escape + this.hide(); + break; + } + } + } diff --git a/frontend/app/gallery/overlay.service.ts b/frontend/app/gallery/overlay.service.ts new file mode 100644 index 0000000..9b137f8 --- /dev/null +++ b/frontend/app/gallery/overlay.service.ts @@ -0,0 +1,60 @@ +import {Injectable} from "@angular/core"; +import {Event} from "../../../common/event/Event"; + +@Injectable() +export class OverlayService { + + OnOverlayChange = new Event(); + private scrollWidth: number = null; + + public showOverlay() { + + //disable scrolling + document.getElementsByTagName('body')[0].style.overflow = 'hidden'; + this.OnOverlayChange.trigger(true); + } + + public hideOverlay() { + + document.getElementsByTagName('body')[0].style.overflowY = 'scroll'; + this.OnOverlayChange.trigger(false); + } + + getScrollbarWidth() { + if (this.scrollWidth == null) { + + + let outer = document.createElement("div"); + outer.style.visibility = "hidden"; + outer.style.width = "100px"; + outer.style.msOverflowStyle = "scrollbar"; // needed for WinJS apps + + document.body.appendChild(outer); + + let widthNoScroll = outer.offsetWidth; + // force scrollbars + outer.style.overflow = "scroll"; + + // add innerdiv + let inner = document.createElement("div"); + inner.style.width = "100%"; + outer.appendChild(inner); + + let widthWithScroll = inner.offsetWidth; + + // remove divs + outer.parentNode.removeChild(outer); + this.scrollWidth = widthNoScroll - widthWithScroll; + } + + return this.scrollWidth; + } + + getPhantomScrollbarWidth() { + if (document.getElementsByTagName('body')[0].style.overflow == 'hidden') { + return this.getScrollbarWidth(); + } + return 0; + } + +}