Browse Source

Refactoring finished, awaiting more tests.

pull/345/head
Sebastian 7 years ago
parent
commit
3eb4bca9f6
  1. 16
      src/Squidex/app/features/content/shared/assets-editor.component.html
  2. 102
      src/Squidex/app/features/content/shared/assets-editor.component.ts
  3. 4
      src/Squidex/app/features/content/shared/preview-button.component.ts
  4. 16
      src/Squidex/app/features/content/shared/references-editor.component.html
  5. 75
      src/Squidex/app/features/content/shared/references-editor.component.ts
  6. 10
      src/Squidex/app/framework/angular/code.component.ts
  7. 8
      src/Squidex/app/framework/angular/forms/autocomplete.component.html
  8. 71
      src/Squidex/app/framework/angular/forms/autocomplete.component.ts
  9. 4
      src/Squidex/app/framework/angular/forms/checkbox-group.component.ts
  10. 2
      src/Squidex/app/framework/angular/forms/control-errors.component.ts
  11. 20
      src/Squidex/app/framework/angular/forms/stars.component.ts
  12. 14
      src/Squidex/app/framework/angular/forms/tag-editor.component.html
  13. 64
      src/Squidex/app/framework/angular/forms/tag-editor.component.ts
  14. 10
      src/Squidex/app/framework/angular/forms/toggle.component.ts
  15. 26
      src/Squidex/app/framework/angular/modals/dialog-renderer.component.ts
  16. 6
      src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.ts
  17. 10
      src/Squidex/app/framework/angular/modals/root-view.component.ts
  18. 6
      src/Squidex/app/framework/angular/modals/tooltip.component.ts
  19. 10
      src/Squidex/app/framework/angular/pager.component.ts
  20. 9
      src/Squidex/app/framework/angular/panel.component.ts
  21. 6
      src/Squidex/app/framework/angular/shortcut.component.ts
  22. 20
      src/Squidex/app/framework/angular/stateful.component.ts
  23. 6
      src/Squidex/app/framework/angular/user-report.component.ts
  24. 21
      src/Squidex/app/framework/state.ts
  25. 4
      src/Squidex/app/shared/components/geolocation-editor.component.html
  26. 64
      src/Squidex/app/shared/components/geolocation-editor.component.ts
  27. 2
      src/Squidex/app/shared/components/markdown-editor.component.html
  28. 36
      src/Squidex/app/shared/components/markdown-editor.component.ts
  29. 21
      src/Squidex/app/shared/components/rich-editor.component.ts
  30. 6
      src/Squidex/app/shared/state/ui.state.ts

16
src/Squidex/app/features/content/shared/assets-editor.component.html

@ -1,4 +1,4 @@
<div class="assets-container" [class.disabled]="isDisabled" (paste)="pasteFiles($event)" tabindex="1000">
<div class="assets-container" [class.disabled]="snapshot.isDisabled" (paste)="pasteFiles($event)" tabindex="1000">
<div class="header list">
<div class="row no-gutters">
<div class="col">
@ -8,10 +8,10 @@
</div>
<div class="col-auto pl-1">
<div class="btn-group">
<button type="button" class="btn btn-secondary btn-toggle" [class.btn-primary]="isListView" [disabled]="isListView" (click)="changeView(true)">
<button type="button" class="btn btn-secondary btn-toggle" [class.btn-primary]="snapshot.isListView" [disabled]="snapshot.isListView" (click)="changeView(true)">
<i class="icon-list"></i>
</button>
<button type="button" class="btn btn-secondary btn-toggle" [class.btn-primary]="!isListView" [disabled]="!isListView" (click)="changeView(false)">
<button type="button" class="btn btn-secondary btn-toggle" [class.btn-primary]="!snapshot.isListView" [disabled]="!snapshot.isListView" (click)="changeView(false)">
<i class="icon-grid"></i>
</button>
</div>
@ -22,10 +22,10 @@
<div class="body">
<ng-container *ngIf="!isListView; else listTemplate">
<div class="row no-gutters">
<sqx-asset *ngFor="let file of newAssets" [initFile]="file"
<sqx-asset *ngFor="let file of snapshot.newAssets" [initFile]="file"
(failed)="removeLoadingAsset(file)" (loaded)="addAsset(file, $event)">
</sqx-asset>
<sqx-asset *ngFor="let asset of oldAssets; trackBy: trackByAsset" [asset]="asset" removeMode="true"
<sqx-asset *ngFor="let asset of snapshot.oldAssets; trackBy: trackByAsset" [asset]="asset" removeMode="true"
(updated)="notifyOthers($event)" (removing)="removeLoadedAsset($event)">
</sqx-asset>
</div>
@ -33,14 +33,14 @@
<ng-template #listTemplate>
<div class="list-view">
<sqx-asset *ngFor="let file of newAssets" [initFile]="file"
<sqx-asset *ngFor="let file of snapshot.newAssets" [initFile]="file"
[isListView]="true" (failed)="removeLoadingAsset(file)" (loaded)="addAsset(file, $event)">
</sqx-asset>
<div
[sqxSortModel]="oldAssets.values"
[sqxSortModel]="snapshot.oldAssets.values"
(sqxSorted)="sortAssets($event)">
<div *ngFor="let asset of oldAssets; trackBy: trackByAsset">
<div *ngFor="let asset of snapshot.oldAssets; trackBy: trackByAsset">
<sqx-asset [asset]="asset" removeMode="true" [isListView]="true"
(updated)="notifyOthers($event)" (removing)="removeLoadedAsset($event)">
</sqx-asset>

102
src/Squidex/app/features/content/shared/assets-editor.component.ts

@ -7,9 +7,8 @@
// tslint:disable:prefer-for-of
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, OnDestroy, OnInit } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Subscription } from 'rxjs';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, OnInit } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import {
AppsState,
@ -19,6 +18,7 @@ import {
ImmutableArray,
LocalStoreService,
MessageBus,
StatefulControlComponent,
Types
} from '@app/shared';
@ -34,6 +34,14 @@ class AssetUpdated {
}
}
interface State {
newAssets: ImmutableArray<File>;
oldAssets: ImmutableArray<AssetDto>;
isListView: boolean;
}
@Component({
selector: 'sqx-assets-editor',
styleUrls: ['./assets-editor.component.scss'],
@ -41,39 +49,32 @@ class AssetUpdated {
providers: [SQX_ASSETS_EDITOR_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AssetsEditorComponent implements ControlValueAccessor, OnInit, OnDestroy {
private callChange = (v: any) => { /* NOOP */ };
private callTouched = () => { /* NOOP */ };
private subscription: Subscription;
export class AssetsEditorComponent extends StatefulControlComponent<State, string[]> implements OnInit {
public assetsDialog = new DialogModel();
public newAssets = ImmutableArray.empty<File>();
public oldAssets = ImmutableArray.empty<AssetDto>();
public isListView = false;
public isDisabled = false;
constructor(
constructor(changeDetector: ChangeDetectorRef,
private readonly appsState: AppsState,
private readonly assetsService: AssetsService,
private readonly changeDetector: ChangeDetectorRef,
private readonly localStore: LocalStoreService,
private readonly messageBus: MessageBus
) {
this.isListView = this.localStore.getBoolean('squidex.assets.list-view');
super(changeDetector, {
oldAssets: ImmutableArray.empty(),
newAssets: ImmutableArray.empty(),
isListView: localStore.getBoolean('squidex.assets.list-view')
});
}
public writeValue(obj: any) {
if (Types.isArrayOfString(obj)) {
if (!Types.isEquals(obj, this.oldAssets.map(x => x.id).values)) {
if (!Types.isEquals(obj, this.snapshot.oldAssets.map(x => x.id).values)) {
const assetIds: string[] = obj;
this.assetsService.getAssets(this.appsState.appName, 0, 0, undefined, undefined, obj)
.subscribe(dtos => {
this.setAssets(ImmutableArray.of(assetIds.map(id => dtos.items.find(x => x.id === id)!).filter(a => !!a)));
if (this.oldAssets.length !== assetIds.length) {
if (this.snapshot.oldAssets.length !== assetIds.length) {
this.updateValue();
}
}, () => {
@ -89,42 +90,18 @@ export class AssetsEditorComponent implements ControlValueAccessor, OnInit, OnDe
this.messageBus.emit(new AssetUpdated(asset, this));
}
public ngOnDestroy() {
this.subscription.unsubscribe();
}
public ngOnInit() {
this.subscription =
this.observe(
this.messageBus.of(AssetUpdated)
.subscribe(event => {
if (event.source !== this) {
this.setAssets(this.oldAssets.replaceBy('id', event.asset));
this.setAssets(this.snapshot.oldAssets.replaceBy('id', event.asset));
}
});
}
public setAssets(asset: ImmutableArray<AssetDto>) {
this.oldAssets = asset;
this.changeDetector.markForCheck();
}
public setDisabledState(isDisabled: boolean): void {
this.isDisabled = isDisabled;
this.changeDetector.markForCheck();
}));
}
public noop() {
return;
}
public registerOnChange(fn: any) {
this.callChange = fn;
}
public registerOnTouched(fn: any) {
this.callTouched = fn;
public setAssets(oldAssets: ImmutableArray<AssetDto>) {
this.next(s => ({ ...s, oldAssets }));
}
public pasteFiles(event: ClipboardEvent) {
@ -132,7 +109,7 @@ export class AssetsEditorComponent implements ControlValueAccessor, OnInit, OnDe
const file = event.clipboardData.items[i].getAsFile();
if (file) {
this.newAssets = this.newAssets.pushFront(file);
this.next(s => ({ ...s, newAssets: s.newAssets.pushFront(file) }));
}
}
}
@ -142,15 +119,13 @@ export class AssetsEditorComponent implements ControlValueAccessor, OnInit, OnDe
const file = files[i];
if (file) {
this.newAssets = this.newAssets.pushFront(file);
this.next(s => ({ ...s, newAssets: s.newAssets.pushFront(file) }));
}
}
}
public selectAssets(assets: AssetDto[]) {
for (let asset of assets) {
this.oldAssets = this.oldAssets.push(asset);
}
this.setAssets(this.snapshot.oldAssets.push(...assets));
if (assets.length > 0) {
this.updateValue();
@ -161,8 +136,11 @@ export class AssetsEditorComponent implements ControlValueAccessor, OnInit, OnDe
public addAsset(file: File, asset: AssetDto) {
if (asset && file) {
this.newAssets = this.newAssets.remove(file);
this.oldAssets = this.oldAssets.pushFront(asset);
this.next(s => ({
...s,
newAssets: s.newAssets.remove(file),
oldAssets: s.oldAssets.pushFront(asset)
}));
this.updateValue();
}
@ -170,7 +148,7 @@ export class AssetsEditorComponent implements ControlValueAccessor, OnInit, OnDe
public sortAssets(assets: AssetDto[]) {
if (assets) {
this.oldAssets = ImmutableArray.of(assets);
this.setAssets(ImmutableArray.of(assets));
this.updateValue();
}
@ -178,24 +156,24 @@ export class AssetsEditorComponent implements ControlValueAccessor, OnInit, OnDe
public removeLoadedAsset(asset: AssetDto) {
if (asset) {
this.oldAssets = this.oldAssets.remove(asset);
this.setAssets(this.snapshot.oldAssets.remove(asset));
this.updateValue();
}
}
public removeLoadingAsset(file: File) {
this.newAssets = this.newAssets.remove(file);
this.next(s => ({ ...s, newAssets: s.newAssets.remove(file) }));
}
public changeView(isListView: boolean) {
this.isListView = isListView;
this.next(s => ({ ...s, isListView }));
this.localStore.setBoolean('squidex.assets.list-view', isListView);
}
private updateValue() {
let ids: string[] | null = this.oldAssets.values.map(x => x.id);
let ids: string[] | null = this.snapshot.oldAssets.values.map(x => x.id);
if (ids.length === 0) {
ids = null;
@ -203,11 +181,9 @@ export class AssetsEditorComponent implements ControlValueAccessor, OnInit, OnDe
this.callTouched();
this.callChange(ids);
this.changeDetector.markForCheck();
}
public trackByAsset(index: number, asset: AssetDto) {
public trackByAsset(asset: AssetDto) {
return asset.id;
}
}

4
src/Squidex/app/features/content/shared/preview-button.component.ts

@ -20,10 +20,10 @@ import {
selector: 'sqx-preview-button',
styleUrls: ['./preview-button.component.scss'],
templateUrl: './preview-button.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [
fadeAnimation
]
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class PreviewButtonComponent implements OnInit {
@Input()

16
src/Squidex/app/features/content/shared/references-editor.component.html

@ -1,27 +1,27 @@
<div class="references-container" [class.disabled]="isDisabled">
<ng-container *ngIf="schema">
<div class="references-container" [class.disabled]="snapshot.isDisabled">
<ng-container *ngIf="snapshot.schema">
<div class="drop-area-container">
<div class="drop-area" (click)="selectorDialog.show()">
Click here to link content items.
</div>
</div>
<table class="table table-items table-fixed" [class.disabled]="isDisabled" *ngIf="schema && contentItems && contentItems.length > 0"
[sqxSortModel]="contentItems.values"
<table class="table table-items table-fixed" [class.disabled]="snapshot.isDisabled" *ngIf="snapshot.schema && snapshot.contentItems && snapshot.contentItems.length > 0"
[sqxSortModel]="snapshot.contentItems.values"
(sqxSorted)="sort($event)">
<tbody *ngFor="let content of contentItems">
<tbody *ngFor="let content of snapshot.contentItems">
<tr [sqxContent]="content"
[language]="language"
[isReadOnly]="true"
[isReference]="true"
[schema]="schema"
[schema]="snapshot.schema"
(deleting)="remove(content)"></tr>
<tr class="spacer"></tr>
</tbody>
</table>
</ng-container>
<div class="invalid" *ngIf="isInvalidSchema">
<div class="invalid" *ngIf="snapshot.schemaInvalid">
Schema not found or not configured yet.
</div>
</div>
@ -30,7 +30,7 @@
<sqx-contents-selector
[language]="language"
[languages]="languages"
[schema]="schema"
[schema]="snapshot.schema"
(selected)="select($event)">
</sqx-contents-selector>
</ng-container>

75
src/Squidex/app/features/content/shared/references-editor.component.ts

@ -8,7 +8,7 @@
// tslint:disable:prefer-for-of
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import {
AppLanguageDto,
@ -20,6 +20,7 @@ import {
MathHelper,
SchemaDetailsDto,
SchemasService,
StatefulControlComponent,
Types
} from '@app/shared';
@ -27,6 +28,13 @@ export const SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ReferencesEditorComponent), multi: true
};
interface State {
schema?: SchemaDetailsDto;
schemaInvalid: boolean;
contentItems: ImmutableArray<ContentDto>;
}
@Component({
selector: 'sqx-references-editor',
styleUrls: ['./references-editor.component.scss'],
@ -34,10 +42,7 @@ export const SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
providers: [SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ReferencesEditorComponent implements ControlValueAccessor, OnInit {
private callChange = (v: any) => { /* NOOP */ };
private callTouched = () => { /* NOOP */ };
export class ReferencesEditorComponent extends StatefulControlComponent<State, string[]> implements OnInit {
@Input()
public schemaId: string;
@ -49,49 +54,41 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit {
public selectorDialog = new DialogModel();
public schema: SchemaDetailsDto;
public contentItems = ImmutableArray.empty<ContentDto>();
public isDisabled = false;
public isInvalidSchema = false;
constructor(
constructor(changeDetector: ChangeDetectorRef,
private readonly appsState: AppsState,
private readonly changeDetector: ChangeDetectorRef,
private readonly contentsService: ContentsService,
private readonly schemasService: SchemasService
) {
super(changeDetector, {
schemaInvalid: false,
contentItems: ImmutableArray.empty()
});
}
public ngOnInit() {
if (this.schemaId === MathHelper.EMPTY_GUID) {
this.isInvalidSchema = true;
this.next(s => ({ ...s, schemaInvalid: true }));
return;
}
this.schemasService.getSchema(this.appsState.appName, this.schemaId)
.subscribe(dto => {
this.schema = dto;
this.changeDetector.markForCheck();
.subscribe(schema => {
this.next(s => ({ ...s, schema }));
}, () => {
this.isInvalidSchema = true;
this.changeDetector.markForCheck();
this.next(s => ({ ...s, schemaInvalid: true }));
});
}
public writeValue(obj: any) {
if (Types.isArrayOfString(obj)) {
if (!Types.isEquals(obj, this.contentItems.map(x => x.id).values)) {
if (!Types.isEquals(obj, this.snapshot.contentItems.map(x => x.id).values)) {
const contentIds: string[] = obj;
this.contentsService.getContents(this.appsState.appName, this.schemaId, 10000, 0, undefined, contentIds)
.subscribe(dtos => {
this.setContentItems(ImmutableArray.of(contentIds.map(id => dtos.items.find(c => c.id === id)!).filter(r => !!r)));
if (this.contentItems.length !== contentIds.length) {
if (this.snapshot.contentItems.length !== contentIds.length) {
this.updateValue();
}
}, () => {
@ -103,29 +100,13 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit {
}
}
public setContentItems(contents: ImmutableArray<ContentDto>) {
this.contentItems = contents;
this.changeDetector.markForCheck();
}
public setDisabledState(isDisabled: boolean): void {
this.isDisabled = isDisabled;
this.changeDetector.markForCheck();
}
public registerOnChange(fn: any) {
this.callChange = fn;
}
public registerOnTouched(fn: any) {
this.callTouched = fn;
public setContentItems(contentItems: ImmutableArray<ContentDto>) {
this.next(s => ({ ...s, contentItems }));
}
public select(contents: ContentDto[]) {
for (let content of contents) {
this.contentItems = this.contentItems.push(content);
this.setContentItems(this.snapshot.contentItems.push(content));
}
if (contents.length > 0) {
@ -137,7 +118,7 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit {
public remove(content: ContentDto) {
if (content) {
this.contentItems = this.contentItems.remove(content);
this.setContentItems(this.snapshot.contentItems.remove(content));
this.updateValue();
}
@ -145,14 +126,14 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit {
public sort(contents: ContentDto[]) {
if (contents) {
this.contentItems = ImmutableArray.of(contents);
this.setContentItems(ImmutableArray.of(contents));
this.updateValue();
}
}
private updateValue() {
let ids: string[] | null = this.contentItems.values.map(x => x.id);
let ids: string[] | null = this.snapshot.contentItems.values.map(x => x.id);
if (ids.length === 0) {
ids = null;
@ -160,7 +141,5 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit {
this.callTouched();
this.callChange(ids);
this.changeDetector.markForCheck();
}
}

10
src/Squidex/app/framework/angular/code.component.ts

@ -5,9 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, ChangeDetectorRef, Component } from '@angular/core';
import { PureComponent } from '@app/framework/internal';
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'sqx-code',
@ -15,8 +13,4 @@ import { PureComponent } from '@app/framework/internal';
templateUrl: './code.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CodeComponent extends PureComponent {
constructor(changeDetector: ChangeDetectorRef) {
super(changeDetector);
}
}
export class CodeComponent { }

8
src/Squidex/app/framework/angular/forms/autocomplete.component.html

@ -5,13 +5,13 @@
autocorrect="off"
autocapitalize="off">
<div *ngIf="suggestedItems.length > 0" [sqxModalTarget]="input" class="control-dropdown" #container position="bottomLeft">
<div *ngFor="let item of suggestedItems; let i = index;" class="control-dropdown-item control-dropdown-item-selectable"
[class.active]="i === suggestedIndex"
<div *ngIf="snapshot.suggestedItems.length > 0" [sqxModalTarget]="input" class="control-dropdown" #container position="bottomLeft">
<div *ngFor="let item of snapshot.suggestedItems; let i = index;" class="control-dropdown-item control-dropdown-item-selectable"
[class.active]="i === snapshot.suggestedIndex"
[container]="container"
(mousedown)="selectItem(item)"
(mouseover)="selectIndex(i)"
[sqxScrollActive]="i === suggestedIndex">
[sqxScrollActive]="i === snapshot.suggestedIndex">
<ng-container *ngIf="!itemTemplate">{{item}}</ng-container>
<ng-template *ngIf="itemTemplate" [sqxTemplateWrapper]="itemTemplate" [item]="item" [index]="i"></ng-template>

71
src/Squidex/app/framework/angular/forms/autocomplete.component.ts

@ -5,11 +5,13 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, ContentChild, ElementRef, forwardRef, Input, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Observable, of, Subscription } from 'rxjs';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, ElementRef, forwardRef, Input, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { FormControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Observable, of } from 'rxjs';
import { catchError, debounceTime, distinctUntilChanged, filter, map, switchMap, tap } from 'rxjs/operators';
import { StatefulControlComponent } from '@app/shared';
export interface AutocompleteSource {
find(query: string): Observable<any[]>;
}
@ -23,6 +25,11 @@ export const SQX_AUTOCOMPLETE_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => AutocompleteComponent), multi: true
};
interface State {
suggestedItems: any[];
suggestedIndex: number;
}
@Component({
selector: 'sqx-autocomplete',
styleUrls: ['./autocomplete.component.scss'],
@ -30,11 +37,7 @@ export const SQX_AUTOCOMPLETE_CONTROL_VALUE_ACCESSOR: any = {
providers: [SQX_AUTOCOMPLETE_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, OnInit {
private subscription: Subscription;
private callChange = (v: any) => { /* NOOP */ };
private callTouched = () => { /* NOOP */ };
export class AutocompleteComponent extends StatefulControlComponent<State, any[]> implements OnInit {
@Input()
public source: AutocompleteSource;
@ -53,17 +56,17 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O
@ViewChild('input')
public inputControl: ElementRef<HTMLInputElement>;
public suggestedItems: any[] = [];
public suggestedIndex = -1;
public queryInput = new FormControl();
public ngOnDestroy() {
this.subscription.unsubscribe();
constructor(changeDetector: ChangeDetectorRef) {
super(changeDetector, {
suggestedItems: [],
suggestedIndex: -1
});
}
public ngOnInit() {
this.subscription =
this.observe(
this.queryInput.valueChanges.pipe(
tap(query => {
this.callChange(query);
@ -80,9 +83,12 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O
filter(query => !!query && !!this.source),
switchMap(query => this.source.find(query)), catchError(() => of([])))
.subscribe(items => {
this.suggestedIndex = -1;
this.suggestedItems = items || [];
});
this.next(s => ({
...s,
suggestedIndex: -1,
suggestedItems: items || []
}));
}));
}
public onKeyDown(event: KeyboardEvent) {
@ -98,7 +104,7 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O
this.reset();
return false;
case KEY_ENTER:
if (this.suggestedItems.length > 0 && this.selectItem()) {
if (this.snapshot.suggestedItems.length > 0 && this.selectItem()) {
return false;
}
break;
@ -149,11 +155,11 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O
public selectItem(selection: any | null = null): boolean {
if (!selection) {
selection = this.suggestedItems[this.suggestedIndex];
selection = this.snapshot.suggestedItems[this.snapshot.suggestedIndex];
}
if (!selection && this.suggestedItems.length === 1) {
selection = this.suggestedItems[0];
if (!selection && this.snapshot.suggestedItems.length === 1) {
selection = this.snapshot.suggestedItems[0];
}
if (selection) {
@ -174,24 +180,24 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O
return false;
}
public selectIndex(selection: number) {
if (selection < 0) {
selection = 0;
public selectIndex(suggestedIndex: number) {
if (suggestedIndex < 0) {
suggestedIndex = 0;
}
if (selection >= this.suggestedItems.length) {
selection = this.suggestedItems.length - 1;
if (suggestedIndex >= this.snapshot.suggestedItems.length) {
suggestedIndex = this.snapshot.suggestedItems.length - 1;
}
this.suggestedIndex = selection;
this.next(s => ({ ...s, suggestedIndex }));
}
private up() {
this.selectIndex(this.suggestedIndex - 1);
this.selectIndex(this.snapshot.suggestedIndex - 1);
}
private down() {
this.selectIndex(this.suggestedIndex + 1);
this.selectIndex(this.snapshot.suggestedIndex + 1);
}
private resetForm() {
@ -199,7 +205,10 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O
}
private reset() {
this.suggestedItems = [];
this.suggestedIndex = -1;
this.next(s => ({
...s,
suggestedItems: [],
suggestedIndex: -1
}));
}
}

4
src/Squidex/app/framework/angular/forms/checkbox-group.component.ts

@ -44,7 +44,7 @@ export class CheckboxGroupComponent extends StatefulControlComponent<State, stri
public writeValue(obj: any) {
const checkedValues = Types.isArrayOfString(obj) ? obj.filter(x => this.values.indexOf(x) >= 0) : [];
this.next({ checkedValues });
this.next(s => ({ ...s, checkedValues }));
}
public check(isChecked: boolean, value: string) {
@ -56,7 +56,7 @@ export class CheckboxGroupComponent extends StatefulControlComponent<State, stri
checkedValues = checkedValues.filter(x => x !== value);
}
this.next({ checkedValues });
this.next(s => ({ ...s, checkedValues }));
this.callChange(checkedValues);
}

2
src/Squidex/app/framework/angular/forms/control-errors.component.ts

@ -127,6 +127,6 @@ export class ControlErrorsComponent extends StatefulComponent<State> implements
}
}
this.next({ errorMessages });
this.next(() => ({ errorMessages }));
}
}

20
src/Squidex/app/framework/angular/forms/stars.component.ts

@ -6,7 +6,7 @@
*/
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { StatefulControlComponent, Types } from '@app/framework/internal';
@ -28,7 +28,7 @@ interface State {
providers: [SQX_STARS_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class StarsComponent extends StatefulControlComponent<State, number | null> implements ControlValueAccessor {
export class StarsComponent extends StatefulControlComponent<State, number | null> {
private maximumStarsValue = 5;
@Input()
@ -38,13 +38,13 @@ export class StarsComponent extends StatefulControlComponent<State, number | nul
if (this.maximumStarsValue !== maxStars) {
this.maximumStarsValue = value;
const starsArray = [];
const starsArray: number[] = [];
for (let i = 1; i <= value; i++) {
for (let i = 1; i <= maxStars; i++) {
starsArray.push(i);
}
this.next({ starsArray });
this.next(s => ({ ...s, starsArray }));
}
}
@ -63,7 +63,7 @@ export class StarsComponent extends StatefulControlComponent<State, number | nul
public writeValue(obj: any) {
const value = Types.isNumber(obj) ? obj : 0;
this.next({ stars: value, value });
this.next(s => ({ ...s, stars: value, value }));
}
public setPreview(stars: number) {
@ -71,7 +71,7 @@ export class StarsComponent extends StatefulControlComponent<State, number | nul
return;
}
this.next({ stars });
this.next(s => ({ ...s, stars }));
}
public stopPreview() {
@ -79,7 +79,7 @@ export class StarsComponent extends StatefulControlComponent<State, number | nul
return;
}
this.next(s => { s.stars = s.value || 0; });
this.next(s => ({ ...s, stars: s.value || 0 }));
}
public reset() {
@ -88,7 +88,7 @@ export class StarsComponent extends StatefulControlComponent<State, number | nul
}
if (this.snapshot.value) {
this.next({ stars: -1, value: null });
this.next(s => ({ ...s, stars: -1, value: null }));
this.callChange(null);
this.callTouched();
@ -103,7 +103,7 @@ export class StarsComponent extends StatefulControlComponent<State, number | nul
}
if (this.snapshot.value !== value) {
this.next({ stars: value, value });
this.next(s => ({ ...s, stars: value, value }));
this.callChange(value);
this.callTouched();

14
src/Squidex/app/framework/angular/forms/tag-editor.component.html

@ -1,9 +1,9 @@
<ng-container>
<div class="form-control {{class}}" #form (click)="input.focus()"
[class.single-line]="singleLine"
[class.focus]="hasFocus"
[class.disabled]="addInput.disabled">
<span class="item" *ngFor="let item of items; let i = index" [class.disabled]="addInput.disabled">
[class.focus]="snapshot.hasFocus"
[class.disabled]snapshot.="addInput.disabled">
<span class="item" *ngFor="let item of snapshot.items; let i = index" [class.disabled]="addInput.disabled">
{{item}} <i class="icon-close" (click)="remove(i)"></i>
</span>
@ -23,13 +23,13 @@
spellcheck="false">
</div>
<div *ngIf="suggestedItems.length > 0" [sqxModalTarget]="form" class="control-dropdown" #container position="bottomLeft">
<div *ngFor="let item of suggestedItems; let i = index;" class="control-dropdown-item control-dropdown-item-selectable"
[class.active]="i === suggestedIndex"
<div *ngIf="snapshot.suggestedItems.length > 0" [sqxModalTarget]="form" class="control-dropdown" #container position="bottomLeft">
<div *ngFor="let item of snapshot.suggestedItems; let i = index;" class="control-dropdown-item control-dropdown-item-selectable"
[class.active]="i === snapshot.suggestedIndex"
[container]="container"
(mousedown)="selectValue(item)"
(mouseover)="selectIndex(i)"
[sqxScrollActive]="i === suggestedIndex">
[sqxScrollActive]="i === snapshot.suggestedIndex">
<ng-container>{{item}}</ng-container>
</div>
</div>

64
src/Squidex/app/framework/angular/forms/tag-editor.component.ts

@ -6,7 +6,7 @@
*/
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core';
import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { FormControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { distinctUntilChanged, map, tap } from 'rxjs/operators';
import { StatefulControlComponent, Types } from '@app/framework/internal';
@ -90,7 +90,7 @@ interface State {
providers: [SQX_TAG_EDITOR_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TagEditorComponent extends StatefulControlComponent<State, any[]> implements AfterViewInit, ControlValueAccessor, OnInit {
export class TagEditorComponent extends StatefulControlComponent<State, any[]> implements AfterViewInit, OnInit {
@Input()
public converter: Converter = new StringConverter();
@ -167,8 +167,11 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i
}
}))
.subscribe(items => {
this.suggestedIndex = -1;
this.suggestedItems = items || [];
this.next(s => ({
...s,
suggestedIndex: -1,
suggestedItems: items || []
}));
}));
}
@ -177,12 +180,10 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i
this.resetSize();
if (this.converter && Types.isArrayOf(obj, v => this.converter.isValidValue(v))) {
this.items = obj;
this.next(s => ({ ...s, items: obj }));
} else {
this.items = [];
this.next(s => ({ ...s, items: [] }));
}
this.changeDetector.markForCheck();
}
public setDisabledState(isDisabled: boolean): void {
@ -197,7 +198,7 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i
public focus() {
if (this.addInput.enabled) {
this.next({ hasFocus: true });
this.next(s => ({ ...s, hasFocus: true }));
}
}
@ -211,7 +212,7 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i
}
public remove(index: number) {
this.updateItems([...this.items.slice(0, index), ...this.items.splice(index + 1)]);
this.updateItems(this.snapshot.items.filter((_, i) => i !== index));
}
public resetSize() {
@ -265,7 +266,7 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i
const value = <string>this.addInput.value;
if (!value || value.length === 0) {
this.updateItems(this.items.slice(0, this.items.length - 1));
this.updateItems(this.snapshot.items.slice(0, this.snapshot.items.length - 1));
return false;
}
@ -276,8 +277,8 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i
this.down();
return false;
} else if (key === KEY_ENTER) {
if (this.suggestedIndex >= 0) {
if (this.selectValue(this.suggestedItems[this.suggestedIndex])) {
if (this.snapshot.suggestedIndex >= 0) {
if (this.selectValue(this.snapshot.suggestedItems[this.snapshot.suggestedIndex])) {
return false;
}
} else if (this.acceptEnter) {
@ -298,8 +299,8 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i
if (value && this.converter.isValidInput(value)) {
const converted = this.converter.convert(value);
if (this.allowDuplicates || this.items.indexOf(converted) < 0) {
this.updateItems([...this.items, converted]);
if (this.allowDuplicates || this.snapshot.items.indexOf(converted) < 0) {
this.updateItems([...this.snapshot.items, converted]);
}
this.resetForm();
@ -309,24 +310,27 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i
}
private resetAutocompletion() {
this.suggestedItems = [];
this.suggestedIndex = -1;
this.next(s => ({
...s,
suggestedItems: [],
suggestedIndex: -1
}));
}
public selectIndex(selection: number) {
if (selection < 0) {
selection = 0;
public selectIndex(suggestedIndex: number) {
if (suggestedIndex < 0) {
suggestedIndex = 0;
}
if (selection >= this.suggestedItems.length) {
selection = this.suggestedItems.length - 1;
if (suggestedIndex >= this.snapshot.suggestedItems.length) {
suggestedIndex = this.snapshot.suggestedItems.length - 1;
}
this.suggestedIndex = selection;
this.next(s => ({ ...s, suggestedIndex }));
}
public resetFocus(): any {
this.hasFocus = false;
this.next(s => ({ ...s, hasFocus: false }));
}
private resetForm() {
@ -334,11 +338,11 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i
}
private up() {
this.selectIndex(this.suggestedIndex - 1);
this.selectIndex(this.snapshot.suggestedIndex - 1);
}
private down() {
this.selectIndex(this.suggestedIndex + 1);
this.selectIndex(this.snapshot.suggestedIndex + 1);
}
public onCut(event: ClipboardEvent) {
@ -351,7 +355,7 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i
public onCopy(event: ClipboardEvent) {
if (!this.hasSelection()) {
event.clipboardData.setData('text/plain', this.items.filter(x => !!x).join(','));
event.clipboardData.setData('text/plain', this.snapshot.items.filter(x => !!x).join(','));
event.preventDefault();
}
@ -363,7 +367,7 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i
if (value) {
this.resetForm();
const values = [...this.items];
const values = [...this.snapshot.items];
for (let part of value.split(',')) {
const converted = this.converter.convert(part);
@ -387,12 +391,12 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i
}
private updateItems(items: any[]) {
const items = items;
this.next(s => ({ ...s, items }));
if (items.length === 0 && this.undefinedWhenEmpty) {
this.callChange(undefined);
} else {
this.callChange(this.items);
this.callChange(items);
}
this.resetSize();

10
src/Squidex/app/framework/angular/forms/toggle.component.ts

@ -6,7 +6,7 @@
*/
import { ChangeDetectorRef, Component, forwardRef, Input } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { Types } from '@app/framework/internal';
@ -26,7 +26,7 @@ interface State {
templateUrl: './toggle.component.html',
providers: [SQX_TOGGLE_CONTROL_VALUE_ACCESSOR]
})
export class ToggleComponent extends StatefulControlComponent<State, boolean | null> implements ControlValueAccessor {
export class ToggleComponent extends StatefulControlComponent<State, boolean | null> {
@Input()
public threeStates = false;
@ -37,7 +37,9 @@ export class ToggleComponent extends StatefulControlComponent<State, boolean | n
}
public writeValue(obj: any) {
this.next({ isChecked: Types.isBoolean(obj) ? obj : null });
const isChecked = Types.isBoolean(obj) ? obj : null;
this.next(s => ({ ...s, isChecked }));
}
public changeState(event: MouseEvent) {
@ -59,7 +61,7 @@ export class ToggleComponent extends StatefulControlComponent<State, boolean | n
isChecked = !(isChecked === true);
}
this.next({ isChecked });
this.next(s => ({ ...s, isChecked }));
this.callChange(isChecked);
this.callTouched();

26
src/Squidex/app/framework/angular/modals/dialog-renderer.component.ts

@ -55,9 +55,10 @@ export class DialogRendererComponent extends StatefulComponent<State> implements
this.observe(
this.dialogs.notifications.subscribe(notification => {
this.next(state => {
state.notifications = [...state.notifications, notification];
});
this.next(s => ({
...s,
notifications: [...s.notifications, notification]
}));
if (notification.displayTime > 0) {
this.observe(timer(notification.displayTime).subscribe(() => {
@ -68,12 +69,10 @@ export class DialogRendererComponent extends StatefulComponent<State> implements
this.observe(
this.dialogs.dialogs
.subscribe(request => {
.subscribe(dialogRequest => {
this.cancel();
this.next(state => {
state.dialogRequest = request;
});
this.next(s => ({ ...s, dialogRequest }));
}));
}
@ -90,17 +89,16 @@ export class DialogRendererComponent extends StatefulComponent<State> implements
}
private finishRequest(value: boolean) {
this.next(state => {
if (state.dialogRequest) {
state.dialogRequest.complete(value);
state.dialogRequest = null;
this.next(s => {
if (s.dialogRequest) {
s.dialogRequest.complete(value);
}
return { ...s, dialogRequest: null };
});
}
public close(notification: Notification) {
this.next(state => {
state.notifications = state.notifications.filter(n => notification !== n);
});
this.next(s => ({ ...s, notifications: s.notifications.filter(n => notification !== n) }));
}
}

6
src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.ts

@ -12,7 +12,7 @@ import {
fadeAnimation,
ModalModel,
OnboardingService,
PureComponent,
StatefulComponent,
Types
} from '@app/framework/internal';
@ -25,7 +25,7 @@ import {
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnboardingTooltipComponent extends PureComponent implements OnDestroy, OnInit {
export class OnboardingTooltipComponent extends StatefulComponent implements OnDestroy, OnInit {
public tooltipModal = new ModalModel();
@Input()
@ -44,7 +44,7 @@ export class OnboardingTooltipComponent extends PureComponent implements OnDestr
private readonly onboardingService: OnboardingService,
private readonly renderer: Renderer2
) {
super(changeDetector);
super(changeDetector, {});
}
public ngOnDestroy() {

10
src/Squidex/app/framework/angular/modals/root-view.component.ts

@ -5,9 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ViewChild, ViewContainerRef } from '@angular/core';
import { PureComponent } from '@app/framework/internal';
import { ChangeDetectionStrategy, Component, ViewChild, ViewContainerRef } from '@angular/core';
@Component({
selector: 'sqx-root-view',
@ -15,11 +13,7 @@ import { PureComponent } from '@app/framework/internal';
templateUrl: './root-view.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class RootViewComponent extends PureComponent {
export class RootViewComponent {
@ViewChild('element', { read: ViewContainerRef })
public viewContainer: ViewContainerRef;
constructor(changeDetector: ChangeDetectorRef) {
super(changeDetector);
}
}

6
src/Squidex/app/framework/angular/modals/tooltip.component.ts

@ -10,7 +10,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy
import {
fadeAnimation,
ModalModel,
PureComponent
StatefulComponent
} from '@app/framework/internal';
@Component({
@ -22,7 +22,7 @@ import {
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TooltipComponent extends PureComponent implements OnDestroy, OnInit {
export class TooltipComponent extends StatefulComponent implements OnDestroy, OnInit {
@Input()
public target: any;
@ -34,7 +34,7 @@ export class TooltipComponent extends PureComponent implements OnDestroy, OnInit
constructor(changeDetector: ChangeDetectorRef,
private readonly renderer: Renderer2
) {
super(changeDetector);
super(changeDetector, {});
}
public ngOnInit() {

10
src/Squidex/app/framework/angular/pager.component.ts

@ -5,9 +5,9 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output } from '@angular/core';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { Pager, PureComponent } from '@app/framework/internal';
import { Pager } from '@app/framework/internal';
@Component({
selector: 'sqx-pager',
@ -15,7 +15,7 @@ import { Pager, PureComponent } from '@app/framework/internal';
templateUrl: './pager.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class PagerComponent extends PureComponent {
export class PagerComponent {
@Output()
public nextPage = new EventEmitter();
@ -27,8 +27,4 @@ export class PagerComponent extends PureComponent {
@Input()
public hideWhenButtonsDisabled = false;
constructor(changeDetector: ChangeDetectorRef) {
super(changeDetector);
}
}

9
src/Squidex/app/framework/angular/panel.component.ts

@ -5,9 +5,9 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Input, OnDestroy, OnInit, Renderer2, ViewChild } from '@angular/core';
import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, Input, OnDestroy, OnInit, Renderer2, ViewChild } from '@angular/core';
import { PureComponent, slideRightAnimation } from '@app/framework/internal';
import { slideRightAnimation } from '@app/framework/internal';
import { PanelContainerDirective } from './panel-container.directive';
@ -20,7 +20,7 @@ import { PanelContainerDirective } from './panel-container.directive';
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class PanelComponent extends PureComponent implements AfterViewInit, OnDestroy, OnInit {
export class PanelComponent implements AfterViewInit, OnDestroy, OnInit {
private styleWidth: string;
public renderWidth = 0;
@ -61,11 +61,10 @@ export class PanelComponent extends PureComponent implements AfterViewInit, OnDe
@ViewChild('panel')
public panel: ElementRef<HTMLElement>;
constructor(changeDetector: ChangeDetectorRef,
constructor(
private readonly container: PanelContainerDirective,
private readonly renderer: Renderer2
) {
super(changeDetector);
}
public ngOnDestroy() {

6
src/Squidex/app/framework/angular/shortcut.component.ts

@ -7,13 +7,13 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from '@angular/core';
import { PureComponent, ShortcutService } from '@app/framework/internal';
import { ShortcutService, StatefulComponent } from '@app/framework/internal';
@Component({
selector: 'sqx-shortcut',
template: ''
})
export class ShortcutComponent extends PureComponent implements OnDestroy, OnInit {
export class ShortcutComponent extends StatefulComponent implements OnDestroy, OnInit {
private lastKeys: string;
@Input()
@ -30,7 +30,7 @@ export class ShortcutComponent extends PureComponent implements OnDestroy, OnIni
private readonly shortcutService: ShortcutService,
private readonly zone: NgZone
) {
super(changeDetector);
super(changeDetector, {});
changeDetector.detach();
}

20
src/Squidex/app/framework/angular/stateful.component.ts

@ -15,7 +15,7 @@ import { State } from '../state';
declare type UnsubscribeFunction = () => void;
export abstract class StatefulComponent<T> extends State<T> implements OnDestroy, OnInit {
export abstract class StatefulComponent<T = any> extends State<T> implements OnDestroy, OnInit {
private subscriptions: (Subscription | UnsubscribeFunction)[] = [];
constructor(
@ -52,11 +52,7 @@ export abstract class StatefulComponent<T> extends State<T> implements OnDestroy
}
}
export interface FormControlState {
isDisabled: boolean;
}
export abstract class StatefulControlComponent<T, TValue> extends StatefulComponent<T & FormControlState> implements ControlValueAccessor {
export abstract class StatefulControlComponent<T, TValue> extends StatefulComponent<T & { isDisabled: boolean }> implements ControlValueAccessor {
private fnChanged = (v: any) => { /* NOOP */ };
private fnTouched = () => { /* NOOP */ };
@ -81,24 +77,18 @@ export abstract class StatefulControlComponent<T, TValue> extends StatefulCompon
}
public setDisabledState(isDisabled: boolean): void {
this.next(state => { state.isDisabled = isDisabled; });
this.next(s => ({ ...s, isDisabled }));
}
public abstract writeValue(obj: any): void;
}
export abstract class PureComponent extends StatefulComponent<any> {
constructor(changeDetector: ChangeDetectorRef) {
super(changeDetector, {});
}
}
export abstract class ExternalControlComponent<TValue> extends PureComponent implements ControlValueAccessor {
export abstract class ExternalControlComponent<TValue> extends StatefulComponent<any> implements ControlValueAccessor {
private fnChanged = (v: any) => { /* NOOP */ };
private fnTouched = () => { /* NOOP */ };
constructor(changeDetector: ChangeDetectorRef) {
super(changeDetector);
super(changeDetector, {});
changeDetector.detach();
}

6
src/Squidex/app/framework/angular/user-report.component.ts

@ -9,8 +9,8 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { timer } from 'rxjs';
import {
PureComponent,
ResourceLoaderService,
StatefulComponent,
UserReportConfig
} from '@app/framework/internal';
@ -18,12 +18,12 @@ import {
selector: 'sqx-user-report',
template: ''
})
export class UserReportComponent extends PureComponent implements OnDestroy, OnInit {
export class UserReportComponent extends StatefulComponent<any> implements OnDestroy, OnInit {
constructor(changeDetector: ChangeDetectorRef,
private readonly config: UserReportConfig,
private readonly resourceLoader: ResourceLoaderService
) {
super(changeDetector);
super(changeDetector, {});
changeDetector.detach();
}

21
src/Squidex/app/framework/state.ts

@ -49,13 +49,13 @@ export class Form<T extends AbstractControl> {
}
public load(value: any) {
this.state.next({ submitted: false, error: null });
this.state.next(_ => ({ submitted: false, error: null }));
this.setValue(value);
}
public submit(): any | null {
this.state.next({ submitted: true });
this.state.next(_ => ({ submitted: true }));
if (this.form.valid) {
const value = fullValue(this.form);
@ -69,7 +69,7 @@ export class Form<T extends AbstractControl> {
}
public submitCompleted(newValue?: any) {
this.state.next({ submitted: false, error: null });
this.state.next(_ => ({ submitted: false, error: null }));
this.enable();
@ -81,7 +81,7 @@ export class Form<T extends AbstractControl> {
}
public submitFailed(error?: string | ErrorDto) {
this.state.next({ submitted: false, error: this.getError(error) });
this.state.next(_ => ({ submitted: false, error: this.getError(error) }));
this.enable();
}
@ -133,17 +133,10 @@ export class State<T extends {}> {
}
public resetState() {
this.next(this.initialState);
this.next(_ => this.initialState);
}
public next(update: ((v: T) => T | void) | Partial<T>) {
if (Types.isFunction(update)) {
const stateNew = { ...this.snapshot };
const stateUpdated = update(stateNew);
this.state.next(stateUpdated || stateNew);
} else {
this.state.next({ ...this.snapshot, ...update });
}
public next(update: (v: T) => T) {
this.state.next(update(this.snapshot));
}
}

4
src/Squidex/app/shared/components/geolocation-editor.component.html

@ -1,7 +1,7 @@
<div class="editor-container">
<form>
<div class="editor" #editor></div>
<input [class.hidden]="!isGoogleMaps" class="form-control search-control" type="text" [disabled]="isDisabled" placeholder="Search Google Maps" #searchBox />
<input [class.hidden]="!snapshot.isGoogleMaps" class="form-control search-control" type="text" [disabled]="snapshot.isDisabled" placeholder="Search Google Maps" #searchBox />
</form>
<div>
<form class="form-inline no-gutters" [formGroup]="geolocationForm" (change)="updateValueByInput()" (ngSubmit)="updateValueByInput()">
@ -24,7 +24,7 @@
</div>
<div class="form-group col-auto">
<button [class.hidden]="!hasValue" type="reset" class="btn btn-text clear" [disabled]="isDisabled" (click)="reset()">Clear</button>
<button [class.hidden]="!hasValue" type="reset" class="btn btn-text clear" [disabled]="snapshot.isDisabled" (click)="reset()">Clear</button>
</div>
</form>
</div>

64
src/Squidex/app/shared/components/geolocation-editor.component.ts

@ -6,10 +6,11 @@
*/
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, ViewChild } from '@angular/core';
import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms';
import { FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms';
import {
ResourceLoaderService,
StatefulControlComponent,
Types,
UIState,
ValidatorsEx
@ -27,6 +28,10 @@ interface Geolocation {
longitude: number;
}
interface State {
isGoogleMaps: boolean;
}
@Component({
selector: 'sqx-geolocation-editor',
styleUrls: ['./geolocation-editor.component.scss'],
@ -34,9 +39,7 @@ interface Geolocation {
providers: [SQX_GEOLOCATION_EDITOR_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class GeolocationEditorComponent implements ControlValueAccessor, AfterViewInit {
private callChange = (v: any) => { /* NOOP */ };
private callTouched = () => { /* NOOP */ };
export class GeolocationEditorComponent extends StatefulControlComponent<State, Geolocation> implements AfterViewInit {
private marker: any;
private map: any;
private value: Geolocation | null = null;
@ -62,20 +65,19 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
});
@ViewChild('editor')
public editor: ElementRef;
public editor: ElementRef<HTMLElement>;
@ViewChild('searchBox')
public searchBoxInput: ElementRef;
public isGoogleMaps = false;
public isDisabled = false;
public searchBoxInput: ElementRef<HTMLInputElement>;
constructor(
private readonly changeDetector: ChangeDetectorRef,
constructor(changeDetector: ChangeDetectorRef,
private readonly resourceLoader: ResourceLoaderService,
private readonly formBuilder: FormBuilder,
private readonly uiState: UIState
) {
super(changeDetector, {
isGoogleMaps: false
});
}
public writeValue(obj: any) {
@ -91,9 +93,9 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
}
public setDisabledState(isDisabled: boolean): void {
this.isDisabled = isDisabled;
super.setDisabledState(isDisabled);
if (!this.isGoogleMaps) {
if (!this.snapshot.isGoogleMaps) {
this.setDisabledStateOSM(isDisabled);
} else {
this.setDisabledStateGoogle(isDisabled);
@ -104,8 +106,6 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
} else {
this.geolocationForm.enable();
}
this.changeDetector.markForCheck();
}
private setDisabledStateOSM(isDisabled: boolean): void {
@ -139,14 +139,6 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
}
}
public registerOnChange(fn: any) {
this.callChange = fn;
}
public registerOnTouched(fn: any) {
this.callTouched = fn;
}
public updateValueByInput() {
const lat = this.geolocationForm.controls['latitude'].value;
const lng = this.geolocationForm.controls['longitude'].value;
@ -164,9 +156,11 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
public ngAfterViewInit() {
this.uiState.settings
.subscribe(settings => {
this.isGoogleMaps = settings.mapType === 'GoogleMaps';
const isGoogleMaps = settings.mapType === 'GoogleMaps';
this.next(s => ({ ...s, isGoogleMaps }));
if (!this.isGoogleMaps) {
if (!this.snapshot.isGoogleMaps) {
this.ngAfterViewInitOSM();
} else {
this.ngAfterViewInitGoogle(settings.mapKey);
@ -189,7 +183,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
this.map.on('click',
(event: any) => {
if (!this.marker && !this.isDisabled) {
if (!this.marker && !this.snapshot.isDisabled) {
const latlng = event.latlng.wrap();
this.updateValue(latlng.lat, latlng.lng);
@ -199,7 +193,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
this.updateMarker(true, false);
if (this.isDisabled) {
if (this.snapshot.isDisabled) {
this.map.zoomControl.disable();
this.map._handlers.forEach((handler: any) => {
@ -224,7 +218,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
this.map.addListener('click',
(event: any) => {
if (!this.isDisabled) {
if (!this.snapshot.isDisabled) {
this.updateValue(event.latLng.lat(), event.latLng.lng());
this.updateMarker(false, true);
}
@ -244,7 +238,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
return;
}
if (!this.isDisabled) {
if (!this.snapshot.isDisabled) {
let lat = place.geometry.location.lat();
let lng = place.geometry.location.lng();
@ -256,7 +250,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
this.updateMarker(true, false);
if (this.isDisabled) {
if (this.snapshot.isDisabled) {
this.map.setOptions({ draggable: false, zoomControl: false });
}
});
@ -264,7 +258,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
public reset() {
this.value = null;
this.searchBoxInput.nativeElement.value = null;
this.searchBoxInput.nativeElement.value = '';
this.updateMarker(true, true);
}
@ -274,7 +268,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
}
private updateMarker(zoom: boolean, fireEvent: boolean) {
if (!this.isGoogleMaps) {
if (!this.snapshot.isGoogleMaps) {
this.updateMarkerOSM(zoom);
} else {
this.updateMarkerGoogle(zoom);
@ -307,7 +301,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
this.updateMarker(false, true);
});
if (this.isDisabled) {
if (this.snapshot.isDisabled) {
this.marker.dragging.disable();
}
}
@ -344,12 +338,12 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
});
this.marker.addListener('drag', (event: any) => {
if (!this.isDisabled) {
if (!this.snapshot.isDisabled) {
this.updateValue(event.latLng.lat(), event.LatLng.lng());
}
});
this.marker.addListener('dragend', (event: any) => {
if (!this.isDisabled) {
if (!this.snapshot.isDisabled) {
this.updateValue(event.latLng.lat(), event.LatLng.lng());
this.updateMarker(false, true);
}

2
src/Squidex/app/shared/components/markdown-editor.component.html

@ -1,5 +1,5 @@
<div #container class="drop-container">
<div #inner [class.fullscreen]="isFullscreen">
<div #inner [class.fullscreen]="snapshot.isFullscreen">
<textarea class="form-control" #editor></textarea>
</div>

36
src/Squidex/app/shared/components/markdown-editor.component.ts

@ -6,12 +6,13 @@
*/
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Renderer2, ViewChild } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import {
AssetDto,
DialogModel,
ResourceLoaderService,
StatefulControlComponent,
Types
} from '@app/shared/internal';
@ -21,6 +22,10 @@ export const SQX_MARKDOWN_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => MarkdownEditorComponent), multi: true
};
interface State {
isFullscreen: false;
}
@Component({
selector: 'sqx-markdown-editor',
styleUrls: ['./markdown-editor.component.scss'],
@ -28,9 +33,7 @@ export const SQX_MARKDOWN_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
providers: [SQX_MARKDOWN_EDITOR_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewInit {
private callChange = (v: any) => { /* NOOP */ };
private callTouched = () => { /* NOOP */ };
export class MarkdownEditorComponent extends StatefulControlComponent<State, string> implements AfterViewInit {
private simplemde: any;
private value: string;
private isDisabled = false;
@ -46,13 +49,14 @@ export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewI
@ViewChild('inner')
public inner: ElementRef;
public isFullscreen = false;
constructor(
private readonly changeDetector: ChangeDetectorRef,
constructor(changeDetector: ChangeDetectorRef,
private readonly renderer: Renderer2,
private readonly resourceLoader: ResourceLoaderService
) {
super(changeDetector, {
isFullscreen: false
});
this.resourceLoader.loadStyle('https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css');
}
@ -72,18 +76,8 @@ export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewI
}
}
public registerOnChange(fn: any) {
this.callChange = fn;
}
public registerOnTouched(fn: any) {
this.callTouched = fn;
}
private showSelector = () => {
this.assetsDialog.show();
this.changeDetector.detectChanges();
}
public ngAfterViewInit() {
@ -182,17 +176,17 @@ export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewI
});
this.simplemde.codemirror.on('refresh', () => {
this.isFullscreen = this.simplemde.isFullscreenActive();
const isFullscreen = this.simplemde.isFullscreenActive();
let target = this.container.nativeElement;
if (this.isFullscreen) {
if (isFullscreen) {
target = document.body;
}
this.renderer.appendChild(target, this.inner.nativeElement);
this.changeDetector.detectChanges();
this.next(s => ({ ...s, isFullscreen }));
});
this.simplemde.codemirror.on('blur', () => {

21
src/Squidex/app/shared/components/rich-editor.component.ts

@ -6,11 +6,12 @@
*/
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, forwardRef, OnDestroy, Output, ViewChild } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import {
AssetDto,
DialogModel,
ExternalControlComponent,
ResourceLoaderService,
Types
} from '@app/shared/internal';
@ -28,9 +29,7 @@ export const SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
providers: [SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class RichEditorComponent implements ControlValueAccessor, AfterViewInit, OnDestroy {
private callChange = (v: any) => { /* NOOP */ };
private callTouched = () => { /* NOOP */ };
export class RichEditorComponent extends ExternalControlComponent<string> implements AfterViewInit, OnDestroy {
private tinyEditor: any;
private tinyInitTimer: any;
private value: string;
@ -44,10 +43,10 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit,
@Output()
public assetPluginClicked = new EventEmitter<any>();
constructor(
private readonly changeDetector: ChangeDetectorRef,
constructor(changeDetector: ChangeDetectorRef,
private readonly resourceLoader: ResourceLoaderService
) {
super(changeDetector);
}
public ngOnDestroy() {
@ -66,8 +65,6 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit,
private showSelector = () => {
this.assetsDialog.show();
this.changeDetector.detectChanges();
}
private getEditorOptions() {
@ -132,14 +129,6 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit,
}
}
public registerOnChange(fn: any) {
this.callChange = fn;
}
public registerOnTouched(fn: any) {
this.callTouched = fn;
}
public insertAssets(assets: AssetDto[]) {
let content = '';

6
src/Squidex/app/shared/state/ui.state.ts

@ -53,7 +53,7 @@ export class UIState extends State<Snapshot> {
this.uiService.getSettings(this.appName)
.subscribe(dtos => {
return this.next({ settings: dtos });
return this.next(s => ({ ...s, settings: dtos }));
});
}
@ -65,7 +65,7 @@ export class UIState extends State<Snapshot> {
current[key] = value;
this.next({ settings: root });
this.next(s => ({ ...s, settings: root }));
}
}
@ -77,7 +77,7 @@ export class UIState extends State<Snapshot> {
delete current[key];
this.next({ settings: root });
this.next(s => ({ ...s, settings: root }));
}
}

Loading…
Cancel
Save