diff --git a/backend/i18n/frontend_en.json b/backend/i18n/frontend_en.json
index def321ad7..1fef6b24b 100644
--- a/backend/i18n/frontend_en.json
+++ b/backend/i18n/frontend_en.json
@@ -307,6 +307,7 @@
"common.lastExecuted": "Last Executed",
"common.latitudeShort": "Lat",
"common.loading": "Loading",
+ "common.loadMore": "Load More",
"common.logout": "Logout",
"common.logs": "Logs",
"common.longitudeShort": "Lon",
@@ -490,6 +491,7 @@
"contents.statusQueries": "Status Queries",
"contents.stockPhotoEmpty": "Nothing selected",
"contents.stockPhotoSearch": "Search for Photos by Unsplash",
+ "contents.stockPhotoSearchEmpty": "Use the search bar above to find photos.",
"contents.tableHeaders.created": "Created",
"contents.tableHeaders.createdBy": "Created By",
"contents.tableHeaders.createdByShort": "By",
diff --git a/backend/i18n/frontend_it.json b/backend/i18n/frontend_it.json
index 9f451f078..ac66d0cff 100644
--- a/backend/i18n/frontend_it.json
+++ b/backend/i18n/frontend_it.json
@@ -307,6 +307,7 @@
"common.lastExecuted": "Last Executed",
"common.latitudeShort": "Lat",
"common.loading": "Caricamento",
+ "common.loadMore": "Load More",
"common.logout": "Esci",
"common.logs": "Log",
"common.longitudeShort": "Lon",
@@ -490,6 +491,7 @@
"contents.statusQueries": "Stato Query",
"contents.stockPhotoEmpty": "Nessuna selezione",
"contents.stockPhotoSearch": "Cerca foto su Unsplash",
+ "contents.stockPhotoSearchEmpty": "Use the search bar above to find photos.",
"contents.tableHeaders.created": "Creato",
"contents.tableHeaders.createdBy": "Creato da",
"contents.tableHeaders.createdByShort": "Da",
diff --git a/backend/i18n/frontend_nl.json b/backend/i18n/frontend_nl.json
index 074693f7d..53815c466 100644
--- a/backend/i18n/frontend_nl.json
+++ b/backend/i18n/frontend_nl.json
@@ -307,6 +307,7 @@
"common.lastExecuted": "Laatst Uitgevoerd",
"common.latitudeShort": "Lat",
"common.loading": "Laden",
+ "common.loadMore": "Load More",
"common.logout": "Uitloggen",
"common.logs": "Logboeken",
"common.longitudeShort": "Lon",
@@ -490,6 +491,7 @@
"contents.statusQueries": "Statusquery's",
"contents.stockPhotoEmpty": "Niets geselecteerd",
"contents.stockPhotoSearch": "Zoeken naar foto's op Unsplash",
+ "contents.stockPhotoSearchEmpty": "Use the search bar above to find photos.",
"contents.tableHeaders.created": "Gemaakt",
"contents.tableHeaders.createdBy": "Gemaakt door",
"contents.tableHeaders.createdByShort": "Door",
diff --git a/backend/i18n/frontend_zh.json b/backend/i18n/frontend_zh.json
index 8b99a5258..e4bafdc99 100644
--- a/backend/i18n/frontend_zh.json
+++ b/backend/i18n/frontend_zh.json
@@ -307,6 +307,7 @@
"common.lastExecuted": "Last Executed",
"common.latitudeShort": "纬度",
"common.loading": "正在加载",
+ "common.loadMore": "Load More",
"common.logout": "注销",
"common.logs": "日志",
"common.longitudeShort": "Lon",
@@ -490,6 +491,7 @@
"contents.statusQueries": "状态查询",
"contents.stockPhotoEmpty": "未选择任何内容",
"contents.stockPhotoSearch": "通过 Unsplash 搜索照片",
+ "contents.stockPhotoSearchEmpty": "Use the search bar above to find photos.",
"contents.tableHeaders.created": "创建",
"contents.tableHeaders.createdBy": "创建者",
"contents.tableHeaders.createdByShort": "By",
diff --git a/backend/i18n/source/frontend_en.json b/backend/i18n/source/frontend_en.json
index def321ad7..1fef6b24b 100644
--- a/backend/i18n/source/frontend_en.json
+++ b/backend/i18n/source/frontend_en.json
@@ -307,6 +307,7 @@
"common.lastExecuted": "Last Executed",
"common.latitudeShort": "Lat",
"common.loading": "Loading",
+ "common.loadMore": "Load More",
"common.logout": "Logout",
"common.logs": "Logs",
"common.longitudeShort": "Lon",
@@ -490,6 +491,7 @@
"contents.statusQueries": "Status Queries",
"contents.stockPhotoEmpty": "Nothing selected",
"contents.stockPhotoSearch": "Search for Photos by Unsplash",
+ "contents.stockPhotoSearchEmpty": "Use the search bar above to find photos.",
"contents.tableHeaders.created": "Created",
"contents.tableHeaders.createdBy": "Created By",
"contents.tableHeaders.createdByShort": "By",
diff --git a/frontend/src/app/features/content/shared/forms/stock-photo-editor.component.html b/frontend/src/app/features/content/shared/forms/stock-photo-editor.component.html
index cf23773d7..b341476a2 100644
--- a/frontend/src/app/features/content/shared/forms/stock-photo-editor.component.html
+++ b/frontend/src/app/features/content/shared/forms/stock-photo-editor.component.html
@@ -1,41 +1,52 @@
-
-
+
+
+
![]()
-
-
+
+
-
+
+
+
+
+
+
+
-
+
-
-
-
![]()
-
-
+
+
![]()
+
+
-
+
+
+
+
+ {{ 'contents.stockPhotoSearchEmpty' | sqxTranslate }}
+
+
+
+
-
-
-
\ No newline at end of file
+
+
+
diff --git a/frontend/src/app/features/content/shared/forms/stock-photo-editor.component.scss b/frontend/src/app/features/content/shared/forms/stock-photo-editor.component.scss
index e3dd07f82..dca77b4c0 100644
--- a/frontend/src/app/features/content/shared/forms/stock-photo-editor.component.scss
+++ b/frontend/src/app/features/content/shared/forms/stock-photo-editor.component.scss
@@ -3,68 +3,45 @@
$color-user-background: rgba(0, 0, 0, 50%);
$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-white;
- display: flex;
- justify-content: center;
- margin: 0;
- margin-top: .5rem;
+ border-radius: 0;
+ color: white;
+ text-align: center;
- &-empty {
- padding: 1rem;
+ i {
+ margin-top: 1rem;
+ margin-bottom: 1rem;
}
img {
- max-height: $height;
+ max-width: 100%;
}
}
-sqx-list-view {
- border: 1px solid $color-input;
- border-radius: .25rem;
- height: $height;
- margin-top: .5rem;
+.spin2 {
+ font-size: 14px;
+}
+
+.search {
+ display: inline-block;
+ margin-left: 0;
+ margin-right: 1rem;
+ width: 300px;
}
-.icon {
- @include absolute($height * .5, auto, auto, 5px);
- color: $color-border;
- font-size: 30px;
- font-weight: lighter;
+.empty {
+ @include absolute(40%, 0, null, 0);
}
.photos {
column-gap: 0;
column-width: 200px;
- margin-left: -1rem;
margin-right: -1rem;
+ margin-left: 0;
}
.photo {
diff --git a/frontend/src/app/features/content/shared/forms/stock-photo-editor.component.ts b/frontend/src/app/features/content/shared/forms/stock-photo-editor.component.ts
index 3c7f8e999..b4dafb10a 100644
--- a/frontend/src/app/features/content/shared/forms/stock-photo-editor.component.ts
+++ b/frontend/src/app/features/content/shared/forms/stock-photo-editor.component.ts
@@ -7,9 +7,9 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core';
import { FormControl, NG_VALUE_ACCESSOR } from '@angular/forms';
-import { of } from 'rxjs';
-import { debounceTime, switchMap, tap } from 'rxjs/operators';
-import { StatefulControlComponent, StockPhotoDto, StockPhotoService, thumbnail, Types, value$, valueProjection$ } from '@app/shared';
+import { BehaviorSubject, of } from 'rxjs';
+import { debounceTime, map, switchMap, tap } from 'rxjs/operators';
+import { DialogModel, StatefulControlComponent, StockPhotoDto, StockPhotoService, thumbnail, Types, value$, valueProjection$ } from '@app/shared';
export const SQX_STOCK_PHOTO_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => StockPhotoEditorComponent), multi: true,
@@ -19,10 +19,18 @@ interface State {
// True when loading assets.
isLoading?: boolean;
- // True, when width less than 600 pixels.
- isCompact?: boolean;
+ // The photos.
+ stockPhotos: ReadonlyArray
;
+
+ // True if more photos are available.
+ hasMore?: boolean;
+
+ // The status of the thumbnail.
+ thumbnailStatus?: 'Loaded' | 'Failed';
}
+type Request = { search?: string; page: number };
+
@Component({
selector: 'sqx-stock-photo-editor',
styleUrls: ['./stock-photo-editor.component.scss'],
@@ -40,41 +48,64 @@ export class StockPhotoEditorComponent extends StatefulControlComponent thumbnail(x, 400) || x);
+ public stockPhotoRequests = new BehaviorSubject({ page: 1 });
+ public stockPhotoThumbnail = valueProjection$(this.valueControl, x => thumbnail(x, undefined, 300) || x);
public stockPhotoSearch = new FormControl('');
- public stockPhotos =
- value$(this.stockPhotoSearch).pipe(
- 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 });
- }));
+ public searchDialog = new DialogModel();
constructor(changeDetector: ChangeDetectorRef,
private readonly stockPhotoService: StockPhotoService,
) {
- super(changeDetector, {});
+ super(changeDetector, {
+ stockPhotos: [],
+ });
}
public ngOnInit() {
+ this.own(
+ value$(this.valueControl)
+ .subscribe(() => {
+ this.next({ thumbnailStatus: undefined });
+ }));
+
+ this.own(
+ value$(this.stockPhotoSearch)
+ .subscribe(search => {
+ this.stockPhotoRequests.next({ search, page: 1 });
+ }));
+
this.own(
this.valueControl.valueChanges
.subscribe(value => {
this.callChange(value);
this.callTouched();
}));
+
+ this.own(
+ this.stockPhotoRequests.pipe(
+ debounceTime(500),
+ tap(request => {
+ if (request.search && request.search.length > 0) {
+ this.next({ isLoading: true });
+ }
+ }),
+ switchMap(request => {
+ if (request.search && request.search.length > 0) {
+ return this.stockPhotoService.getImages(request.search, request.page).pipe(map(result => ({ request, result })));
+ } else {
+ return of(({ request, result: [] }));
+ }
+ }),
+ tap(({ request, result }) => {
+ this.next(s => ({
+ ...s,
+ isLoading: false,
+ isDisabled: s.isDisabled,
+ stockPhotos: request.page > 1 ? [...s.stockPhotos, ...result] : result,
+ hasMore: result.length === 20,
+ }));
+ })));
}
public writeValue(obj: string) {
@@ -93,13 +124,11 @@ export class StockPhotoEditorComponent extends StatefulControlComponent {
+ this.own(
+ this.renderer.listen(this.element.nativeElement, 'load', () => {
+ this.onLoad();
+ }));
+
+ this.own(
+ this.renderer.listen(this.element.nativeElement, 'error', () => {
+ this.onError();
+ }));
+ });
+ }
+
+ public ngOnChanges() {
+ this.onError();
+ }
+
+ public onLoad() {
+ this.renderer.setStyle(this.element.nativeElement, 'visibility', 'visible');
+ }
+
+ public onError() {
+ this.renderer.setStyle(this.element.nativeElement, 'visibility', 'hidden');
+ }
+}
diff --git a/frontend/src/app/framework/angular/list-view.component.scss b/frontend/src/app/framework/angular/list-view.component.scss
index c6daedc7e..d35a7da5d 100644
--- a/frontend/src/app/framework/angular/list-view.component.scss
+++ b/frontend/src/app/framework/angular/list-view.component.scss
@@ -1,12 +1,20 @@
@import 'mixins';
@import 'vars';
+%transition-opacity {
+ transition: opacity .2s ease;
+}
+
+%disabled {
+ pointer-events: none;
+}
+
:host {
display: flex;
flex-direction: column;
flex-grow: 1;
overflow: hidden;
- padding: 0;
+ position: relative;
}
.inner {
@@ -20,10 +28,6 @@
}
}
-%transition-opacity {
- transition: opacity .2s ease;
-}
-
.list {
&-content {
@extend %transition-opacity;
@@ -96,16 +100,13 @@
overflow: visible;
}
-%disabled {
- pointer-events: none;
-}
-
.loading-indicator {
@extend %disabled;
opacity: .5;
}
.loader {
- @include absolute(20%, 0, auto, 0);
+ @include absolute(50%, 0, auto, 0);
@extend %disabled;
+ margin-top: -.5rem;
}
\ No newline at end of file
diff --git a/frontend/src/app/framework/declarations.ts b/frontend/src/app/framework/declarations.ts
index 626c0439d..3c208da70 100644
--- a/frontend/src/app/framework/declarations.ts
+++ b/frontend/src/app/framework/declarations.ts
@@ -43,6 +43,7 @@ export * from './angular/http/caching.interceptor';
export * from './angular/http/http-extensions';
export * from './angular/http/loading.interceptor';
export * from './angular/image-source.directive';
+export * from './angular/image-url.directive';
export * from './angular/language-selector.component';
export * from './angular/layout-container.directive';
export * from './angular/layout.component';
diff --git a/frontend/src/app/framework/module.ts b/frontend/src/app/framework/module.ts
index 0bc75db2b..73c0570ba 100644
--- a/frontend/src/app/framework/module.ts
+++ b/frontend/src/app/framework/module.ts
@@ -11,7 +11,7 @@ import { ModuleWithProviders, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { ColorPickerModule } from 'ngx-color-picker';
-import { AnalyticsService, AutocompleteComponent, AvatarComponent, CachingInterceptor, CanDeactivateGuard, CheckboxGroupComponent, ClipboardService, CodeComponent, CodeEditorComponent, ColorPickerComponent, ConfirmClickDirective, ControlErrorsComponent, ControlErrorsMessagesComponent, CopyDirective, DarkenPipe, DatePipe, DateTimeEditorComponent, DayOfWeekPipe, DayPipe, DialogRendererComponent, DialogService, DisplayNamePipe, DropdownComponent, DropdownMenuComponent, DurationPipe, EditableTitleComponent, ExternalLinkDirective, FileDropDirective, FileSizePipe, FocusOnInitDirective, FormAlertComponent, FormErrorComponent, FormHintComponent, FromNowPipe, FullDateTimePipe, HighlightPipe, HoverBackgroundDirective, ImageSourceDirective, IndeterminateValueDirective, ISODatePipe, KeysPipe, KNumberPipe, LanguageSelectorComponent, LayoutComponent, LayoutContainerDirective, LightenPipe, ListViewComponent, LoadingInterceptor, LoadingService, LocalizedInputComponent, LocalStoreService, MarkdownDirective, MarkdownInlinePipe, MarkdownPipe, MessageBus, ModalDialogComponent, ModalDirective, ModalPlacementDirective, MonthPipe, OnboardingService, OnboardingTooltipComponent, PagerComponent, ParentLinkDirective, ProgressBarComponent, ResizedDirective, ResizeService, ResourceLoaderService, RootViewComponent, SafeHtmlPipe, SafeResourceUrlPipe, SafeUrlPipe, ScrollActiveDirective, ShortcutComponent, ShortcutDirective, ShortcutService, ShortDatePipe, ShortTimePipe, StarsComponent, StatusIconComponent, StopClickDirective, StopDragDirective, SyncScollingDirective, SyncWidthDirective, TabRouterlinkDirective, TagEditorComponent, TemplateWrapperDirective, TempService, TitleComponent, TitleService, ToggleComponent, ToolbarComponent, TooltipDirective, TransformInputDirective, TranslatePipe, VideoPlayerComponent } from './declarations';
+import { AnalyticsService, AutocompleteComponent, AvatarComponent, CachingInterceptor, CanDeactivateGuard, CheckboxGroupComponent, ClipboardService, CodeComponent, CodeEditorComponent, ColorPickerComponent, ConfirmClickDirective, ControlErrorsComponent, ControlErrorsMessagesComponent, CopyDirective, DarkenPipe, DatePipe, DateTimeEditorComponent, DayOfWeekPipe, DayPipe, DialogRendererComponent, DialogService, DisplayNamePipe, DropdownComponent, DropdownMenuComponent, DurationPipe, EditableTitleComponent, ExternalLinkDirective, FileDropDirective, FileSizePipe, FocusOnInitDirective, FormAlertComponent, FormErrorComponent, FormHintComponent, FromNowPipe, FullDateTimePipe, HighlightPipe, HoverBackgroundDirective, ImageSourceDirective, ImageUrlDirective, IndeterminateValueDirective, ISODatePipe, KeysPipe, KNumberPipe, LanguageSelectorComponent, LayoutComponent, LayoutContainerDirective, LightenPipe, ListViewComponent, LoadingInterceptor, LoadingService, LocalizedInputComponent, LocalStoreService, MarkdownDirective, MarkdownInlinePipe, MarkdownPipe, MessageBus, ModalDialogComponent, ModalDirective, ModalPlacementDirective, MonthPipe, OnboardingService, OnboardingTooltipComponent, PagerComponent, ParentLinkDirective, ProgressBarComponent, ResizedDirective, ResizeService, ResourceLoaderService, RootViewComponent, SafeHtmlPipe, SafeResourceUrlPipe, SafeUrlPipe, ScrollActiveDirective, ShortcutComponent, ShortcutDirective, ShortcutService, ShortDatePipe, ShortTimePipe, StarsComponent, StatusIconComponent, StopClickDirective, StopDragDirective, SyncScollingDirective, SyncWidthDirective, TabRouterlinkDirective, TagEditorComponent, TemplateWrapperDirective, TempService, TitleComponent, TitleService, ToggleComponent, ToolbarComponent, TooltipDirective, TransformInputDirective, TranslatePipe, VideoPlayerComponent } from './declarations';
@NgModule({
imports: [
@@ -55,6 +55,7 @@ import { AnalyticsService, AutocompleteComponent, AvatarComponent, CachingInterc
HighlightPipe,
HoverBackgroundDirective,
ImageSourceDirective,
+ ImageUrlDirective,
IndeterminateValueDirective,
ISODatePipe,
KeysPipe,
@@ -138,6 +139,7 @@ import { AnalyticsService, AutocompleteComponent, AvatarComponent, CachingInterc
HighlightPipe,
HoverBackgroundDirective,
ImageSourceDirective,
+ ImageUrlDirective,
IndeterminateValueDirective,
ISODatePipe,
KeysPipe,
diff --git a/frontend/src/app/shared/services/stock-photo.service.spec.ts b/frontend/src/app/shared/services/stock-photo.service.spec.ts
index e49a89696..889220cb7 100644
--- a/frontend/src/app/shared/services/stock-photo.service.spec.ts
+++ b/frontend/src/app/shared/services/stock-photo.service.spec.ts
@@ -29,11 +29,11 @@ describe('StockPhotoService', () => {
inject([StockPhotoService, HttpTestingController], (stockPhotoService: StockPhotoService, httpMock: HttpTestingController) => {
let images: ReadonlyArray;
- stockPhotoService.getImages('my-query').subscribe(result => {
+ stockPhotoService.getImages('my-query', 4).subscribe(result => {
images = result;
});
- const req = httpMock.expectOne('https://stockphoto.squidex.io/?query=my-query&pageSize=100');
+ const req = httpMock.expectOne('https://stockphoto.squidex.io/?query=my-query&page=4');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
@@ -64,7 +64,7 @@ describe('StockPhotoService', () => {
images = result;
});
- const req = httpMock.expectOne('https://stockphoto.squidex.io/?query=my-query&pageSize=100');
+ const req = httpMock.expectOne('https://stockphoto.squidex.io/?query=my-query&page=1');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
diff --git a/frontend/src/app/shared/services/stock-photo.service.ts b/frontend/src/app/shared/services/stock-photo.service.ts
index e9cec8b83..02ebcc109 100644
--- a/frontend/src/app/shared/services/stock-photo.service.ts
+++ b/frontend/src/app/shared/services/stock-photo.service.ts
@@ -27,8 +27,8 @@ export class StockPhotoService {
) {
}
- public getImages(query: string): Observable> {
- const url = `https://stockphoto.squidex.io/?query=${query}&pageSize=100`;
+ public getImages(query: string, page = 1): Observable> {
+ const url = `https://stockphoto.squidex.io/?query=${query}&page=${page}`;
return this.http.get(url).pipe(
map(body => {
diff --git a/frontend/src/app/theme/_common.scss b/frontend/src/app/theme/_common.scss
index 1d691f0ae..a51c266d7 100644
--- a/frontend/src/app/theme/_common.scss
+++ b/frontend/src/app/theme/_common.scss
@@ -175,6 +175,10 @@ hr {
display: none;
}
+.hidden-important {
+ display: none !important;
+}
+
// Hidden helper (fast *ngIf replacement)
.invisible {
visibility: hidden;
diff --git a/frontend/src/app/theme/_forms.scss b/frontend/src/app/theme/_forms.scss
index f6b88d183..9e1df422d 100644
--- a/frontend/src/app/theme/_forms.scss
+++ b/frontend/src/app/theme/_forms.scss
@@ -21,6 +21,14 @@
}
}
}
+
+ &.preview {
+ background-color: $color-input;
+ border: 0;
+ border-radius: $border-radius;
+ opacity: .4;
+ pointer-events: none;
+ }
}
//
@@ -134,14 +142,6 @@
}
-.preview {
- background-color: $color-input;
- border: 0;
- border-radius: $border-radius;
- opacity: .4;
- pointer-events: none;
-}
-
//
// Control Dropdown item
//