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="header list">
<div class="row no-gutters"> <div class="row no-gutters">
<div class="col"> <div class="col">
@ -8,10 +8,10 @@
</div> </div>
<div class="col-auto pl-1"> <div class="col-auto pl-1">
<div class="btn-group"> <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> <i class="icon-list"></i>
</button> </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> <i class="icon-grid"></i>
</button> </button>
</div> </div>
@ -22,10 +22,10 @@
<div class="body"> <div class="body">
<ng-container *ngIf="!isListView; else listTemplate"> <ng-container *ngIf="!isListView; else listTemplate">
<div class="row no-gutters"> <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)"> (failed)="removeLoadingAsset(file)" (loaded)="addAsset(file, $event)">
</sqx-asset> </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)"> (updated)="notifyOthers($event)" (removing)="removeLoadedAsset($event)">
</sqx-asset> </sqx-asset>
</div> </div>
@ -33,14 +33,14 @@
<ng-template #listTemplate> <ng-template #listTemplate>
<div class="list-view"> <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)"> [isListView]="true" (failed)="removeLoadingAsset(file)" (loaded)="addAsset(file, $event)">
</sqx-asset> </sqx-asset>
<div <div
[sqxSortModel]="oldAssets.values" [sqxSortModel]="snapshot.oldAssets.values"
(sqxSorted)="sortAssets($event)"> (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" <sqx-asset [asset]="asset" removeMode="true" [isListView]="true"
(updated)="notifyOthers($event)" (removing)="removeLoadedAsset($event)"> (updated)="notifyOthers($event)" (removing)="removeLoadedAsset($event)">
</sqx-asset> </sqx-asset>

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

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

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

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

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

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

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

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

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

@ -5,13 +5,13 @@
autocorrect="off" autocorrect="off"
autocapitalize="off"> autocapitalize="off">
<div *ngIf="suggestedItems.length > 0" [sqxModalTarget]="input" class="control-dropdown" #container position="bottomLeft"> <div *ngIf="snapshot.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" <div *ngFor="let item of snapshot.suggestedItems; let i = index;" class="control-dropdown-item control-dropdown-item-selectable"
[class.active]="i === suggestedIndex" [class.active]="i === snapshot.suggestedIndex"
[container]="container" [container]="container"
(mousedown)="selectItem(item)" (mousedown)="selectItem(item)"
(mouseover)="selectIndex(i)" (mouseover)="selectIndex(i)"
[sqxScrollActive]="i === suggestedIndex"> [sqxScrollActive]="i === snapshot.suggestedIndex">
<ng-container *ngIf="!itemTemplate">{{item}}</ng-container> <ng-container *ngIf="!itemTemplate">{{item}}</ng-container>
<ng-template *ngIf="itemTemplate" [sqxTemplateWrapper]="itemTemplate" [item]="item" [index]="i"></ng-template> <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. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, Component, ContentChild, ElementRef, forwardRef, Input, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, ElementRef, forwardRef, Input, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; import { FormControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Observable, of, Subscription } from 'rxjs'; import { Observable, of } from 'rxjs';
import { catchError, debounceTime, distinctUntilChanged, filter, map, switchMap, tap } from 'rxjs/operators'; import { catchError, debounceTime, distinctUntilChanged, filter, map, switchMap, tap } from 'rxjs/operators';
import { StatefulControlComponent } from '@app/shared';
export interface AutocompleteSource { export interface AutocompleteSource {
find(query: string): Observable<any[]>; 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 provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => AutocompleteComponent), multi: true
}; };
interface State {
suggestedItems: any[];
suggestedIndex: number;
}
@Component({ @Component({
selector: 'sqx-autocomplete', selector: 'sqx-autocomplete',
styleUrls: ['./autocomplete.component.scss'], styleUrls: ['./autocomplete.component.scss'],
@ -30,11 +37,7 @@ export const SQX_AUTOCOMPLETE_CONTROL_VALUE_ACCESSOR: any = {
providers: [SQX_AUTOCOMPLETE_CONTROL_VALUE_ACCESSOR], providers: [SQX_AUTOCOMPLETE_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, OnInit { export class AutocompleteComponent extends StatefulControlComponent<State, any[]> implements OnInit {
private subscription: Subscription;
private callChange = (v: any) => { /* NOOP */ };
private callTouched = () => { /* NOOP */ };
@Input() @Input()
public source: AutocompleteSource; public source: AutocompleteSource;
@ -53,17 +56,17 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O
@ViewChild('input') @ViewChild('input')
public inputControl: ElementRef<HTMLInputElement>; public inputControl: ElementRef<HTMLInputElement>;
public suggestedItems: any[] = [];
public suggestedIndex = -1;
public queryInput = new FormControl(); public queryInput = new FormControl();
public ngOnDestroy() { constructor(changeDetector: ChangeDetectorRef) {
this.subscription.unsubscribe(); super(changeDetector, {
suggestedItems: [],
suggestedIndex: -1
});
} }
public ngOnInit() { public ngOnInit() {
this.subscription = this.observe(
this.queryInput.valueChanges.pipe( this.queryInput.valueChanges.pipe(
tap(query => { tap(query => {
this.callChange(query); this.callChange(query);
@ -80,9 +83,12 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O
filter(query => !!query && !!this.source), filter(query => !!query && !!this.source),
switchMap(query => this.source.find(query)), catchError(() => of([]))) switchMap(query => this.source.find(query)), catchError(() => of([])))
.subscribe(items => { .subscribe(items => {
this.suggestedIndex = -1; this.next(s => ({
this.suggestedItems = items || []; ...s,
}); suggestedIndex: -1,
suggestedItems: items || []
}));
}));
} }
public onKeyDown(event: KeyboardEvent) { public onKeyDown(event: KeyboardEvent) {
@ -98,7 +104,7 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O
this.reset(); this.reset();
return false; return false;
case KEY_ENTER: case KEY_ENTER:
if (this.suggestedItems.length > 0 && this.selectItem()) { if (this.snapshot.suggestedItems.length > 0 && this.selectItem()) {
return false; return false;
} }
break; break;
@ -149,11 +155,11 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O
public selectItem(selection: any | null = null): boolean { public selectItem(selection: any | null = null): boolean {
if (!selection) { if (!selection) {
selection = this.suggestedItems[this.suggestedIndex]; selection = this.snapshot.suggestedItems[this.snapshot.suggestedIndex];
} }
if (!selection && this.suggestedItems.length === 1) { if (!selection && this.snapshot.suggestedItems.length === 1) {
selection = this.suggestedItems[0]; selection = this.snapshot.suggestedItems[0];
} }
if (selection) { if (selection) {
@ -174,24 +180,24 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O
return false; return false;
} }
public selectIndex(selection: number) { public selectIndex(suggestedIndex: number) {
if (selection < 0) { if (suggestedIndex < 0) {
selection = 0; suggestedIndex = 0;
} }
if (selection >= this.suggestedItems.length) { if (suggestedIndex >= this.snapshot.suggestedItems.length) {
selection = this.suggestedItems.length - 1; suggestedIndex = this.snapshot.suggestedItems.length - 1;
} }
this.suggestedIndex = selection; this.next(s => ({ ...s, suggestedIndex }));
} }
private up() { private up() {
this.selectIndex(this.suggestedIndex - 1); this.selectIndex(this.snapshot.suggestedIndex - 1);
} }
private down() { private down() {
this.selectIndex(this.suggestedIndex + 1); this.selectIndex(this.snapshot.suggestedIndex + 1);
} }
private resetForm() { private resetForm() {
@ -199,7 +205,10 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O
} }
private reset() { private reset() {
this.suggestedItems = []; this.next(s => ({
this.suggestedIndex = -1; ...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) { public writeValue(obj: any) {
const checkedValues = Types.isArrayOfString(obj) ? obj.filter(x => this.values.indexOf(x) >= 0) : []; 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) { public check(isChecked: boolean, value: string) {
@ -56,7 +56,7 @@ export class CheckboxGroupComponent extends StatefulControlComponent<State, stri
checkedValues = checkedValues.filter(x => x !== value); checkedValues = checkedValues.filter(x => x !== value);
} }
this.next({ checkedValues }); this.next(s => ({ ...s, checkedValues }));
this.callChange(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 { 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'; import { StatefulControlComponent, Types } from '@app/framework/internal';
@ -28,7 +28,7 @@ interface State {
providers: [SQX_STARS_CONTROL_VALUE_ACCESSOR], providers: [SQX_STARS_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class StarsComponent extends StatefulControlComponent<State, number | null> implements ControlValueAccessor { export class StarsComponent extends StatefulControlComponent<State, number | null> {
private maximumStarsValue = 5; private maximumStarsValue = 5;
@Input() @Input()
@ -38,13 +38,13 @@ export class StarsComponent extends StatefulControlComponent<State, number | nul
if (this.maximumStarsValue !== maxStars) { if (this.maximumStarsValue !== maxStars) {
this.maximumStarsValue = value; 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); 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) { public writeValue(obj: any) {
const value = Types.isNumber(obj) ? obj : 0; const value = Types.isNumber(obj) ? obj : 0;
this.next({ stars: value, value }); this.next(s => ({ ...s, stars: value, value }));
} }
public setPreview(stars: number) { public setPreview(stars: number) {
@ -71,7 +71,7 @@ export class StarsComponent extends StatefulControlComponent<State, number | nul
return; return;
} }
this.next({ stars }); this.next(s => ({ ...s, stars }));
} }
public stopPreview() { public stopPreview() {
@ -79,7 +79,7 @@ export class StarsComponent extends StatefulControlComponent<State, number | nul
return; return;
} }
this.next(s => { s.stars = s.value || 0; }); this.next(s => ({ ...s, stars: s.value || 0 }));
} }
public reset() { public reset() {
@ -88,7 +88,7 @@ export class StarsComponent extends StatefulControlComponent<State, number | nul
} }
if (this.snapshot.value) { if (this.snapshot.value) {
this.next({ stars: -1, value: null }); this.next(s => ({ ...s, stars: -1, value: null }));
this.callChange(null); this.callChange(null);
this.callTouched(); this.callTouched();
@ -103,7 +103,7 @@ export class StarsComponent extends StatefulControlComponent<State, number | nul
} }
if (this.snapshot.value !== value) { if (this.snapshot.value !== value) {
this.next({ stars: value, value }); this.next(s => ({ ...s, stars: value, value }));
this.callChange(value); this.callChange(value);
this.callTouched(); this.callTouched();

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

@ -1,9 +1,9 @@
<ng-container> <ng-container>
<div class="form-control {{class}}" #form (click)="input.focus()" <div class="form-control {{class}}" #form (click)="input.focus()"
[class.single-line]="singleLine" [class.single-line]="singleLine"
[class.focus]="hasFocus" [class.focus]="snapshot.hasFocus"
[class.disabled]="addInput.disabled"> [class.disabled]snapshot.="addInput.disabled">
<span class="item" *ngFor="let item of items; let i = index" [class.disabled]="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> {{item}} <i class="icon-close" (click)="remove(i)"></i>
</span> </span>
@ -23,13 +23,13 @@
spellcheck="false"> spellcheck="false">
</div> </div>
<div *ngIf="suggestedItems.length > 0" [sqxModalTarget]="form" class="control-dropdown" #container position="bottomLeft"> <div *ngIf="snapshot.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" <div *ngFor="let item of snapshot.suggestedItems; let i = index;" class="control-dropdown-item control-dropdown-item-selectable"
[class.active]="i === suggestedIndex" [class.active]="i === snapshot.suggestedIndex"
[container]="container" [container]="container"
(mousedown)="selectValue(item)" (mousedown)="selectValue(item)"
(mouseover)="selectIndex(i)" (mouseover)="selectIndex(i)"
[sqxScrollActive]="i === suggestedIndex"> [sqxScrollActive]="i === snapshot.suggestedIndex">
<ng-container>{{item}}</ng-container> <ng-container>{{item}}</ng-container>
</div> </div>
</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 { 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 { distinctUntilChanged, map, tap } from 'rxjs/operators';
import { StatefulControlComponent, Types } from '@app/framework/internal'; import { StatefulControlComponent, Types } from '@app/framework/internal';
@ -90,7 +90,7 @@ interface State {
providers: [SQX_TAG_EDITOR_CONTROL_VALUE_ACCESSOR], providers: [SQX_TAG_EDITOR_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush 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() @Input()
public converter: Converter = new StringConverter(); public converter: Converter = new StringConverter();
@ -167,8 +167,11 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i
} }
})) }))
.subscribe(items => { .subscribe(items => {
this.suggestedIndex = -1; this.next(s => ({
this.suggestedItems = items || []; ...s,
suggestedIndex: -1,
suggestedItems: items || []
}));
})); }));
} }
@ -177,12 +180,10 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i
this.resetSize(); this.resetSize();
if (this.converter && Types.isArrayOf(obj, v => this.converter.isValidValue(v))) { if (this.converter && Types.isArrayOf(obj, v => this.converter.isValidValue(v))) {
this.items = obj; this.next(s => ({ ...s, items: obj }));
} else { } else {
this.items = []; this.next(s => ({ ...s, items: [] }));
} }
this.changeDetector.markForCheck();
} }
public setDisabledState(isDisabled: boolean): void { public setDisabledState(isDisabled: boolean): void {
@ -197,7 +198,7 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i
public focus() { public focus() {
if (this.addInput.enabled) { 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) { 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() { public resetSize() {
@ -265,7 +266,7 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i
const value = <string>this.addInput.value; const value = <string>this.addInput.value;
if (!value || value.length === 0) { 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; return false;
} }
@ -276,8 +277,8 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i
this.down(); this.down();
return false; return false;
} else if (key === KEY_ENTER) { } else if (key === KEY_ENTER) {
if (this.suggestedIndex >= 0) { if (this.snapshot.suggestedIndex >= 0) {
if (this.selectValue(this.suggestedItems[this.suggestedIndex])) { if (this.selectValue(this.snapshot.suggestedItems[this.snapshot.suggestedIndex])) {
return false; return false;
} }
} else if (this.acceptEnter) { } else if (this.acceptEnter) {
@ -298,8 +299,8 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i
if (value && this.converter.isValidInput(value)) { if (value && this.converter.isValidInput(value)) {
const converted = this.converter.convert(value); const converted = this.converter.convert(value);
if (this.allowDuplicates || this.items.indexOf(converted) < 0) { if (this.allowDuplicates || this.snapshot.items.indexOf(converted) < 0) {
this.updateItems([...this.items, converted]); this.updateItems([...this.snapshot.items, converted]);
} }
this.resetForm(); this.resetForm();
@ -309,24 +310,27 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i
} }
private resetAutocompletion() { private resetAutocompletion() {
this.suggestedItems = []; this.next(s => ({
this.suggestedIndex = -1; ...s,
suggestedItems: [],
suggestedIndex: -1
}));
} }
public selectIndex(selection: number) { public selectIndex(suggestedIndex: number) {
if (selection < 0) { if (suggestedIndex < 0) {
selection = 0; suggestedIndex = 0;
} }
if (selection >= this.suggestedItems.length) { if (suggestedIndex >= this.snapshot.suggestedItems.length) {
selection = this.suggestedItems.length - 1; suggestedIndex = this.snapshot.suggestedItems.length - 1;
} }
this.suggestedIndex = selection; this.next(s => ({ ...s, suggestedIndex }));
} }
public resetFocus(): any { public resetFocus(): any {
this.hasFocus = false; this.next(s => ({ ...s, hasFocus: false }));
} }
private resetForm() { private resetForm() {
@ -334,11 +338,11 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i
} }
private up() { private up() {
this.selectIndex(this.suggestedIndex - 1); this.selectIndex(this.snapshot.suggestedIndex - 1);
} }
private down() { private down() {
this.selectIndex(this.suggestedIndex + 1); this.selectIndex(this.snapshot.suggestedIndex + 1);
} }
public onCut(event: ClipboardEvent) { public onCut(event: ClipboardEvent) {
@ -351,7 +355,7 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i
public onCopy(event: ClipboardEvent) { public onCopy(event: ClipboardEvent) {
if (!this.hasSelection()) { 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(); event.preventDefault();
} }
@ -363,7 +367,7 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i
if (value) { if (value) {
this.resetForm(); this.resetForm();
const values = [...this.items]; const values = [...this.snapshot.items];
for (let part of value.split(',')) { for (let part of value.split(',')) {
const converted = this.converter.convert(part); const converted = this.converter.convert(part);
@ -387,12 +391,12 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i
} }
private updateItems(items: any[]) { private updateItems(items: any[]) {
const items = items; this.next(s => ({ ...s, items }));
if (items.length === 0 && this.undefinedWhenEmpty) { if (items.length === 0 && this.undefinedWhenEmpty) {
this.callChange(undefined); this.callChange(undefined);
} else { } else {
this.callChange(this.items); this.callChange(items);
} }
this.resetSize(); 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 { 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'; import { Types } from '@app/framework/internal';
@ -26,7 +26,7 @@ interface State {
templateUrl: './toggle.component.html', templateUrl: './toggle.component.html',
providers: [SQX_TOGGLE_CONTROL_VALUE_ACCESSOR] 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() @Input()
public threeStates = false; public threeStates = false;
@ -37,7 +37,9 @@ export class ToggleComponent extends StatefulControlComponent<State, boolean | n
} }
public writeValue(obj: any) { 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) { public changeState(event: MouseEvent) {
@ -59,7 +61,7 @@ export class ToggleComponent extends StatefulControlComponent<State, boolean | n
isChecked = !(isChecked === true); isChecked = !(isChecked === true);
} }
this.next({ isChecked }); this.next(s => ({ ...s, isChecked }));
this.callChange(isChecked); this.callChange(isChecked);
this.callTouched(); 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.observe(
this.dialogs.notifications.subscribe(notification => { this.dialogs.notifications.subscribe(notification => {
this.next(state => { this.next(s => ({
state.notifications = [...state.notifications, notification]; ...s,
}); notifications: [...s.notifications, notification]
}));
if (notification.displayTime > 0) { if (notification.displayTime > 0) {
this.observe(timer(notification.displayTime).subscribe(() => { this.observe(timer(notification.displayTime).subscribe(() => {
@ -68,12 +69,10 @@ export class DialogRendererComponent extends StatefulComponent<State> implements
this.observe( this.observe(
this.dialogs.dialogs this.dialogs.dialogs
.subscribe(request => { .subscribe(dialogRequest => {
this.cancel(); this.cancel();
this.next(state => { this.next(s => ({ ...s, dialogRequest }));
state.dialogRequest = request;
});
})); }));
} }
@ -90,17 +89,16 @@ export class DialogRendererComponent extends StatefulComponent<State> implements
} }
private finishRequest(value: boolean) { private finishRequest(value: boolean) {
this.next(state => { this.next(s => {
if (state.dialogRequest) { if (s.dialogRequest) {
state.dialogRequest.complete(value); s.dialogRequest.complete(value);
state.dialogRequest = null;
} }
return { ...s, dialogRequest: null };
}); });
} }
public close(notification: Notification) { public close(notification: Notification) {
this.next(state => { this.next(s => ({ ...s, notifications: s.notifications.filter(n => notification !== n) }));
state.notifications = state.notifications.filter(n => notification !== n);
});
} }
} }

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

@ -12,7 +12,7 @@ import {
fadeAnimation, fadeAnimation,
ModalModel, ModalModel,
OnboardingService, OnboardingService,
PureComponent, StatefulComponent,
Types Types
} from '@app/framework/internal'; } from '@app/framework/internal';
@ -25,7 +25,7 @@ import {
], ],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class OnboardingTooltipComponent extends PureComponent implements OnDestroy, OnInit { export class OnboardingTooltipComponent extends StatefulComponent implements OnDestroy, OnInit {
public tooltipModal = new ModalModel(); public tooltipModal = new ModalModel();
@Input() @Input()
@ -44,7 +44,7 @@ export class OnboardingTooltipComponent extends PureComponent implements OnDestr
private readonly onboardingService: OnboardingService, private readonly onboardingService: OnboardingService,
private readonly renderer: Renderer2 private readonly renderer: Renderer2
) { ) {
super(changeDetector); super(changeDetector, {});
} }
public ngOnDestroy() { 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. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ViewChild, ViewContainerRef } from '@angular/core'; import { ChangeDetectionStrategy, Component, ViewChild, ViewContainerRef } from '@angular/core';
import { PureComponent } from '@app/framework/internal';
@Component({ @Component({
selector: 'sqx-root-view', selector: 'sqx-root-view',
@ -15,11 +13,7 @@ import { PureComponent } from '@app/framework/internal';
templateUrl: './root-view.component.html', templateUrl: './root-view.component.html',
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class RootViewComponent extends PureComponent { export class RootViewComponent {
@ViewChild('element', { read: ViewContainerRef }) @ViewChild('element', { read: ViewContainerRef })
public viewContainer: 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 { import {
fadeAnimation, fadeAnimation,
ModalModel, ModalModel,
PureComponent StatefulComponent
} from '@app/framework/internal'; } from '@app/framework/internal';
@Component({ @Component({
@ -22,7 +22,7 @@ import {
], ],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class TooltipComponent extends PureComponent implements OnDestroy, OnInit { export class TooltipComponent extends StatefulComponent implements OnDestroy, OnInit {
@Input() @Input()
public target: any; public target: any;
@ -34,7 +34,7 @@ export class TooltipComponent extends PureComponent implements OnDestroy, OnInit
constructor(changeDetector: ChangeDetectorRef, constructor(changeDetector: ChangeDetectorRef,
private readonly renderer: Renderer2 private readonly renderer: Renderer2
) { ) {
super(changeDetector); super(changeDetector, {});
} }
public ngOnInit() { public ngOnInit() {

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

@ -5,9 +5,9 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * 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({ @Component({
selector: 'sqx-pager', selector: 'sqx-pager',
@ -15,7 +15,7 @@ import { Pager, PureComponent } from '@app/framework/internal';
templateUrl: './pager.component.html', templateUrl: './pager.component.html',
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class PagerComponent extends PureComponent { export class PagerComponent {
@Output() @Output()
public nextPage = new EventEmitter(); public nextPage = new EventEmitter();
@ -27,8 +27,4 @@ export class PagerComponent extends PureComponent {
@Input() @Input()
public hideWhenButtonsDisabled = false; 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. * 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'; import { PanelContainerDirective } from './panel-container.directive';
@ -20,7 +20,7 @@ import { PanelContainerDirective } from './panel-container.directive';
], ],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class PanelComponent extends PureComponent implements AfterViewInit, OnDestroy, OnInit { export class PanelComponent implements AfterViewInit, OnDestroy, OnInit {
private styleWidth: string; private styleWidth: string;
public renderWidth = 0; public renderWidth = 0;
@ -61,11 +61,10 @@ export class PanelComponent extends PureComponent implements AfterViewInit, OnDe
@ViewChild('panel') @ViewChild('panel')
public panel: ElementRef<HTMLElement>; public panel: ElementRef<HTMLElement>;
constructor(changeDetector: ChangeDetectorRef, constructor(
private readonly container: PanelContainerDirective, private readonly container: PanelContainerDirective,
private readonly renderer: Renderer2 private readonly renderer: Renderer2
) { ) {
super(changeDetector);
} }
public ngOnDestroy() { 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 { 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({ @Component({
selector: 'sqx-shortcut', selector: 'sqx-shortcut',
template: '' template: ''
}) })
export class ShortcutComponent extends PureComponent implements OnDestroy, OnInit { export class ShortcutComponent extends StatefulComponent implements OnDestroy, OnInit {
private lastKeys: string; private lastKeys: string;
@Input() @Input()
@ -30,7 +30,7 @@ export class ShortcutComponent extends PureComponent implements OnDestroy, OnIni
private readonly shortcutService: ShortcutService, private readonly shortcutService: ShortcutService,
private readonly zone: NgZone private readonly zone: NgZone
) { ) {
super(changeDetector); super(changeDetector, {});
changeDetector.detach(); changeDetector.detach();
} }

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

@ -15,7 +15,7 @@ import { State } from '../state';
declare type UnsubscribeFunction = () => void; 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)[] = []; private subscriptions: (Subscription | UnsubscribeFunction)[] = [];
constructor( constructor(
@ -52,11 +52,7 @@ export abstract class StatefulComponent<T> extends State<T> implements OnDestroy
} }
} }
export interface FormControlState { export abstract class StatefulControlComponent<T, TValue> extends StatefulComponent<T & { isDisabled: boolean }> implements ControlValueAccessor {
isDisabled: boolean;
}
export abstract class StatefulControlComponent<T, TValue> extends StatefulComponent<T & FormControlState> implements ControlValueAccessor {
private fnChanged = (v: any) => { /* NOOP */ }; private fnChanged = (v: any) => { /* NOOP */ };
private fnTouched = () => { /* NOOP */ }; private fnTouched = () => { /* NOOP */ };
@ -81,24 +77,18 @@ export abstract class StatefulControlComponent<T, TValue> extends StatefulCompon
} }
public setDisabledState(isDisabled: boolean): void { public setDisabledState(isDisabled: boolean): void {
this.next(state => { state.isDisabled = isDisabled; }); this.next(s => ({ ...s, isDisabled }));
} }
public abstract writeValue(obj: any): void; public abstract writeValue(obj: any): void;
} }
export abstract class PureComponent extends StatefulComponent<any> { export abstract class ExternalControlComponent<TValue> extends StatefulComponent<any> implements ControlValueAccessor {
constructor(changeDetector: ChangeDetectorRef) {
super(changeDetector, {});
}
}
export abstract class ExternalControlComponent<TValue> extends PureComponent implements ControlValueAccessor {
private fnChanged = (v: any) => { /* NOOP */ }; private fnChanged = (v: any) => { /* NOOP */ };
private fnTouched = () => { /* NOOP */ }; private fnTouched = () => { /* NOOP */ };
constructor(changeDetector: ChangeDetectorRef) { constructor(changeDetector: ChangeDetectorRef) {
super(changeDetector); super(changeDetector, {});
changeDetector.detach(); 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 { timer } from 'rxjs';
import { import {
PureComponent,
ResourceLoaderService, ResourceLoaderService,
StatefulComponent,
UserReportConfig UserReportConfig
} from '@app/framework/internal'; } from '@app/framework/internal';
@ -18,12 +18,12 @@ import {
selector: 'sqx-user-report', selector: 'sqx-user-report',
template: '' template: ''
}) })
export class UserReportComponent extends PureComponent implements OnDestroy, OnInit { export class UserReportComponent extends StatefulComponent<any> implements OnDestroy, OnInit {
constructor(changeDetector: ChangeDetectorRef, constructor(changeDetector: ChangeDetectorRef,
private readonly config: UserReportConfig, private readonly config: UserReportConfig,
private readonly resourceLoader: ResourceLoaderService private readonly resourceLoader: ResourceLoaderService
) { ) {
super(changeDetector); super(changeDetector, {});
changeDetector.detach(); changeDetector.detach();
} }

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

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

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

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

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

@ -1,5 +1,5 @@
<div #container class="drop-container"> <div #container class="drop-container">
<div #inner [class.fullscreen]="isFullscreen"> <div #inner [class.fullscreen]="snapshot.isFullscreen">
<textarea class="form-control" #editor></textarea> <textarea class="form-control" #editor></textarea>
</div> </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 { 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 { import {
AssetDto, AssetDto,
DialogModel, DialogModel,
ResourceLoaderService, ResourceLoaderService,
StatefulControlComponent,
Types Types
} from '@app/shared/internal'; } 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 provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => MarkdownEditorComponent), multi: true
}; };
interface State {
isFullscreen: false;
}
@Component({ @Component({
selector: 'sqx-markdown-editor', selector: 'sqx-markdown-editor',
styleUrls: ['./markdown-editor.component.scss'], 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], providers: [SQX_MARKDOWN_EDITOR_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewInit { export class MarkdownEditorComponent extends StatefulControlComponent<State, string> implements AfterViewInit {
private callChange = (v: any) => { /* NOOP */ };
private callTouched = () => { /* NOOP */ };
private simplemde: any; private simplemde: any;
private value: string; private value: string;
private isDisabled = false; private isDisabled = false;
@ -46,13 +49,14 @@ export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewI
@ViewChild('inner') @ViewChild('inner')
public inner: ElementRef; public inner: ElementRef;
public isFullscreen = false; constructor(changeDetector: ChangeDetectorRef,
constructor(
private readonly changeDetector: ChangeDetectorRef,
private readonly renderer: Renderer2, private readonly renderer: Renderer2,
private readonly resourceLoader: ResourceLoaderService private readonly resourceLoader: ResourceLoaderService
) { ) {
super(changeDetector, {
isFullscreen: false
});
this.resourceLoader.loadStyle('https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css'); 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 = () => { private showSelector = () => {
this.assetsDialog.show(); this.assetsDialog.show();
this.changeDetector.detectChanges();
} }
public ngAfterViewInit() { public ngAfterViewInit() {
@ -182,17 +176,17 @@ export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewI
}); });
this.simplemde.codemirror.on('refresh', () => { this.simplemde.codemirror.on('refresh', () => {
this.isFullscreen = this.simplemde.isFullscreenActive(); const isFullscreen = this.simplemde.isFullscreenActive();
let target = this.container.nativeElement; let target = this.container.nativeElement;
if (this.isFullscreen) { if (isFullscreen) {
target = document.body; target = document.body;
} }
this.renderer.appendChild(target, this.inner.nativeElement); this.renderer.appendChild(target, this.inner.nativeElement);
this.changeDetector.detectChanges(); this.next(s => ({ ...s, isFullscreen }));
}); });
this.simplemde.codemirror.on('blur', () => { 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 { 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 { import {
AssetDto, AssetDto,
DialogModel, DialogModel,
ExternalControlComponent,
ResourceLoaderService, ResourceLoaderService,
Types Types
} from '@app/shared/internal'; } from '@app/shared/internal';
@ -28,9 +29,7 @@ export const SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
providers: [SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR], providers: [SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class RichEditorComponent implements ControlValueAccessor, AfterViewInit, OnDestroy { export class RichEditorComponent extends ExternalControlComponent<string> implements AfterViewInit, OnDestroy {
private callChange = (v: any) => { /* NOOP */ };
private callTouched = () => { /* NOOP */ };
private tinyEditor: any; private tinyEditor: any;
private tinyInitTimer: any; private tinyInitTimer: any;
private value: string; private value: string;
@ -44,10 +43,10 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit,
@Output() @Output()
public assetPluginClicked = new EventEmitter<any>(); public assetPluginClicked = new EventEmitter<any>();
constructor( constructor(changeDetector: ChangeDetectorRef,
private readonly changeDetector: ChangeDetectorRef,
private readonly resourceLoader: ResourceLoaderService private readonly resourceLoader: ResourceLoaderService
) { ) {
super(changeDetector);
} }
public ngOnDestroy() { public ngOnDestroy() {
@ -66,8 +65,6 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit,
private showSelector = () => { private showSelector = () => {
this.assetsDialog.show(); this.assetsDialog.show();
this.changeDetector.detectChanges();
} }
private getEditorOptions() { 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[]) { public insertAssets(assets: AssetDto[]) {
let content = ''; 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) this.uiService.getSettings(this.appName)
.subscribe(dtos => { .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; 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]; delete current[key];
this.next({ settings: root }); this.next(s => ({ ...s, settings: root }));
} }
} }

Loading…
Cancel
Save