From 2f9c9c2b77235e9428eab929bfc3cd31b925a252 Mon Sep 17 00:00:00 2001 From: MiharyR Date: Tue, 9 Nov 2021 14:10:36 +0100 Subject: [PATCH] ajout du drag and drop pour ajouter une image --- .../drag-and-drop.component.html | 29 ++++ .../drag-and-drop.component.scss | 143 ++++++++++++++++++ .../drag-and-drop.component.spec.ts | 25 +++ .../drag-and-drop/drag-and-drop.component.ts | 90 +++++++++++ .../page-advertiser.component.ts | 6 +- .../popup-add-or-update-ad.component.html | 8 +- .../popup-add-or-update-ad.component.scss | 10 +- .../popup-add-or-update-ad.component.ts | 30 +++- src/app/app.module.ts | 4 + .../drag-and-drop.directive.spec.ts | 8 + .../dragAndDrop/drag-and-drop.directive.ts | 36 +++++ src/assets/uploadFile.png | Bin 0 -> 8853 bytes 12 files changed, 377 insertions(+), 12 deletions(-) create mode 100644 src/app/advertiser/drag-and-drop/drag-and-drop.component.html create mode 100644 src/app/advertiser/drag-and-drop/drag-and-drop.component.scss create mode 100644 src/app/advertiser/drag-and-drop/drag-and-drop.component.spec.ts create mode 100644 src/app/advertiser/drag-and-drop/drag-and-drop.component.ts create mode 100644 src/app/utils/directives/dragAndDrop/drag-and-drop.directive.spec.ts create mode 100644 src/app/utils/directives/dragAndDrop/drag-and-drop.directive.ts create mode 100644 src/assets/uploadFile.png diff --git a/src/app/advertiser/drag-and-drop/drag-and-drop.component.html b/src/app/advertiser/drag-and-drop/drag-and-drop.component.html new file mode 100644 index 0000000..f173baa --- /dev/null +++ b/src/app/advertiser/drag-and-drop/drag-and-drop.component.html @@ -0,0 +1,29 @@ +
+ +

Images

+ +
Glisser déposer
+
ou
+
Cliquer pour selectionner
+
+info +
+
+ file +
+

+ {{ file?.name }} +

+

+ {{ formatBytes(file?.size) }} +

+
+
+
+
+
+ +
+
diff --git a/src/app/advertiser/drag-and-drop/drag-and-drop.component.scss b/src/app/advertiser/drag-and-drop/drag-and-drop.component.scss new file mode 100644 index 0000000..4683f9f --- /dev/null +++ b/src/app/advertiser/drag-and-drop/drag-and-drop.component.scss @@ -0,0 +1,143 @@ +.container { + width: 450px; + height: 220px; + padding: 20px 0px 20px 0px; + text-align: center; + border: dashed 1px #979797; + position: relative; + margin: 0 auto; +} + +input { + opacity: 0; + position: absolute; + z-index: 2; + width: 100%; + height: 100%; + top: 0; + left: 0; +} + + +h3 { + font-size: 20px; + font-weight: 600; + color: #38424c; +} + + + +.fileover { + animation: shake 1s; + animation-iteration-count: infinite; +} + +.files-list { + margin-top: 1.5rem; + + .single-file { + display: flex; + padding: 0.5rem; + justify-content: space-between; + align-items: center; + border: dashed 1px #979797; + margin-bottom: 1rem; + + img.delete { + margin-left: 0.5rem; + cursor: pointer; + align-self: flex-end; + } + + + display: flex; + flex-grow: 1; + + .name { + font-size: 14px; + font-weight: 500; + color: #353f4a; + margin: 0; + } + + .size { + font-size: 12px; + font-weight: 500; + color: #a4a4a4; + margin: 0; + margin-bottom: 0.25rem; + } + + .info { + width: 100% + } + } +} + +/* Shake animation */ +@keyframes shake { + 0% { + transform: translate(1px, 1px) rotate(0deg); + } + + 10% { + transform: translate(-1px, -2px) rotate(-1deg); + } + + 20% { + transform: translate(-3px, 0px) rotate(1deg); + } + + 30% { + transform: translate(3px, 2px) rotate(0deg); + } + + 40% { + transform: translate(1px, -1px) rotate(1deg); + } + + 50% { + transform: translate(-1px, 2px) rotate(-1deg); + } + + 60% { + transform: translate(-3px, 1px) rotate(0deg); + } + + 70% { + transform: translate(3px, 1px) rotate(-1deg); + } + + 80% { + transform: translate(-1px, -1px) rotate(1deg); + } + + 90% { + transform: translate(1px, 2px) rotate(0deg); + } + + 100% { + transform: translate(1px, -2px) rotate(-1deg); + } +} + + +.progress-cont { + height: 7px; + width: 100%; + border-radius: 4px; + background-color: #d0d0d0; + position: relative; + + .progress { + width: 0; + height: 100%; + position: absolute; + z-index: 1; + top: 0; + left: 0; + border-radius: 4px; + background-color: #4c97cb; + transition: 0.5s all; + } +} diff --git a/src/app/advertiser/drag-and-drop/drag-and-drop.component.spec.ts b/src/app/advertiser/drag-and-drop/drag-and-drop.component.spec.ts new file mode 100644 index 0000000..e4666b0 --- /dev/null +++ b/src/app/advertiser/drag-and-drop/drag-and-drop.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DragAndDropComponent } from './drag-and-drop.component'; + +describe('DragAndDropComponent', () => { + let component: DragAndDropComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ DragAndDropComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DragAndDropComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/advertiser/drag-and-drop/drag-and-drop.component.ts b/src/app/advertiser/drag-and-drop/drag-and-drop.component.ts new file mode 100644 index 0000000..6fe4a6d --- /dev/null +++ b/src/app/advertiser/drag-and-drop/drag-and-drop.component.ts @@ -0,0 +1,90 @@ +import {Component, ElementRef, OnInit, ViewChild} from '@angular/core'; + +@Component({ + selector: 'app-drag-and-drop', + templateUrl: './drag-and-drop.component.html', + styleUrls: ['./drag-and-drop.component.scss'] +}) +export class DragAndDropComponent +{ + @ViewChild("fileDropRef", { static: false }) fileDropEl: ElementRef; + files: any[] = []; + info_image = "Vos annonces seront affichées dans un rectangle de rapport 1/5 avec: \n • 1 la largeur durectangle \n • 5 la hauteur du rectangle" ; + + + /** + * on file drop handler + */ + onFileDropped($event) { + this.prepareFilesList($event); + } + + /** + * handle file from browsing + */ + fileBrowseHandler(files) { + this.prepareFilesList(files); + } + + /** + * Delete file from files list + * @param index (File index) + */ + deleteFile(index: number) { + if (this.files[index].progress < 100) { + console.log("Upload in progress."); + return; + } + this.files.splice(index, 1); + } + + /** + * Simulate the upload process + */ + uploadFilesSimulator(index: number) { + setTimeout(() => { + if (index === this.files.length) { + return; + } else { + const progressInterval = setInterval(() => { + if (this.files[index].progress === 100) { + clearInterval(progressInterval); + this.uploadFilesSimulator(index + 1); + } else { + this.files[index].progress += 5; + } + }, 200); + } + }, 1000); + } + + /** + * Convert Files list to normal array list + * @param files (Files List) + */ + prepareFilesList(files: Array) { + for (const item of files) { + item.progress = 0; + this.files.push(item); + } + this.fileDropEl.nativeElement.value = ""; + this.uploadFilesSimulator(0); + } + + /** + * format bytes + * @param bytes (File size in bytes) + * @param decimals (Decimals point) + */ + formatBytes(bytes, decimals = 2) { + if (bytes === 0) { + return "0 Bytes"; + } + const k = 1024; + const dm = decimals <= 0 ? 0 : decimals; + const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i]; + } + +} diff --git a/src/app/advertiser/page-advertiser/page-advertiser.component.ts b/src/app/advertiser/page-advertiser/page-advertiser.component.ts index dc83d59..939ff63 100644 --- a/src/app/advertiser/page-advertiser/page-advertiser.component.ts +++ b/src/app/advertiser/page-advertiser/page-advertiser.component.ts @@ -62,7 +62,8 @@ export class PageAdvertiserComponent implements OnInit onAdd(): void { const config = { - width: '50%', + width: '40%', + height: '80%', data: { action: "add", advert: null } }; this.dialog @@ -89,7 +90,8 @@ export class PageAdvertiserComponent implements OnInit onUpdate(advertToUpdate: Advert): void { const config = { - width: '50%', + width: '40%', + height: '80%', data: { action: "update", advert: advertToUpdate } }; this.dialog diff --git a/src/app/advertiser/popup-add-or-update-ad/popup-add-or-update-ad.component.html b/src/app/advertiser/popup-add-or-update-ad/popup-add-or-update-ad.component.html index 3d4fc66..86f8a65 100644 --- a/src/app/advertiser/popup-add-or-update-ad/popup-add-or-update-ad.component.html +++ b/src/app/advertiser/popup-add-or-update-ad/popup-add-or-update-ad.component.html @@ -5,7 +5,7 @@ - +
@@ -15,9 +15,7 @@
- - - + @@ -31,7 +29,7 @@ Visible - +
diff --git a/src/app/advertiser/popup-add-or-update-ad/popup-add-or-update-ad.component.scss b/src/app/advertiser/popup-add-or-update-ad/popup-add-or-update-ad.component.scss index f7796bd..5bfabd8 100644 --- a/src/app/advertiser/popup-add-or-update-ad/popup-add-or-update-ad.component.scss +++ b/src/app/advertiser/popup-add-or-update-ad/popup-add-or-update-ad.component.scss @@ -1,5 +1,7 @@ .lightTheme, .darkTheme { background-image: none; + overflow-y: hidden; + overflow-x: hidden; } h1 { @@ -12,15 +14,15 @@ h1 { // ------------------------------------------------------------------------- -mat-dialog-content { - overflow-y: hidden; - //padding: 20px 20px 20px 20px; -} .commentContainer { width: 100%; } +.imageContainer { + border: solid 1px grey; +} + // ------------------------------------------------------------------------- // --- LightTheme --- diff --git a/src/app/advertiser/popup-add-or-update-ad/popup-add-or-update-ad.component.ts b/src/app/advertiser/popup-add-or-update-ad/popup-add-or-update-ad.component.ts index 5c81ce0..d109b04 100644 --- a/src/app/advertiser/popup-add-or-update-ad/popup-add-or-update-ad.component.ts +++ b/src/app/advertiser/popup-add-or-update-ad/popup-add-or-update-ad.component.ts @@ -29,6 +29,9 @@ export class PopupAddOrUpdateAdComponent implements OnInit advert: Advert; urlBackend: string = "" ; title: string = "" ; + tabWaitingFile: File[] = []; // fichiers selectionnés mais pas encore "validés" + tabSelectedFile: File[] = []; // fichier "validés" + _event; constructor( public dialogRef: MatDialogRef, @@ -79,9 +82,34 @@ export class PopupAddOrUpdateAdComponent implements OnInit } - onEventBarTags(myTags: string[]) + onEventBarTags(myTags: string[]): void { this.advert.tags = myTags; } + + // Lorsque l'annonceur selectionne des fichiers + onSelectFile(event) + { + const nbFileSelected = event.target.files.length ; + for(let i=0 ; i file); + this.tabSelectedFile.splice(index, 1); + } + } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 76d3331..b8dc48d 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -46,6 +46,8 @@ import {MatAutocompleteModule} from "@angular/material/autocomplete"; import {MatSelectModule} from "@angular/material/select"; import { PopupVisualizeImagesComponent } from './advertiser/popup-visualize-images/popup-visualize-images.component'; import {IvyCarouselModule} from "angular-responsive-carousel"; +import { DragAndDropComponent } from './advertiser/drag-and-drop/drag-and-drop.component'; +import { DragAndDropDirective } from './utils/directives/dragAndDrop/drag-and-drop.directive'; @NgModule({ @@ -75,6 +77,8 @@ import {IvyCarouselModule} from "angular-responsive-carousel"; PopupVisualizeAdComponent, BarTagsComponent, PopupVisualizeImagesComponent, + DragAndDropComponent, + DragAndDropDirective, ], imports: [ BrowserModule, diff --git a/src/app/utils/directives/dragAndDrop/drag-and-drop.directive.spec.ts b/src/app/utils/directives/dragAndDrop/drag-and-drop.directive.spec.ts new file mode 100644 index 0000000..60cf3d6 --- /dev/null +++ b/src/app/utils/directives/dragAndDrop/drag-and-drop.directive.spec.ts @@ -0,0 +1,8 @@ +import { DragAndDropDirective } from './drag-and-drop.directive'; + +describe('DragAndDropDirective', () => { + it('should create an instance', () => { + const directive = new DragAndDropDirective(); + expect(directive).toBeTruthy(); + }); +}); diff --git a/src/app/utils/directives/dragAndDrop/drag-and-drop.directive.ts b/src/app/utils/directives/dragAndDrop/drag-and-drop.directive.ts new file mode 100644 index 0000000..b3d1162 --- /dev/null +++ b/src/app/utils/directives/dragAndDrop/drag-and-drop.directive.ts @@ -0,0 +1,36 @@ +import {Directive, EventEmitter, HostBinding, HostListener, Output} from '@angular/core'; + +@Directive({ + selector: '[appDragAndDrop]' +}) +export class DragAndDropDirective +{ + @HostBinding('class.fileover') fileOver: boolean; + @Output() fileDropped = new EventEmitter(); + + // Dragover listener + @HostListener('dragover', ['$event']) onDragOver(evt) { + evt.preventDefault(); + evt.stopPropagation(); + this.fileOver = true; + } + + // Dragleave listener + @HostListener('dragleave', ['$event']) public onDragLeave(evt) { + evt.preventDefault(); + evt.stopPropagation(); + this.fileOver = false; + } + + // Drop listener + @HostListener('drop', ['$event']) public ondrop(evt) { + evt.preventDefault(); + evt.stopPropagation(); + this.fileOver = false; + let files = evt.dataTransfer.files; + if (files.length > 0) { + this.fileDropped.emit(files); + } + } + +} diff --git a/src/assets/uploadFile.png b/src/assets/uploadFile.png new file mode 100644 index 0000000000000000000000000000000000000000..cff9f3853d882c8079b4aeaffa40aa2b728d57fd GIT binary patch literal 8853 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 AOaK4? literal 0 HcmV?d00001