From 5aefe19da6ecb791cf7c039cf7e2738dd77a1729 Mon Sep 17 00:00:00 2001 From: Florent Poittevin <florent.poittevin@unige.ch> Date: Thu, 5 Dec 2024 16:21:48 +0100 Subject: [PATCH] feat: improve file drop zone directive --- .../data-test/data-test.directive.ts | 1 - .../file-drop-zone.directive.ts | 135 ++++++++++++------ .../src/lib/core/scss/abstracts/_mixins.scss | 11 ++ .../override/default-browser-override.scss | 14 +- ...abstract-image-upload-wrapper.container.ts | 19 +-- .../avatar-upload-wrapper.container.html | 6 +- .../avatar-upload-wrapper.container.ts | 3 +- .../image-upload-wrapper.container.html | 5 +- .../image-upload-wrapper.container.ts | 3 +- .../upload-image/upload-image.dialog.html | 2 +- .../upload-image/upload-image.dialog.ts | 5 +- .../file-upload-input.container.html | 4 +- .../file-upload-input.container.ts | 8 +- 13 files changed, 151 insertions(+), 65 deletions(-) diff --git a/projects/solidify-frontend/src/lib/core/directives/data-test/data-test.directive.ts b/projects/solidify-frontend/src/lib/core/directives/data-test/data-test.directive.ts index d7e91a471..6e96e99ae 100644 --- a/projects/solidify-frontend/src/lib/core/directives/data-test/data-test.directive.ts +++ b/projects/solidify-frontend/src/lib/core/directives/data-test/data-test.directive.ts @@ -21,7 +21,6 @@ * ----------------------------------------------------------------------------------------------%% */ - import { Directive, ElementRef, diff --git a/projects/solidify-frontend/src/lib/core/directives/file-drop-zone/file-drop-zone.directive.ts b/projects/solidify-frontend/src/lib/core/directives/file-drop-zone/file-drop-zone.directive.ts index 219f3ae63..f5bce8e8f 100644 --- a/projects/solidify-frontend/src/lib/core/directives/file-drop-zone/file-drop-zone.directive.ts +++ b/projects/solidify-frontend/src/lib/core/directives/file-drop-zone/file-drop-zone.directive.ts @@ -21,12 +21,12 @@ * ----------------------------------------------------------------------------------------------%% */ - import { Directive, ElementRef, HostBinding, HostListener, + Input, OnInit, Output, Renderer2, @@ -35,26 +35,23 @@ import { BehaviorSubject, Observable, } from "rxjs"; -import { - debounceTime, - distinctUntilChanged, - filter, - tap, -} from "rxjs/operators"; +import {SOLIDIFY_CONSTANTS} from "../../constants"; import {ObservableUtil} from "../../utils/observable.util"; +import {SsrUtil} from "../../utils/ssr.util"; import {CoreAbstractDirective} from "../core-abstract/core-abstract.directive"; @Directive({ selector: "[solidifyFileDropZone]", }) export class FileDropZoneDirective extends CoreAbstractDirective implements OnInit { - @HostBinding("class.file-over") - get isFileOver(): boolean { - return this._fileOverBS.value; - } + private readonly _DIRECTIVE_CLASS: string = "file-drop-zone"; + private readonly _ATTRIBUTE_IS_FILE_DRAG: string = "is-file-drag"; + + private _windowDragFileCounter: number = 0; + private _elementDragFileCounter: number = 0; - @HostBinding("class.pointer-events-all-child") - preventEventsAllChild: boolean; + @Input("solidifyFileDropZoneDisabled") + dragDisabled: boolean = false; private readonly _fileDroppedBS: BehaviorSubject<FileList | undefined> = new BehaviorSubject<FileList | undefined>(undefined); @Output("fileDropped") @@ -64,48 +61,106 @@ export class FileDropZoneDirective extends CoreAbstractDirective implements OnIn @Output("fileOver") readonly fileOverObs: Observable<boolean | undefined> = ObservableUtil.asObservable(this._fileOverBS); - constructor(private readonly _elementRef: ElementRef, - private readonly _renderer: Renderer2) { + constructor(private readonly _renderer: Renderer2, + private readonly _elementRef: ElementRef) { super(); + _renderer.addClass(_elementRef.nativeElement, this._DIRECTIVE_CLASS); } - ngOnInit(): void { - super.ngOnInit(); - - this.subscribe(this.fileOverObs.pipe( - distinctUntilChanged(), - filter(isOver => !isOver), - debounceTime(250), - filter(isOver => !this._fileOverBS.value), - tap(isOver => { - this._renderer.removeClass(this._elementRef.nativeElement, "pointer-events-all-child"); - }), - )); + @HostBinding("class.file-over") + get isFileOver(): boolean { + return this._fileOverBS.value; } - @HostListener("dragover", ["$event"]) - onDragOver($event: Event): void { + @HostListener("window:dragenter") + onDragEnterWindow(): void { + if (this.dragDisabled === true) { + return; + } + this._windowDragFileCounter++; + this._computeWindowDragFileClass(); + } + + @HostListener("window:dragover", ["$event"]) + onDragOverWindow($event: Event): void { + if (this.dragDisabled === true) { + return; + } $event.preventDefault(); - $event.stopPropagation(); - this._fileOverBS.next(true); - this._renderer.addClass(this._elementRef.nativeElement, "pointer-events-all-child"); } - @HostListener("dragleave", ["$event"]) - public onDragLeave($event: Event): void { + @HostListener("window:dragleave") + onDragLeaveWindow(): void { + if (this.dragDisabled === true) { + return; + } + this._windowDragFileCounter--; + this._computeWindowDragFileClass(); + } + + @HostListener("window:drop", ["$event"]) + onDropWindow($event: DragEvent): void { + if (this.dragDisabled === true) { + return; + } + $event.preventDefault(); // avoid to open file dragged in elemet in new tab + this._windowDragFileCounter = 0; + this._computeWindowDragFileClass(); + } + + @HostListener("dragenter") + onDragEnterElement(): void { + if (this.dragDisabled === true) { + return; + } + this._elementDragFileCounter++; + this._computeElementDragFile(); + } + + @HostListener("dragover", ["$event"]) + onDragOverElement($event: Event): void { + if (this.dragDisabled === true) { + return; + } $event.preventDefault(); - $event.stopPropagation(); - this._fileOverBS.next(false); + } + + @HostListener("dragleave") + onDragLeaveElement(): void { + if (this.dragDisabled === true) { + return; + } + this._elementDragFileCounter--; + this._computeElementDragFile(); } @HostListener("drop", ["$event"]) - public ondrop($event: DragEvent): void { - $event.preventDefault(); - $event.stopPropagation(); - this._fileOverBS.next(false); + onDropElement($event: DragEvent): void { + if (this.dragDisabled === true) { + return; + } + $event.preventDefault(); // avoid to open file dragged in elemet in new tab + this._elementDragFileCounter = 0; + this._computeElementDragFile(); const files = $event.dataTransfer.files; if (files.length > 0) { this._fileDroppedBS.next(files); } } + + private _computeWindowDragFileClass(): void { + if (this._windowDragFileCounter > 0) { + this._renderer.setAttribute(SsrUtil.document.body, this._ATTRIBUTE_IS_FILE_DRAG, SOLIDIFY_CONSTANTS.STRING_TRUE); + } else { + this._renderer.removeAttribute(SsrUtil.document.body, this._ATTRIBUTE_IS_FILE_DRAG); + } + } + + private _computeElementDragFile(): void { + if (this._elementDragFileCounter > 0 && this.isFileOver !== true) { + this._fileOverBS.next(true); + } else if (this._elementDragFileCounter === 0 && this.isFileOver === true) { + this._fileOverBS.next(false); + } + } } diff --git a/projects/solidify-frontend/src/lib/core/scss/abstracts/_mixins.scss b/projects/solidify-frontend/src/lib/core/scss/abstracts/_mixins.scss index a6aa00552..622e2fa0a 100644 --- a/projects/solidify-frontend/src/lib/core/scss/abstracts/_mixins.scss +++ b/projects/solidify-frontend/src/lib/core/scss/abstracts/_mixins.scss @@ -202,3 +202,14 @@ $breakpoints-media-interval: ( } } +@mixin isFileDrag($inGlobalScss: false) { + @if $inGlobalScss { + body[is-file-drag='true'] { + @content; + } + } @else { + :host-context(body[is-file-drag='true']) { + @content; + } + } +} diff --git a/projects/solidify-frontend/src/lib/core/scss/override/default-browser-override.scss b/projects/solidify-frontend/src/lib/core/scss/override/default-browser-override.scss index d273444f0..afac4e2c4 100644 --- a/projects/solidify-frontend/src/lib/core/scss/override/default-browser-override.scss +++ b/projects/solidify-frontend/src/lib/core/scss/override/default-browser-override.scss @@ -134,13 +134,19 @@ dl { margin: 0; } -.pointer-events-all-child { - * { - pointer-events: none; +@include isFileDrag(true) { + .file-drop-zone { + outline: 4px dashed $intermediate-grey; + outline-offset: 5px; + + &.file-over { + outline-offset: 8px; + transition: all 0.1s ease-in-out 0s; + outline-color: $primary-color; + } } } - .solidify-alternative-button { background-color: transparent; border-radius: 50px; diff --git a/projects/solidify-frontend/src/lib/image/components/containers/abstract-image-upload-wrapper/abstract-image-upload-wrapper.container.ts b/projects/solidify-frontend/src/lib/image/components/containers/abstract-image-upload-wrapper/abstract-image-upload-wrapper.container.ts index 833910679..6c3e1f982 100644 --- a/projects/solidify-frontend/src/lib/image/components/containers/abstract-image-upload-wrapper/abstract-image-upload-wrapper.container.ts +++ b/projects/solidify-frontend/src/lib/image/components/containers/abstract-image-upload-wrapper/abstract-image-upload-wrapper.container.ts @@ -124,7 +124,7 @@ export abstract class AbstractImageUploadWrapperContainer extends AbstractIntern return this._imagePendingUploadUrl; } - @ViewChild("fileInput", {static: false}) + @ViewChild("fileInput") fileInput: ElementRef; protected readonly _logoChangeBS: BehaviorSubject<Blob | undefined | null> = new BehaviorSubject<Blob | undefined | null>(undefined); @@ -190,28 +190,32 @@ export abstract class AbstractImageUploadWrapperContainer extends AbstractIntern } } - fileChangeEvent(event: FileEvent): void { + fileChangeEventByInput(event: FileEvent): void { const target = event.target; if (isNullOrUndefined(target.value) || isEmptyString(target.value)) { return; } - const file = target.files[0]; + this.fileChangeEvent(target.files); + this.fileInput.nativeElement.value = null; + } + + fileChangeEvent(files: FileList): void { + const file = files[0]; if (this._LIST_FORMAT_BYPASS_CROP.includes(file.type)) { this._uploadImage(file); } else { - this._cropImage(event); + this._cropImage(file); } } - private _cropImage(event: Event): void { - this._sharedUploadImageDialogData.imageChangedEvent = event; + private _cropImage(file: File): void { + this._sharedUploadImageDialogData.file = file; this.subscribe(DialogUtil.open(this._dialog, UploadImageDialog, this._sharedUploadImageDialogData, { width: "90%", }, blob => { this._uploadImage(blob); this._cd.detectChanges(); }, () => { - this.fileInput.nativeElement.value = null; })); } @@ -230,6 +234,5 @@ export abstract class AbstractImageUploadWrapperContainer extends AbstractIntern this.imagePendingUpload = blob; this._logoChangeBS.next(blob); } - this.fileInput.nativeElement.value = null; } } diff --git a/projects/solidify-frontend/src/lib/image/components/containers/avatar-upload-wrapper/avatar-upload-wrapper.container.html b/projects/solidify-frontend/src/lib/image/components/containers/avatar-upload-wrapper/avatar-upload-wrapper.container.html index 0734ce14d..d34641cbe 100644 --- a/projects/solidify-frontend/src/lib/image/components/containers/avatar-upload-wrapper/avatar-upload-wrapper.container.html +++ b/projects/solidify-frontend/src/lib/image/components/containers/avatar-upload-wrapper/avatar-upload-wrapper.container.html @@ -2,6 +2,10 @@ [class.is-readonly]="!isEditable" [solidifySpinner]="isLoadingObs | async" class="wrapper-avatar" + solidifyFileDropZone + (fileDropped)="fileChangeEvent($event)" + [solidifyFileDropZoneDisabled]="!isEditable" + > <div *ngIf="imagePendingUploadUrl; else noPhoto" class="photo-wrapper" @@ -33,7 +37,7 @@ </div> <input #fileInput - (change)="fileChangeEvent($any($event))" + (change)="fileChangeEventByInput($any($event))" [accept]="filesAccepted" class="hide" type="file" diff --git a/projects/solidify-frontend/src/lib/image/components/containers/avatar-upload-wrapper/avatar-upload-wrapper.container.ts b/projects/solidify-frontend/src/lib/image/components/containers/avatar-upload-wrapper/avatar-upload-wrapper.container.ts index abb4fc940..45b19fdda 100644 --- a/projects/solidify-frontend/src/lib/image/components/containers/avatar-upload-wrapper/avatar-upload-wrapper.container.ts +++ b/projects/solidify-frontend/src/lib/image/components/containers/avatar-upload-wrapper/avatar-upload-wrapper.container.ts @@ -39,6 +39,7 @@ import { Actions, Store, } from "@ngxs/store"; +import {by} from "ng-packagr/lib/graph/select"; import { take, tap, @@ -105,7 +106,7 @@ export class AvatarUploadWrapperContainer extends AbstractImageUploadWrapperCont confirmToTranslate: this.labelTranslateInterface.imageUploadAvatarDialogConfirm, cancelToTranslate: this.labelTranslateInterface.imageUploadAvatarDialogCancel, imageNotSupportedToTranslate: this.labelTranslateInterface.imageUploadAvatarNotSupported, - imageChangedEvent: undefined, + file: undefined, isLoadingObs: this.isLoadingObs, aspectRatio: this.aspectRatio, roundCropped: this.roundCropped, diff --git a/projects/solidify-frontend/src/lib/image/components/containers/image-upload-wrapper/image-upload-wrapper.container.html b/projects/solidify-frontend/src/lib/image/components/containers/image-upload-wrapper/image-upload-wrapper.container.html index 5b885d9f7..238a770b6 100644 --- a/projects/solidify-frontend/src/lib/image/components/containers/image-upload-wrapper/image-upload-wrapper.container.html +++ b/projects/solidify-frontend/src/lib/image/components/containers/image-upload-wrapper/image-upload-wrapper.container.html @@ -2,6 +2,9 @@ [class.is-readonly]="!isEditable" [solidifySpinner]="isLoadingObs | async" class="wrapper-avatar" + solidifyFileDropZone + (fileDropped)="fileChangeEvent($event)" + [solidifyFileDropZoneDisabled]="!isEditable" > <div *ngIf="imagePendingUploadUrl; else noPhoto" class="photo-wrapper" @@ -39,7 +42,7 @@ </div> <input #fileInput - (change)="fileChangeEvent($any($event))" + (change)="fileChangeEventByInput($any($event))" [accept]="filesAccepted" class="hide" type="file" diff --git a/projects/solidify-frontend/src/lib/image/components/containers/image-upload-wrapper/image-upload-wrapper.container.ts b/projects/solidify-frontend/src/lib/image/components/containers/image-upload-wrapper/image-upload-wrapper.container.ts index e89c9d62c..a3fca7f3a 100644 --- a/projects/solidify-frontend/src/lib/image/components/containers/image-upload-wrapper/image-upload-wrapper.container.ts +++ b/projects/solidify-frontend/src/lib/image/components/containers/image-upload-wrapper/image-upload-wrapper.container.ts @@ -21,7 +21,6 @@ * ----------------------------------------------------------------------------------------------%% */ - import { ChangeDetectionStrategy, ChangeDetectorRef, @@ -91,7 +90,7 @@ export class ImageUploadWrapperContainer extends AbstractImageUploadWrapperConta confirmToTranslate: this.labelTranslateInterface.imageUploadImageDialogConfirm, cancelToTranslate: this.labelTranslateInterface.imageUploadImageDialogCancel, imageNotSupportedToTranslate: this.labelTranslateInterface.imageUploadImageNotSupported, - imageChangedEvent: undefined, + file: undefined, isLoadingObs: this.isLoadingObs, aspectRatio: this.aspectRatio, roundCropped: this.roundCropped, diff --git a/projects/solidify-frontend/src/lib/image/components/dialogs/upload-image/upload-image.dialog.html b/projects/solidify-frontend/src/lib/image/components/dialogs/upload-image/upload-image.dialog.html index 2a4b9a8e0..06ede3c54 100644 --- a/projects/solidify-frontend/src/lib/image/components/dialogs/upload-image/upload-image.dialog.html +++ b/projects/solidify-frontend/src/lib/image/components/dialogs/upload-image/upload-image.dialog.html @@ -10,7 +10,7 @@ [maintainAspectRatio]="data.aspectRatio | isNumber" [aspectRatio]="data.aspectRatio" [containWithinAspectRatio]="data.aspectRatio | isNumber" - [imageChangedEvent]="data.imageChangedEvent" + [imageFile]="data.file" [imageQuality]="80" [onlyScaleDown]="true" [resizeToHeight]="data.resizeToHeight" diff --git a/projects/solidify-frontend/src/lib/image/components/dialogs/upload-image/upload-image.dialog.ts b/projects/solidify-frontend/src/lib/image/components/dialogs/upload-image/upload-image.dialog.ts index 2b42c48ec..f28f60644 100644 --- a/projects/solidify-frontend/src/lib/image/components/dialogs/upload-image/upload-image.dialog.ts +++ b/projects/solidify-frontend/src/lib/image/components/dialogs/upload-image/upload-image.dialog.ts @@ -82,8 +82,7 @@ export class UploadImageDialog extends AbstractInternalDialog<UploadImageDialogD confirm(): void { const blob = this.croppedImage; - const fileList: FileList = this.data.imageChangedEvent.target.files; - Object.assign(blob, {name: fileList.item(0).name}); + Object.assign(blob, {name: this.data.file.name}); this.submit(blob); } @@ -107,7 +106,7 @@ export interface UploadImageDialogData { confirmToTranslate: string; cancelToTranslate: string; imageNotSupportedToTranslate: string; - imageChangedEvent: any; + file: File; isLoadingObs: Observable<boolean>; aspectRatio: number; roundCropped: boolean; diff --git a/projects/solidify-frontend/src/lib/input/containers/file-upload-input/file-upload-input.container.html b/projects/solidify-frontend/src/lib/input/containers/file-upload-input/file-upload-input.container.html index a142d317f..748ec545e 100644 --- a/projects/solidify-frontend/src/lib/input/containers/file-upload-input/file-upload-input.container.html +++ b/projects/solidify-frontend/src/lib/input/containers/file-upload-input/file-upload-input.container.html @@ -73,13 +73,15 @@ solidifyAlternativeButton class="browse-button" type="button" + solidifyFileDropZone + (fileDropped)="onFileChange($event)" > <solidify-icon [iconName]="uploadButtonIcon" class="icon" ></solidify-icon> <span>{{uploadButtonLabelToTranslate | translate}}</span> <input #fileInput - (change)="onFileChange($any($event))" + (change)="onFileChangeByInput($any($event))" class="hide" type="file" > diff --git a/projects/solidify-frontend/src/lib/input/containers/file-upload-input/file-upload-input.container.ts b/projects/solidify-frontend/src/lib/input/containers/file-upload-input/file-upload-input.container.ts index a3b01c0f8..9aa994051 100644 --- a/projects/solidify-frontend/src/lib/input/containers/file-upload-input/file-upload-input.container.ts +++ b/projects/solidify-frontend/src/lib/input/containers/file-upload-input/file-upload-input.container.ts @@ -324,16 +324,20 @@ export class FileUploadInputContainer extends AbstractInternalContainer implemen return fileUploadType; } - onFileChange(event: any): void { + onFileChangeByInput(event: any): void { if (event.target.files.length > 0) { const files: FileList = event.target.files; - this.browserFile = files[0]; + this.onFileChange(files); } if (isNotNullNorUndefined(this.fileInput)) { this.fileInput.nativeElement.value = null; } } + onFileChange(files: FileList): void { + this.browserFile = files[0]; + } + deleteFile(): void { this.browserFile = null; } -- GitLab