mirror of https://github.com/Squidex/squidex.git
17 changed files with 458 additions and 15 deletions
@ -0,0 +1,42 @@ |
|||
<div class="row no-gutters"> |
|||
<div class="col-auto col-image" [class.expand]="isCompact"> |
|||
<input class="form-control value" [formControl]="valueControl" readonly /> |
|||
|
|||
<button type="button" class="btn btn-text-secondary value-clear" (click)="reset()"> |
|||
<i class="icon-close"></i> |
|||
</button> |
|||
|
|||
<div *ngIf="valueThumb | async; let thumbUrl; else noThumb" class="preview"> |
|||
<img [src]="thumbUrl" /> |
|||
</div> |
|||
|
|||
<ng-template #noThumb> |
|||
<div class="preview preview-empty"> |
|||
Nothing selected |
|||
</div> |
|||
</ng-template> |
|||
</div> |
|||
<div class="col pl-4" *ngIf="!isCompact"> |
|||
<i class="icon-angle-left icon"></i> |
|||
|
|||
<input class="form-control" [formControl]="stockPhotoSearch" placeholder="Search for Photos by Unsplash" /> |
|||
|
|||
<sqx-list-view [isLoading]="snapshot.isLoading" table="true"> |
|||
<div content> |
|||
<div class="photos"> |
|||
<ng-container *ngIf="stockPhotos | async; let photos"> |
|||
<div *ngFor="let photo of photos" class="photo" [class.selected]="isSelected(photo)" (click)="selectPhoto(photo)"> |
|||
<img [src]="photo.thumbUrl" /> |
|||
|
|||
<div class="photo-user"> |
|||
<a class="photo-user-link" [href]="photo.userProfileUrl" sqxExternalLink sqxStopClick> |
|||
{{photo.user}} |
|||
</a> |
|||
</div> |
|||
</div> |
|||
</ng-container> |
|||
</div> |
|||
</div> |
|||
</sqx-list-view> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,106 @@ |
|||
@import '_vars'; |
|||
@import '_mixins'; |
|||
|
|||
$color-user-background: rgba(0, 0, 0, .5); |
|||
$color-background: #000; |
|||
|
|||
$height: 300px; |
|||
|
|||
.col-image { |
|||
@include force-width(400px); |
|||
|
|||
&.expand { |
|||
@include force-width(100%); |
|||
} |
|||
|
|||
img { |
|||
max-width: 100%; |
|||
} |
|||
} |
|||
|
|||
.value { |
|||
& { |
|||
padding-right: 2.5rem; |
|||
} |
|||
|
|||
&-clear { |
|||
@include absolute(0, 0, auto, auto); |
|||
} |
|||
} |
|||
|
|||
.preview { |
|||
& { |
|||
@include force-height($height); |
|||
align-items: center; |
|||
background: $color-background; |
|||
border: 0; |
|||
border-radius: .25rem; |
|||
color: $color-dark-foreground; |
|||
display: flex; |
|||
justify-content: center; |
|||
margin: 0; |
|||
margin-top: .5rem; |
|||
} |
|||
|
|||
&-empty { |
|||
padding: 1rem; |
|||
} |
|||
|
|||
img { |
|||
max-height: $height; |
|||
} |
|||
} |
|||
|
|||
sqx-list-view { |
|||
border: 1px solid $color-input; |
|||
border-radius: .25rem; |
|||
height: $height; |
|||
margin-top: .5rem; |
|||
} |
|||
|
|||
.icon { |
|||
@include absolute($height * .5, auto, auto, 5px); |
|||
color: $color-border; |
|||
font-size: 30px; |
|||
font-weight: lighter; |
|||
} |
|||
|
|||
.photos { |
|||
column-gap: 0; |
|||
column-width: 200px; |
|||
margin-left: -1rem; |
|||
margin-right: -1rem; |
|||
} |
|||
|
|||
.photo { |
|||
& { |
|||
border: 2px solid $color-border; |
|||
border-radius: 0; |
|||
display: inline-block; |
|||
margin-bottom: .5rem; |
|||
margin-right: .5rem; |
|||
position: relative; |
|||
} |
|||
|
|||
&:hover { |
|||
border-color: $color-theme-blue; |
|||
} |
|||
|
|||
&.selected { |
|||
border-color: $color-theme-blue; |
|||
} |
|||
|
|||
&-user { |
|||
@include absolute(auto, 0, 0, 0); |
|||
background: $color-user-background; |
|||
border: 0; |
|||
padding: .5rem .75rem; |
|||
} |
|||
|
|||
&-user-link { |
|||
@include truncate; |
|||
color: $color-dark-foreground; |
|||
font-size: 90%; |
|||
font-weight: normal; |
|||
} |
|||
} |
|||
@ -0,0 +1,114 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core'; |
|||
import { FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; |
|||
import { of } from 'rxjs'; |
|||
import { debounceTime, distinctUntilChanged, map, shareReplay, startWith, switchMap, tap } from 'rxjs/operators'; |
|||
|
|||
import { |
|||
StatefulControlComponent, |
|||
StockPhotoDto, |
|||
StockPhotoService, |
|||
thumbnail, |
|||
Types |
|||
} from '@app/shared'; |
|||
|
|||
interface State { |
|||
isLoading?: boolean; |
|||
} |
|||
|
|||
export const SQX_STOCK_PHOTO_EDITOR_CONTROL_VALUE_ACCESSOR: any = { |
|||
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => StockPhotoEditorComponent), multi: true |
|||
}; |
|||
|
|||
@Component({ |
|||
selector: 'sqx-stock-photo-editor', |
|||
styleUrls: ['./stock-photo-editor.component.scss'], |
|||
templateUrl: './stock-photo-editor.component.html', |
|||
providers: [SQX_STOCK_PHOTO_EDITOR_CONTROL_VALUE_ACCESSOR], |
|||
changeDetection: ChangeDetectionStrategy.OnPush |
|||
}) |
|||
export class StockPhotoEditorComponent extends StatefulControlComponent<State, string> implements OnInit { |
|||
@Input() |
|||
public isCompact = false; |
|||
|
|||
public valueControl = new FormControl(''); |
|||
|
|||
public valueThumb = |
|||
this.valueControl.valueChanges.pipe( |
|||
startWith(this.valueControl.value), |
|||
shareReplay(1), |
|||
map(value => thumbnail(value, 400) || value)); |
|||
|
|||
public stockPhotoSearch = new FormControl(''); |
|||
|
|||
public stockPhotos = |
|||
this.stockPhotoSearch.valueChanges.pipe( |
|||
startWith(this.stockPhotoSearch.value), |
|||
distinctUntilChanged(), |
|||
debounceTime(500), |
|||
tap(query => { |
|||
if (query && query.length > 0) { |
|||
this.next({ isLoading: true }); |
|||
} |
|||
}), |
|||
switchMap(query => { |
|||
if (query && query.length > 0) { |
|||
return this.stockPhotoService.getImages(query); |
|||
} else { |
|||
return of([]); |
|||
} |
|||
}), |
|||
tap(() => { |
|||
this.next({ isLoading: false }); |
|||
})); |
|||
|
|||
constructor(changeDetector: ChangeDetectorRef, |
|||
private readonly stockPhotoService: StockPhotoService |
|||
) { |
|||
super(changeDetector, {}); |
|||
} |
|||
|
|||
public ngOnInit() { |
|||
this.own(this.valueThumb); |
|||
|
|||
this.own( |
|||
this.valueControl.valueChanges |
|||
.subscribe(value => { |
|||
this.callChange(value); |
|||
})); |
|||
} |
|||
|
|||
public writeValue(obj: string) { |
|||
if (Types.isString(obj)) { |
|||
this.valueControl.setValue(obj, { emitEvent: true }); |
|||
} else { |
|||
this.valueControl.setValue('', { emitEvent: true }); |
|||
} |
|||
} |
|||
|
|||
public selectPhoto(photo: StockPhotoDto) { |
|||
if (!this.snapshot.isDisabled) { |
|||
this.valueControl.setValue(photo.url); |
|||
} |
|||
} |
|||
|
|||
public reset() { |
|||
if (!this.snapshot.isDisabled) { |
|||
this.valueControl.setValue(''); |
|||
} |
|||
} |
|||
|
|||
public isSelected(photo: StockPhotoDto) { |
|||
return photo.url === this.valueControl.value; |
|||
} |
|||
|
|||
public trackByPhoto(index: number, photo: StockPhotoDto) { |
|||
return photo.thumbUrl; |
|||
} |
|||
} |
|||
@ -0,0 +1,79 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; |
|||
import { inject, TestBed } from '@angular/core/testing'; |
|||
|
|||
import { StockPhotoDto, StockPhotoService } from '@app/shared/internal'; |
|||
|
|||
describe('StockPhotoService', () => { |
|||
beforeEach(() => { |
|||
TestBed.configureTestingModule({ |
|||
imports: [ |
|||
HttpClientTestingModule |
|||
], |
|||
providers: [ |
|||
StockPhotoService |
|||
] |
|||
}); |
|||
}); |
|||
|
|||
afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => { |
|||
httpMock.verify(); |
|||
})); |
|||
|
|||
it('should make get request to get stock photos', |
|||
inject([StockPhotoService, HttpTestingController], (stockPhotoService: StockPhotoService, httpMock: HttpTestingController) => { |
|||
|
|||
let images: ReadonlyArray<StockPhotoDto>; |
|||
|
|||
stockPhotoService.getImages('my-query').subscribe(result => { |
|||
images = result; |
|||
}); |
|||
|
|||
const req = httpMock.expectOne('https://stockphoto.squidex.io/?query=my-query&pageSize=100'); |
|||
|
|||
expect(req.request.method).toEqual('GET'); |
|||
expect(req.request.headers.get('If-Match')).toBeNull(); |
|||
|
|||
req.flush([{ |
|||
url: 'url1', |
|||
thumbUrl: 'thumb1', |
|||
user: 'user1', |
|||
userProfileUrl: 'user1-url' |
|||
}, { |
|||
url: 'url2', |
|||
thumbUrl: 'thumb2', |
|||
user: 'user2', |
|||
userProfileUrl: 'user2-url' |
|||
}]); |
|||
|
|||
expect(images!).toEqual([ |
|||
new StockPhotoDto('url1', 'thumb1', 'user1', 'user1-url'), |
|||
new StockPhotoDto('url2', 'thumb2', 'user2', 'user2-url') |
|||
]); |
|||
})); |
|||
|
|||
it('should return empty stock photos if get request fails', |
|||
inject([StockPhotoService, HttpTestingController], (stockPhotoService: StockPhotoService, httpMock: HttpTestingController) => { |
|||
|
|||
let images: ReadonlyArray<StockPhotoDto>; |
|||
|
|||
stockPhotoService.getImages('my-query').subscribe(result => { |
|||
images = result; |
|||
}); |
|||
|
|||
const req = httpMock.expectOne('https://stockphoto.squidex.io/?query=my-query&pageSize=100'); |
|||
|
|||
expect(req.request.method).toEqual('GET'); |
|||
expect(req.request.headers.get('If-Match')).toBeNull(); |
|||
|
|||
req.error(<any>{}); |
|||
|
|||
expect(images!).toEqual([]); |
|||
})); |
|||
}); |
|||
@ -0,0 +1,45 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { HttpClient } from '@angular/common/http'; |
|||
import { Injectable } from '@angular/core'; |
|||
import { Observable, of } from 'rxjs'; |
|||
import { catchError, map } from 'rxjs/operators'; |
|||
|
|||
export class StockPhotoDto { |
|||
constructor ( |
|||
public readonly url: string, |
|||
public readonly thumbUrl: string, |
|||
public readonly user: string, |
|||
public readonly userProfileUrl: string |
|||
) { |
|||
} |
|||
} |
|||
|
|||
@Injectable() |
|||
export class StockPhotoService { |
|||
constructor( |
|||
private readonly http: HttpClient |
|||
) { |
|||
} |
|||
|
|||
public getImages(query: string): Observable<ReadonlyArray<StockPhotoDto>> { |
|||
const url = `https://stockphoto.squidex.io/?query=${query}&pageSize=100`; |
|||
|
|||
return this.http.get<any[]>(url).pipe( |
|||
map(body => { |
|||
return body.map(x => |
|||
new StockPhotoDto( |
|||
x.url, |
|||
x.thumbUrl, |
|||
x.user, |
|||
x.userProfileUrl |
|||
)); |
|||
}), |
|||
catchError(() => of([]))); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue