From c659f9e67883bb3f5aa0024e702bf71422560697 Mon Sep 17 00:00:00 2001 From: damiandominella Date: Fri, 22 Jun 2018 18:00:04 +0200 Subject: [PATCH 1/6] first --- .../form-type-switcher.component.html | 9 + src/app/panel/components/form/form.config.ts | 1 + .../form/interfaces/form-field-file.ts | 8 + .../cloudinary-library.component.html | 106 +++++++ .../cloudinary-library.component.scss | 285 ++++++++++++++++++ .../cloudinary-library.component.spec.ts | 25 ++ .../cloudinary-library.component.ts | 282 +++++++++++++++++ src/app/panel/panel.module.ts | 9 +- 8 files changed, 722 insertions(+), 3 deletions(-) create mode 100644 src/app/panel/components/form/types/cloudinary-library/cloudinary-library.component.html create mode 100644 src/app/panel/components/form/types/cloudinary-library/cloudinary-library.component.scss create mode 100644 src/app/panel/components/form/types/cloudinary-library/cloudinary-library.component.spec.ts create mode 100644 src/app/panel/components/form/types/cloudinary-library/cloudinary-library.component.ts diff --git a/src/app/panel/components/form/form-type-switcher/form-type-switcher.component.html b/src/app/panel/components/form/form-type-switcher/form-type-switcher.component.html index d6ad7443..8c57d550 100644 --- a/src/app/panel/components/form/form-type-switcher/form-type-switcher.component.html +++ b/src/app/panel/components/form/form-type-switcher/form-type-switcher.component.html @@ -192,6 +192,15 @@ [index]="index" > + + +
+
+
+ Cloudinary library +
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+ + +
+
+ {{media.name}} + {{media.type}} +
+
+
+ + +
+
+
+ +
+ {{activeMedia.name}} + {{activeMedia.type}} +
+
+

Tags:

+
    +
  • {{tag.name}} + × +
  • +
+
+ × +
+
+
+
+
+ \ No newline at end of file diff --git a/src/app/panel/components/form/types/cloudinary-library/cloudinary-library.component.scss b/src/app/panel/components/form/types/cloudinary-library/cloudinary-library.component.scss new file mode 100644 index 00000000..bf8a9adb --- /dev/null +++ b/src/app/panel/components/form/types/cloudinary-library/cloudinary-library.component.scss @@ -0,0 +1,285 @@ +@import '../../../../../../assets/sass/partials/imports'; + +app-cloudinary-library { + div.cloudinary-library { + border: 1px solid $primary; + margin-top: 15px; + > div.header { + position: relative; + padding: 10px 20px; + background-color: $primary; + color: #ffffff; + font-weight: $regular; + > span { + font-weight: $light; + &.remove { + cursor: pointer; + position: absolute; + font-size: 18px; + top: 5px; + right: 8px; + } + } + } + > div.container-fluid#filters-wrapper { + margin-top: 20px; + > div.filters { + div.input-wrapper { + position: relative; + > input { + height: 40px; + padding-right: 30px; + } + > i { + position: absolute; + top: 11px; + font-size: 18px; + right: 8px; + color: $default; + } + } + + ng-select.ng-select.bottom { + > div.ng-control { + min-width: 150px; + height: 40px; + font-weight: $light; + } + } + } + } + > div.container-fluid#library-wrapper { + margin-top: 20px; + min-height: 500px; + overflow: hidden; + $sidebarWidth: 300px; + position: relative; + padding: 15px 20px; + > div.medias { + > div.row { + &.library { + > div { + margin: 15px 0; + cursor: pointer; + > * { + pointer-events: none; + } + > div.media { + border: 2px solid $lightGrey; + border-bottom: none; + position: relative; + padding-top: 100%; + + > img { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + } + + &.selected { + border: 2px solid $primary; + border-bottom: none; + + div.info { + border: 2px solid $primary; + border-top: none; + } + > span.selected { + display: block; + } + } + + > span.selected { + display: none; + position: absolute; + top: 0; + right: 0; + padding: 2px 5px; + background: $primary; + color: #ffffff; + } + } + > div.info { + background-color: #ffffff; + border: 2px solid $lightGrey; + border-top: none; + padding: 3px 5px; + > span { + @include ellipsis-multiline(1, 12px, 1.3); + font-size: 12px; + display: block; + &.name { + font-weight: $regular; + } + } + } + } + } + &.selection { + margin-top: 15px; + > div { + margin-left: -5px; + > p { + color: $default; + font-size: 13px; + margin-left: 5px; + } + > div.media { + display: inline-block; + border: 1px solid $lightGrey; + margin: 5px; + > img { + width: 60px; + height: 60px; + } + > span { + cursor: pointer; + display: block; + width: 60px; + color: $default; + text-align: center; + } + } + } + } + &.footer { + margin-top: 15px; + > div { + > nav.pagination { + margin: 0; + > ul { + margin: 0; + padding-left: 0; + display: inline-block; + list-style: none; + > li { + display: inline; + &.disabled { + > span, > a { + cursor: not-allowed; + &:hover { + background-color: inherit; + } + } + } + &.active { + > span, > a { + border-color: $primary; + background-color: $primary; + color: #ffffff; + &:hover { + border-color: darken($primary, 10); + background-color: darken($primary, 10); + color: #ffffff; + } + > span { + color: #ffffff; + } + } + } + > a, > span { + @include _transition(all 0.3s); + padding: 6px 12px; + cursor: pointer; + color: $primary; + border-radius: $borderRadius; + position: relative; + float: left; + margin-left: -1px; + text-decoration: none; + background-color: #fff; + border: 1px solid $lightGrey; + + &:hover { + background-color: #eee; + } + } + } + } + } + } + } + } + + float: left; + @include _transition(all 0.5s); + width: calc(100% - #{$sidebarWidth}); + } + > div.media-detail { + width: $sidebarWidth; + overflow-y: auto; + position: absolute; + height: 100%; + top: 0; + right: 0; + border-top: 1px solid $grey; + border-left: 1px solid $grey; + @include _transition(all 0.5s); + + > .wrapper { + position: relative; + > img { + border-bottom: 1px solid $grey; + width: 100%; + } + > div.info { + border-bottom: 1px solid $lightGrey; + border-top: none; + padding: 3px 5px; + > span { + font-size: 14px; + display: block; + &.name { + font-weight: $regular; + } + } + } + > div.tags { + padding: 10px; + > ul { + list-style: none; + margin: 0; + padding: 0; + > li { + display: inline-block; + border: 1px solid $sidebar; + padding: 2px 10px; + background: $primary; + color: #fff; + font-size: 13px; + margin-bottom: 10px; + &:not(:last-child) { + margin-right: 10px; + } + + > span.remove { + margin-left: 5px; + cursor: pointer; + font-size: 15px; + font-weight: $regular; + } + } + } + } + > span.remove { + cursor: pointer; + position: absolute; + font-size: 18px; + top: 5px; + right: 8px; + } + } + } + + &.media-closed { + > .media-detail { + @include _transform(translateX(100%)); + } + > .medias { + width: 100%; + } + } + } + } +} \ No newline at end of file diff --git a/src/app/panel/components/form/types/cloudinary-library/cloudinary-library.component.spec.ts b/src/app/panel/components/form/types/cloudinary-library/cloudinary-library.component.spec.ts new file mode 100644 index 00000000..fa58dfb2 --- /dev/null +++ b/src/app/panel/components/form/types/cloudinary-library/cloudinary-library.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CloudinaryLibraryComponent } from './cloudinary-library.component'; + +describe('CloudinaryLibraryComponent', () => { + let component: CloudinaryLibraryComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ CloudinaryLibraryComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CloudinaryLibraryComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/panel/components/form/types/cloudinary-library/cloudinary-library.component.ts b/src/app/panel/components/form/types/cloudinary-library/cloudinary-library.component.ts new file mode 100644 index 00000000..2b4f3179 --- /dev/null +++ b/src/app/panel/components/form/types/cloudinary-library/cloudinary-library.component.ts @@ -0,0 +1,282 @@ +import { + ChangeDetectorRef, + Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChange, + ViewEncapsulation +} from '@angular/core'; +import { + CloudinaryField, + Media, + MediaLibraryParams, + UploadedFile +} from '../../interfaces/form-field-file'; +import {ApiService, ErrorResponse} from '../../../../../api/api.service'; +import {UtilsService} from '../../../../../services/utils.service'; +import {ToastsService} from '../../../../../services/toasts.service'; +import {environment} from '../../../../../../environments/environment'; +import {FormControl} from '@angular/forms'; +import 'rxjs/add/operator/distinctUntilChanged'; +import 'rxjs/add/operator/debounceTime'; +import 'rxjs/add/operator/skip'; +import {Subscription} from 'rxjs/Subscription'; +import {BaseInputComponent} from '../base-input/base-input.component'; +import {debounceTime, distinctUntilChanged, switchMap} from "rxjs/operators"; +import {SelectData} from "../select/select.component"; + +declare const $: any; + +@Component({ + selector: 'app-cloudinary-library', + templateUrl: './cloudinary-library.component.html', + styleUrls: ['./cloudinary-library.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class CloudinaryLibraryComponent extends BaseInputComponent implements OnInit, OnDestroy { + + @Input() field: CloudinaryField; + + isLoading: boolean; + data: Media[] = []; + activeMedia: Media = null; + selection: Media[]; + count: number; + pages: number[]; + + params: MediaLibraryParams; + + // Search + typeAhead: EventEmitter = new EventEmitter(); + searchOptions: SelectData[]; + search: any; + + subscriptionInputControl = Subscription.EMPTY; + + constructor(private _apiService: ApiService, + private _cd: ChangeDetectorRef, + private _toast: ToastsService) { + super(); + } + + ngOnInit() { + this.isLoading = false; + this.count = 0; + this.reset(); + this.initPages(); + this.typeListener(); + } + + ngOnDestroy() { + this.subscriptionInputControl.unsubscribe(); + } + + /* When type in select */ + private typeListener(): void { + this.typeAhead.pipe( + distinctUntilChanged(), + debounceTime(300), + switchMap(tag => this._apiService.get(this.field.options.searchEndpoint, {search: tag})) + ).subscribe(data => { + this._cd.markForCheck(); + this.searchOptions = data; + }, (err) => { + console.log(err); + this.searchOptions = []; + }); + } + + reset() { + this.params = { + page: 1, + perPage: 12, + search: null, + }; + this.data = [{ + id: 1, + type: 'image/jpg', + url: 'http://via.placeholder.com/350x150/green', + name: 'test image', + tags: [{name: 'tag1'}, {name: 'tag2'}] + }, + { + id: 2, + type: 'image/jpg', + url: 'http://placehold.it/200x200?color=green', + name: 'test image 2' + }]; + this.selection = []; + } + + closeMedia() { + $('#library-wrapper').addClass('media-closed'); + setTimeout(() => { + this.activeMedia = null; + }, 500); + } + + removeTag(tag: {id: number, name: string}) { + // TODO: remove tag from activeMedia + } + + onImageError($event: any): void { + $event.target.src = environment.assets.imageError; + } + + shouldShowPagination(): boolean { + return this.count > this.params.perPage; + } + + initPages() { + const pagesCount = this.getLastPage(); + let showPagesCount = 4; + showPagesCount = pagesCount < showPagesCount ? pagesCount : showPagesCount; + this.pages = []; + + if (this.shouldShowPagination()) { + + let middleOne = Math.ceil(showPagesCount / 2); + middleOne = this.params.page >= middleOne ? this.params.page : middleOne; + + let lastOne = middleOne + Math.floor(showPagesCount / 2); + lastOne = lastOne >= pagesCount ? pagesCount : lastOne; + + const firstOne = lastOne - showPagesCount + 1; + + for (let i = firstOne; i <= lastOne; i++) { + this.pages.push(i); + } + } + } + + paginate(page: number): boolean { + this.params.page = page; + this.getData(); + this.initPages(); + return false; + } + + getCurrentPage(): number { + return this.params.page; + } + + getPages(): number[] { + return this.pages; + } + + getLastPage(): number { + return Math.ceil(this.count / this.params.perPage); + } + + selectMedia(event: any, media: Media): void { + this.activeMedia = media; + $('#library-wrapper').removeClass('media-closed'); + /*const index = UtilsService.containsObject(media, this.selection); + if (index > -1) { + media.selected = false; + this.selection.splice(index, 1); + } else { + if ((this.maxFiles > 0 && this.selection.length < this.maxFiles) || this.maxFiles === 0) { + media.selected = true; + this.selection.push(media); + } + }*/ + } + + onSearch() { + console.log(this.search); + // TODO: search and reload library + } + + /*private onFilterChange() { + this.subscriptionInputControl = this.inputControl.valueChanges.skip(1).distinctUntilChanged().debounceTime(300) + .subscribe((value: string) => { + + }); + } + + private reload(reset?: boolean) { + if (reset) { + this.reset(); + } + this.getCount() + .then((res: { count: number }) => { + this.count = res.count; + this.initPages(); + this.getData(); + }) + .catch((response: ErrorResponse) => { + this.isLoading = false; + this._toast.error(response.error); + this.close(); + }); + } + + private getCount(): Promise { + this.composeParams(); + return this._apiService.get(this.options.endpoint + '/count', this.params); + } + + private getData(): void { + this.isLoading = true; + this.composeParams(); + this._apiService.get(this.options.endpoint, this.params) + .then((data) => { + this.isLoading = false; + this.data = data; + this.checkSelection(); + }) + .catch((response: ErrorResponse) => { + this.isLoading = false; + this._toast.error(response.error); + this.close(); + }); + } + + private composeParams(): void { + if (this.uploadedFiles && this.uploadedFiles.length > 0) { + const skipIds = []; + this.uploadedFiles.forEach((file: UploadedFile) => { + skipIds.push(file.id); + }); + this.params.skipIds = JSON.stringify(skipIds); + } + + if (!this.params.search) { + this.params.search = ''; + } + } + + unSelectMedia(event: any, media: Media): void { + const index = UtilsService.containsObject(media, this.selection); + if (index > -1) { + media.selected = false; + this.selection.splice(index, 1); + } + + this.data.forEach((item: Media) => { + if (item.id === media.id) { + item.selected = false; + } + }); + } + + private checkSelection(): void { + this.data.forEach((media: Media) => { + this.selection.forEach((selected: Media) => { + if (media.id === selected.id) { + media.selected = true; + } + }); + }); + } + + close(): void { + this.confirm.emit(); + this.subscriptionInputControl.unsubscribe(); + this.reset(); + } + + confirmSelection(): void { + this.confirm.emit(this.selection); + this.subscriptionInputControl.unsubscribe(); + this.reset(); + } */ +} diff --git a/src/app/panel/panel.module.ts b/src/app/panel/panel.module.ts index 242d9ebe..53751362 100644 --- a/src/app/panel/panel.module.ts +++ b/src/app/panel/panel.module.ts @@ -59,6 +59,8 @@ import {TimetablePickerComponent} from './components/form/types/timetable-picker import {GeoSearchComponent} from './components/form/types/geo-search/geo-search.component'; import { GalleryComponent } from './components/form/types/gallery/gallery.component'; import { Select2Component } from './components/form/types/select-2/select-2.component'; +import { CloudinaryLibraryComponent } from './components/form/types/cloudinary-library/cloudinary-library.component'; + const COMPONENTS = [ PanelComponent, @@ -91,7 +93,10 @@ const COMPONENTS = [ DateRangePickerComponent, MediaLibraryComponent, TimetablePickerComponent, - GeoSearchComponent + GeoSearchComponent, + GalleryComponent, + Select2Component, + CloudinaryLibraryComponent ]; const PROVIDERS = [ @@ -161,8 +166,6 @@ const routes: Routes = [ ], declarations: [ ...COMPONENTS, - GalleryComponent, - Select2Component ], providers: [ ...PROVIDERS From f5200c03de7fb4406993284cd6f1ccce0c94ed5e Mon Sep 17 00:00:00 2001 From: damiandominella Date: Mon, 25 Jun 2018 10:06:58 +0200 Subject: [PATCH 2/6] implementations --- .../form/interfaces/form-field-file.ts | 1 + .../cloudinary-library.component.html | 24 +++- .../cloudinary-library.component.scss | 51 +++++--- .../cloudinary-library.component.ts | 118 +++++++++++------- 4 files changed, 127 insertions(+), 67 deletions(-) diff --git a/src/app/panel/components/form/interfaces/form-field-file.ts b/src/app/panel/components/form/interfaces/form-field-file.ts index 14d8ce7b..09fa55e0 100644 --- a/src/app/panel/components/form/interfaces/form-field-file.ts +++ b/src/app/panel/components/form/interfaces/form-field-file.ts @@ -45,6 +45,7 @@ export interface Media { export interface CloudinaryField extends FormField { options: { searchEndpoint: string; + dataEndpoint: string; page?: number; perPage?: number; }; diff --git a/src/app/panel/components/form/types/cloudinary-library/cloudinary-library.component.html b/src/app/panel/components/form/types/cloudinary-library/cloudinary-library.component.html index 641e20b8..9939b541 100644 --- a/src/app/panel/components/form/types/cloudinary-library/cloudinary-library.component.html +++ b/src/app/panel/components/form/types/cloudinary-library/cloudinary-library.component.html @@ -13,7 +13,7 @@
-
+
- +
{{media.name}} @@ -38,7 +39,7 @@
- +
+
+ + + Aggiungi tag(s) +
+
+