From 182fb383166a5d96e050304d786745e3ff3cfa34 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 3 Dec 2019 22:04:47 +0100 Subject: [PATCH] Tag editor for references. (#458) * Tag editor for references. * Checkboxes for tag editor. --- .../Schemas/ReferencesFieldEditor.cs | 3 +- .../shared/field-editor.component.html | 7 + .../types/array-validation.component.ts | 4 +- .../schema/types/references-ui.component.html | 7 + .../references-validation.component.html | 2 +- .../types/references-validation.component.ts | 4 +- .../pages/workflows/workflow.component.html | 6 +- .../pages/workflows/workflow.component.scss | 2 +- .../pages/workflows/workflow.component.ts | 4 +- .../workflows/workflows-page.component.ts | 4 +- .../angular/forms/dropdown.component.html | 58 +++--- .../angular/forms/dropdown.component.scss | 2 +- .../angular/forms/dropdown.component.ts | 20 +- .../angular/forms/tag-editor.component.html | 95 ++++++---- .../angular/forms/tag-editor.component.scss | 91 ++++++--- .../angular/forms/tag-editor.component.ts | 132 ++++++++----- .../references-dropdown.component.ts | 4 - .../components/references-tags.component.ts | 173 ++++++++++++++++++ frontend/app/shared/declarations.ts | 1 + frontend/app/shared/internal.ts | 2 +- frontend/app/shared/module.ts | 7 +- ...-tag-converter.ts => schema-tag-source.ts} | 44 +++-- 22 files changed, 480 insertions(+), 192 deletions(-) create mode 100644 frontend/app/shared/components/references-tags.component.ts rename frontend/app/shared/state/{schema-tag-converter.ts => schema-tag-source.ts} (52%) diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldEditor.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldEditor.cs index 8b6483d01..07400c19a 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldEditor.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldEditor.cs @@ -10,6 +10,7 @@ namespace Squidex.Domain.Apps.Core.Schemas public enum ReferencesFieldEditor { List, - Dropdown + Dropdown, + Tags } } diff --git a/frontend/app/features/content/shared/field-editor.component.html b/frontend/app/features/content/shared/field-editor.component.html index 0970a8773..2c3b729bd 100644 --- a/frontend/app/features/content/shared/field-editor.component.html +++ b/frontend/app/features/content/shared/field-editor.component.html @@ -95,6 +95,13 @@ [schemaId]="field.rawProperties.singleId"> + + + + diff --git a/frontend/app/features/schemas/pages/schema/types/array-validation.component.ts b/frontend/app/features/schemas/pages/schema/types/array-validation.component.ts index 1d175cd12..92f343e10 100644 --- a/frontend/app/features/schemas/pages/schema/types/array-validation.component.ts +++ b/frontend/app/features/schemas/pages/schema/types/array-validation.component.ts @@ -11,7 +11,7 @@ import { FormControl, FormGroup } from '@angular/forms'; import { ArrayFieldPropertiesDto, FieldDto, - SchemaTagConverter + SchemaTagSource } from '@app/shared'; @Component({ @@ -30,7 +30,7 @@ export class ArrayValidationComponent implements OnInit { public properties: ArrayFieldPropertiesDto; constructor( - public readonly schemasSource: SchemaTagConverter + public readonly schemasSource: SchemaTagSource ) { } diff --git a/frontend/app/features/schemas/pages/schema/types/references-ui.component.html b/frontend/app/features/schemas/pages/schema/types/references-ui.component.html index 4f9cc2372..e282432dd 100644 --- a/frontend/app/features/schemas/pages/schema/types/references-ui.component.html +++ b/frontend/app/features/schemas/pages/schema/types/references-ui.component.html @@ -17,6 +17,13 @@ Dropdown + diff --git a/frontend/app/features/schemas/pages/schema/types/references-validation.component.html b/frontend/app/features/schemas/pages/schema/types/references-validation.component.html index 3eed8d835..0068ae4b6 100644 --- a/frontend/app/features/schemas/pages/schema/types/references-validation.component.html +++ b/frontend/app/features/schemas/pages/schema/types/references-validation.component.html @@ -4,7 +4,7 @@
+ [converter]="schemasSource.converter | async" [suggestedValues]="(schemasSource.converter | async)?.suggestions">
diff --git a/frontend/app/features/schemas/pages/schema/types/references-validation.component.ts b/frontend/app/features/schemas/pages/schema/types/references-validation.component.ts index 9ae393e4a..bafbaf5bb 100644 --- a/frontend/app/features/schemas/pages/schema/types/references-validation.component.ts +++ b/frontend/app/features/schemas/pages/schema/types/references-validation.component.ts @@ -11,7 +11,7 @@ import { FormControl, FormGroup } from '@angular/forms'; import { FieldDto, ReferencesFieldPropertiesDto, - SchemaTagConverter + SchemaTagSource } from '@app/shared'; @Component({ @@ -30,7 +30,7 @@ export class ReferencesValidationComponent implements OnInit { public properties: ReferencesFieldPropertiesDto; constructor( - public readonly schemasSource: SchemaTagConverter + public readonly schemasSource: SchemaTagSource ) { } diff --git a/frontend/app/features/settings/pages/workflows/workflow.component.html b/frontend/app/features/settings/pages/workflows/workflow.component.html index fab98df20..d30ab9643 100644 --- a/frontend/app/features/settings/pages/workflows/workflow.component.html +++ b/frontend/app/features/settings/pages/workflows/workflow.component.html @@ -5,7 +5,7 @@ {{workflow.displayName}}
- + [suggestedValues]="(schemasSource.converter | async)?.suggestions"> diff --git a/frontend/app/features/settings/pages/workflows/workflow.component.scss b/frontend/app/features/settings/pages/workflows/workflow.component.scss index bccddffa8..94b651f0f 100644 --- a/frontend/app/features/settings/pages/workflows/workflow.component.scss +++ b/frontend/app/features/settings/pages/workflows/workflow.component.scss @@ -18,7 +18,7 @@ } .col-tags { - padding: .6rem 1rem 0; + padding: .375rem 1rem 0; } .form-group { diff --git a/frontend/app/features/settings/pages/workflows/workflow.component.ts b/frontend/app/features/settings/pages/workflows/workflow.component.ts index 866376e87..3a89fd476 100644 --- a/frontend/app/features/settings/pages/workflows/workflow.component.ts +++ b/frontend/app/features/settings/pages/workflows/workflow.component.ts @@ -12,7 +12,7 @@ import { Component, Input, OnChanges } from '@angular/core'; import { ErrorDto, MathHelper, - SchemaTagConverter, + SchemaTagSource, WorkflowDto, WorkflowsState, WorkflowStep, @@ -36,7 +36,7 @@ export class WorkflowComponent implements OnChanges { public roles: ReadonlyArray; @Input() - public schemasSource: SchemaTagConverter; + public schemasSource: SchemaTagSource; public error: string | null; diff --git a/frontend/app/features/settings/pages/workflows/workflows-page.component.ts b/frontend/app/features/settings/pages/workflows/workflows-page.component.ts index b367be908..1dc3793d3 100644 --- a/frontend/app/features/settings/pages/workflows/workflows-page.component.ts +++ b/frontend/app/features/settings/pages/workflows/workflows-page.component.ts @@ -10,7 +10,7 @@ import { Component, OnInit } from '@angular/core'; import { ResourceOwner, RolesState, - SchemaTagConverter, + SchemaTagSource, WorkflowDto, WorkflowsState } from '@app/shared'; @@ -25,7 +25,7 @@ export class WorkflowsPageComponent extends ResourceOwner implements OnInit { constructor( public readonly rolesState: RolesState, - public readonly schemasSource: SchemaTagConverter, + public readonly schemasSource: SchemaTagSource, public readonly workflowsState: WorkflowsState ) { super(); diff --git a/frontend/app/framework/angular/forms/dropdown.component.html b/frontend/app/framework/angular/forms/dropdown.component.html index 33ee77aef..094c8b88c 100644 --- a/frontend/app/framework/angular/forms/dropdown.component.html +++ b/frontend/app/framework/angular/forms/dropdown.component.html @@ -1,36 +1,34 @@ - -
- +
+ + +
+ {{snapshot.selectedItem}} -
- {{snapshot.selectedItem}} - - -
+ +
- -
+ +
-
- -
-
- -
+
+ +
+
+ +
-
-
- {{item}} - - -
+
+
+ {{item}} + +
- -
- \ No newline at end of file +
+
+
\ No newline at end of file diff --git a/frontend/app/framework/angular/forms/dropdown.component.scss b/frontend/app/framework/angular/forms/dropdown.component.scss index 0a5e383da..5323ce057 100644 --- a/frontend/app/framework/angular/forms/dropdown.component.scss +++ b/frontend/app/framework/angular/forms/dropdown.component.scss @@ -47,7 +47,7 @@ $color-input-disabled: #eef1f4; } .icon-caret-down { - @include absolute(30%, .4rem, auto, auto); + @include absolute(30%, 5px, null, null); font-size: .9rem; font-weight: normal; pointer-events: none; diff --git a/frontend/app/framework/angular/forms/dropdown.component.ts b/frontend/app/framework/angular/forms/dropdown.component.ts index 36b8ffae2..ae0021b82 100644 --- a/frontend/app/framework/angular/forms/dropdown.component.ts +++ b/frontend/app/framework/angular/forms/dropdown.component.ts @@ -130,10 +130,10 @@ export class DropdownComponent extends StatefulControlComponent im public onKeyDown(event: KeyboardEvent) { switch (event.keyCode) { case Keys.UP: - this.up(); + this.selectPrevIndex(); return false; case Keys.DOWN: - this.down(); + this.selectNextIndex(); return false; case Keys.ENTER: this.selectIndexAndClose(this.snapshot.selectedIndex); @@ -172,6 +172,14 @@ export class DropdownComponent extends StatefulControlComponent im this.queryInput.setValue(''); } + public selectPrevIndex() { + this.selectIndex(this.snapshot.selectedIndex - 1, true); + } + + public selectNextIndex() { + this.selectIndex(this.snapshot.selectedIndex + 1, true); + } + public selectIndex(selectedIndex: number, emitEvents: boolean) { if (selectedIndex < 0) { selectedIndex = 0; @@ -195,12 +203,4 @@ export class DropdownComponent extends StatefulControlComponent im } } - - private up() { - this.selectIndex(this.snapshot.selectedIndex - 1, true); - } - - private down() { - this.selectIndex(this.snapshot.selectedIndex + 1, true); - } } \ No newline at end of file diff --git a/frontend/app/framework/angular/forms/tag-editor.component.html b/frontend/app/framework/angular/forms/tag-editor.component.html index 4b9c9024d..b126ee0ca 100644 --- a/frontend/app/framework/angular/forms/tag-editor.component.html +++ b/frontend/app/framework/angular/forms/tag-editor.component.html @@ -1,36 +1,65 @@ -
- - {{item}} - +
+
- -
+ + {{item}} + - -
-
- {{item}} -
+ +
+ +
+
-
\ No newline at end of file + + +
+
+ {{item}} +
+
+
+ + + +
+
+
+
+ + +
+
+
+
+
+
+
\ No newline at end of file diff --git a/frontend/app/framework/angular/forms/tag-editor.component.scss b/frontend/app/framework/angular/forms/tag-editor.component.scss index 57c96273b..8aa3f205b 100644 --- a/frontend/app/framework/angular/forms/tag-editor.component.scss +++ b/frontend/app/framework/angular/forms/tag-editor.component.scss @@ -4,13 +4,26 @@ $focus-color: #b3d3ff; $focus-shadow: rgba(51, 137, 255, .25); +$inner-height: 1.75rem; + :host { text-align: left; } +.form-container { + position: relative; +} + .form-control { & { cursor: text; + padding-bottom: 0; + padding-left: .25rem; + padding-right: 2rem; + padding-top: .25rem; + position: relative; + text-align: left; + text-decoration: none; } &.disabled { @@ -27,7 +40,7 @@ $focus-shadow: rgba(51, 137, 255, .25); box-shadow: 0 0 0 .2rem $focus-shadow; } - &.single-line { + &.singleline { overflow-x: hidden; overflow-y: hidden; white-space: nowrap; @@ -38,12 +51,13 @@ $focus-shadow: rgba(51, 137, 255, .25); } } +.multiline { + height: auto; +} + div { - &.form-control { + &.blank { height: auto; - position: relative; - text-align: left; - text-decoration: none; } } @@ -52,15 +66,12 @@ div { @include placeholder-color($color-input-placeholder); background: transparent; border: 0; - height: auto !important; - max-width: 100%; - min-width: 50px; + border-radius: 0; padding: 0; } &:focus, &.focus { - box-shadow: none; outline: none; } @@ -72,6 +83,25 @@ div { &:hover { background: transparent; } + + &.singleline { + .item { + margin-bottom: 0; + } + + .blank { + margin-bottom: 0; + } + } +} + +.text-input { + height: $inner-height; + margin-bottom: .25rem; + margin-left: .25rem; + max-width: 100%; + min-width: 50px; + padding-left: .25rem; } .gray { @@ -88,35 +118,46 @@ div { .item { & { - @include border-radius(10px); background: $color-theme-blue; border: 0; + border-radius: 2px; color: $color-dark-foreground; cursor: default; - font-size: .8rem; - font-weight: normal; - height: 1.25rem; + display: inline-block; + height: $inner-height; + margin-bottom: .25rem; margin-right: 2px; - padding: 1px .6rem; + padding: 1px .5rem; + vertical-align: top; white-space: nowrap; - } - - &, - &-container { - display: inline-block; - } - - &-container { - height: 24px; - padding: 2px; - padding-left: 0; + width: auto; } &.disabled { pointer-events: none; + + i { + display: none; + } } &:hover { background: $color-theme-blue-dark; } +} + +.btn { + @include absolute(.25rem, 0, null, null); + border: 0; + cursor: pointer; + font-size: .9rem; + font-weight: normal; + padding-left: 5px; + padding-right: 5px; +} + +.suggestions-dropdown { + max-width: 300px; + min-width: 300px; + padding: 1rem; } \ No newline at end of file diff --git a/frontend/app/framework/angular/forms/tag-editor.component.ts b/frontend/app/framework/angular/forms/tag-editor.component.ts index 4fbe7a367..b6bcbe223 100644 --- a/frontend/app/framework/angular/forms/tag-editor.component.ts +++ b/frontend/app/framework/angular/forms/tag-editor.component.ts @@ -7,13 +7,14 @@ // tslint:disable:template-use-track-by-function -import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core'; import { FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; import { distinctUntilChanged, map, tap } from 'rxjs/operators'; import { fadeAnimation, Keys, + ModalModel, StatefulControlComponent, Types } from '@app/framework/internal'; @@ -45,7 +46,7 @@ export interface Converter { export class IntConverter implements Converter { private static ZERO = new TagValue(0, '0', 0); - public convertInput(input: string): TagValue | null { + public convertInput(input: string) { if (input === '0') { return IntConverter.ZERO; } @@ -59,7 +60,7 @@ export class IntConverter implements Converter { return null; } - public convertValue(value: any): TagValue | null { + public convertValue(value: any) { if (Types.isNumber(value)) { return new TagValue(value, `${value}`, value); } @@ -71,7 +72,7 @@ export class IntConverter implements Converter { export class FloatConverter implements Converter { private static ZERO = new TagValue(0, '0', 0); - public convertInput(input: string): TagValue | null { + public convertInput(input: string) { if (input === '0') { return FloatConverter.ZERO; } @@ -85,7 +86,7 @@ export class FloatConverter implements Converter { return null; } - public convertValue(value: any): TagValue | null { + public convertValue(value: any) { if (Types.isNumber(value)) { return new TagValue(value, `${value}`, value); } @@ -95,7 +96,7 @@ export class FloatConverter implements Converter { } export class StringConverter implements Converter { - public convertInput(input: string): TagValue | null { + public convertInput(input: string) { if (input) { const trimmed = input.trim(); @@ -107,7 +108,7 @@ export class StringConverter implements Converter { return null; } - public convertValue(value: any): TagValue | null { + public convertValue(value: any) { if (Types.isString(value)) { const trimmed = value.trim(); @@ -122,8 +123,6 @@ export const SQX_TAG_EDITOR_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => TagEditorComponent), multi: true }; -const CACHED_SIZES: { [key: string]: number } = {}; - let CACHED_FONT: string; interface State { @@ -140,22 +139,21 @@ interface State { styleUrls: ['./tag-editor.component.scss'], templateUrl: './tag-editor.component.html', providers: [SQX_TAG_EDITOR_CONTROL_VALUE_ACCESSOR], - changeDetection: ChangeDetectionStrategy.OnPush, animations: [ fadeAnimation - ] + ], + changeDetection: ChangeDetectionStrategy.OnPush }) // tslint:disable-next-line: readonly-array -export class TagEditorComponent extends StatefulControlComponent implements AfterViewInit, OnInit { +export class TagEditorComponent extends StatefulControlComponent implements AfterViewInit, OnChanges, OnInit { + private latestValue: any; + @ViewChild('form', { static: false }) public formElement: ElementRef; @ViewChild('input', { static: false }) public inputElement: ElementRef; - @Input() - public suggestedValues: ReadonlyArray = []; - @Input() public converter: Converter = new StringConverter(); @@ -186,12 +184,21 @@ export class TagEditorComponent extends StatefulControlComponent i @Input() public inputName = 'tag-editor'; + @Input() + public set suggestedValues(value: ReadonlyArray) { + if (value) { + this.suggestionsSorted = value.sortedByString(x => x.lowerCaseName); + } else { + this.suggestionsSorted = []; + } + } + @Input() public set suggestions(value: ReadonlyArray) { if (value) { - this.suggestedValues = value.map(x => new TagValue(x, x, x)); + this.suggestionsSorted = value.map(x => new TagValue(x, x, x)).sortedByString(x => x.lowerCaseName); } else { - this.suggestedValues = []; + this.suggestionsSorted = []; } } @@ -200,6 +207,9 @@ export class TagEditorComponent extends StatefulControlComponent i this.setDisabledState(value); } + public suggestionsSorted: ReadonlyArray = []; + public suggestionsModal = new ModalModel(); + public addInput = new FormControl(); constructor(changeDetector: ChangeDetectorRef) { @@ -212,13 +222,13 @@ export class TagEditorComponent extends StatefulControlComponent i } public ngAfterViewInit() { - if (!CACHED_FONT) { - const style = window.getComputedStyle(this.inputElement.nativeElement); + this.resetSize(); + } - CACHED_FONT = `${style.getPropertyValue('font-size')} ${style.getPropertyValue('font-family')}`; + public ngOnChanges(changes: SimpleChanges) { + if (changes['converter']) { + this.writeValue(this.latestValue); } - - this.resetSize(); } public ngOnInit() { @@ -236,8 +246,8 @@ export class TagEditorComponent extends StatefulControlComponent i }), distinctUntilChanged(), map(query => { - if (Types.isArray(this.suggestedValues) && query && query.length > 0) { - return this.suggestedValues.filter(s => s.lowerCaseName.indexOf(query) >= 0 && !this.snapshot.items.find(x => x.id === s.id)); + if (Types.isArray(this.suggestionsSorted) && query && query.length > 0) { + return this.suggestionsSorted.filter(s => s.lowerCaseName.indexOf(query) >= 0 && !this.snapshot.items.find(x => x.id === s.id)); } else { return []; } @@ -252,6 +262,8 @@ export class TagEditorComponent extends StatefulControlComponent i } public writeValue(obj: any) { + this.latestValue = obj; + this.resetForm(); this.resetSize(); @@ -304,6 +316,8 @@ export class TagEditorComponent extends StatefulControlComponent i } public resetSize() { + this.calculateStyle(); + if (!CACHED_FONT || !this.inputElement || !this.inputElement.nativeElement) { @@ -321,18 +335,11 @@ export class TagEditorComponent extends StatefulControlComponent i ctx.font = CACHED_FONT; const textValue = this.inputElement.nativeElement.value; - const textKey = `${textValue}§${this.placeholder}§${ctx.font}`; - - let width = CACHED_SIZES[textKey]; - - if (!width) { - const widthText = ctx.measureText(textValue).width; - const widthPlaceholder = ctx.measureText(this.placeholder).width; - width = Math.max(widthText, widthPlaceholder); + const widthText = ctx.measureText(textValue).width; + const widthPlaceholder = ctx.measureText(this.placeholder).width; - CACHED_SIZES[textKey] = width; - } + const width = Math.max(widthText, widthPlaceholder); this.inputElement.nativeElement.style.width = ((width + 5) + 'px'); } @@ -345,6 +352,25 @@ export class TagEditorComponent extends StatefulControlComponent i } } + private calculateStyle() { + if (CACHED_FONT || + !this.inputElement || + !this.inputElement.nativeElement) { + return; + } + + const style = window.getComputedStyle(this.inputElement.nativeElement); + + const fontSize = style.getPropertyValue('font-size'); + const fontFamily = style.getPropertyValue('font-family'); + + if (!fontSize || !fontFamily) { + return; + } + + CACHED_FONT = `${fontSize} ${fontFamily}`; + } + public onKeyDown(event: KeyboardEvent) { const key = event.keyCode; @@ -361,10 +387,10 @@ export class TagEditorComponent extends StatefulControlComponent i return false; } } else if (key === Keys.UP) { - this.up(); + this.selectPrevIndex(); return false; } else if (key === Keys.DOWN) { - this.down(); + this.selectNextIndex(); return false; } else if (key === Keys.ENTER) { if (this.snapshot.suggestedIndex >= 0) { @@ -395,7 +421,7 @@ export class TagEditorComponent extends StatefulControlComponent i } if (tagValue) { - if (this.allowDuplicates || !this.snapshot.items.find(x => x.id === tagValue!.id)) { + if (this.allowDuplicates || !this.isSelected(tagValue)) { this.updateItems([...this.snapshot.items, tagValue]); } @@ -407,12 +433,20 @@ export class TagEditorComponent extends StatefulControlComponent i return false; } - private resetAutocompletion() { - this.next(s => ({ - ...s, - suggestedItems: [], - suggestedIndex: -1 - })); + public toggleValue(isSelected: boolean, tagValue: TagValue) { + if (isSelected) { + this.updateItems([...this.snapshot.items, tagValue]); + } else { + this.updateItems(this.snapshot.items.filter(x => x.id !== tagValue.id)); + } + } + + public selectPrevIndex() { + this.selectIndex(this.snapshot.suggestedIndex - 1); + } + + public selectNextIndex() { + this.selectIndex(this.snapshot.suggestedIndex + 1); } public selectIndex(suggestedIndex: number) { @@ -431,16 +465,16 @@ export class TagEditorComponent extends StatefulControlComponent i this.next(s => ({ ...s, hasFocus: false })); } - private resetForm() { - this.addInput.reset(); + private resetAutocompletion() { + this.next(s => ({ ...s, suggestedItems: [], suggestedIndex: -1 })); } - private up() { - this.selectIndex(this.snapshot.suggestedIndex - 1); + private resetForm() { + this.addInput.reset(); } - private down() { - this.selectIndex(this.snapshot.suggestedIndex + 1); + public isSelected(tagValue: TagValue) { + return this.snapshot.items.find(x => x.id === tagValue.id); } public onCut(event: ClipboardEvent) { diff --git a/frontend/app/shared/components/references-dropdown.component.ts b/frontend/app/shared/components/references-dropdown.component.ts index dd06a151f..eddd86b8f 100644 --- a/frontend/app/shared/components/references-dropdown.component.ts +++ b/frontend/app/shared/components/references-dropdown.component.ts @@ -180,8 +180,4 @@ export class ReferencesDropdownComponent extends StatefulControlComponent ReferencesTagsComponent), multi: true +}; + +const NO_EMIT = { emitEvent: false }; + +class TagsConverter implements Converter { + public suggestions: ReadonlyArray = []; + + constructor(language: LanguageDto, contents: ReadonlyArray) { + this.suggestions = this.createTags(language, contents); + } + + public convertInput(input: string) { + const result = this.suggestions.find(x => x.name === input); + + return result || null; + } + + public convertValue(value: any) { + const result = this.suggestions.find(x => x.id === value); + + return result || null; + } + + private createTags(language: LanguageDto, contents: ReadonlyArray): ReadonlyArray { + if (contents.length === 0) { + return []; + } + + const values = contents.map(content => { + const name = + content.referenceFields + .map(f => getContentValue(content, language, f, false)) + .map(v => v.formatted || 'No value') + .filter(v => !!v) + .join(', '); + + return new TagValue(content.id, name, content.id); + }); + + return values; + } +} + +interface State { + converter: TagsConverter; +} + +@Component({ + selector: 'sqx-references-tags', + template: ` + + `, + styles: [ + '.truncate { min-height: 1.5rem; }' + ], + providers: [SQX_REFERENCES_TAGS_CONTROL_VALUE_ACCESSOR], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ReferencesTagsComponent extends StatefulControlComponent> implements OnChanges { + private itemCount: number; + private contentItems: ReadonlyArray | null = null; + + @Input() + public schemaId: string; + + @Input() + public language: LanguageDto; + + public get isValid() { + return !!this.schemaId && !!this.language; + } + + public selectionControl = new FormControl([]); + + constructor(changeDetector: ChangeDetectorRef, uiOptions: UIOptions, + private readonly appsState: AppsState, + private readonly contentsService: ContentsService + ) { + super(changeDetector, { converter: new TagsConverter(null!, []) }); + + this.itemCount = uiOptions.get('referencesDropdownItemCount'); + + this.own( + this.selectionControl.valueChanges + .subscribe((value: string[]) => { + if (value && value.length > 0) { + this.callTouched(); + this.callChange(value); + } else { + this.callTouched(); + this.callChange(null); + } + })); + } + + public ngOnChanges(changes: SimpleChanges) { + if (changes['schemaId']) { + this.resetState(); + + if (this.isValid) { + this.contentsService.getContents(this.appsState.appName, this.schemaId, this.itemCount, 0) + .subscribe(contents => { + this.contentItems = contents.items; + + this.resetConverterState(); + }, () => { + this.contentItems = null; + + this.resetConverterState(); + }); + } else { + this.contentItems = null; + + this.resetConverterState(); + } + } + } + + public setDisabledState(isDisabled: boolean) { + if (isDisabled) { + this.selectionControl.disable(); + } else if (this.isValid) { + this.selectionControl.enable(); + } + + super.setDisabledState(isDisabled); + } + + public writeValue(obj: ReadonlyArray) { + this.selectionControl.setValue(obj, NO_EMIT); + } + + private resetConverterState() { + let converter: TagsConverter; + + if (this.isValid && this.contentItems && this.contentItems.length > 0) { + converter = new TagsConverter(this.language, this.contentItems); + + this.selectionControl.enable(); + } else { + converter = new TagsConverter(null!, []); + + this.selectionControl.disable(); + } + + this.next({ converter }); + } +} diff --git a/frontend/app/shared/declarations.ts b/frontend/app/shared/declarations.ts index bbbf65b1d..8eed15fbd 100644 --- a/frontend/app/shared/declarations.ts +++ b/frontend/app/shared/declarations.ts @@ -25,6 +25,7 @@ export * from './components/language-selector.component'; export * from './components/markdown-editor.component'; export * from './components/pipes'; export * from './components/references-dropdown.component'; +export * from './components/references-tags.component'; export * from './components/rich-editor.component'; export * from './components/saved-queries.component'; export * from './components/schema-category.component'; diff --git a/frontend/app/shared/internal.ts b/frontend/app/shared/internal.ts index 2d2533126..152df1578 100644 --- a/frontend/app/shared/internal.ts +++ b/frontend/app/shared/internal.ts @@ -61,7 +61,7 @@ export * from './state/roles.forms'; export * from './state/roles.state'; export * from './state/rule-events.state'; export * from './state/rules.state'; -export * from './state/schema-tag-converter'; +export * from './state/schema-tag-source'; export * from './state/schemas.forms'; export * from './state/schemas.state'; export * from './state/ui.state'; diff --git a/frontend/app/shared/module.ts b/frontend/app/shared/module.ts index 5c52e73b8..cee32433e 100644 --- a/frontend/app/shared/module.ts +++ b/frontend/app/shared/module.ts @@ -75,6 +75,7 @@ import { PlansState, QueryComponent, ReferencesDropdownComponent, + ReferencesTagsComponent, RichEditorComponent, RolesService, RolesState, @@ -88,7 +89,7 @@ import { SchemaMustNotBeSingletonGuard, SchemasService, SchemasState, - SchemaTagConverter, + SchemaTagSource, SearchFormComponent, SortingComponent, TableHeaderComponent, @@ -144,6 +145,7 @@ import { MarkdownEditorComponent, QueryComponent, ReferencesDropdownComponent, + ReferencesTagsComponent, RichEditorComponent, SavedQueriesComponent, SchemaCategoryComponent, @@ -182,6 +184,7 @@ import { LanguageSelectorComponent, MarkdownEditorComponent, ReferencesDropdownComponent, + ReferencesTagsComponent, RichEditorComponent, RouterModule, SavedQueriesComponent, @@ -247,7 +250,7 @@ export class SqxSharedModule { SchemaMustNotBeSingletonGuard, SchemasService, SchemasState, - SchemaTagConverter, + SchemaTagSource, TranslationsService, UIService, UIState, diff --git a/frontend/app/shared/state/schema-tag-converter.ts b/frontend/app/shared/state/schema-tag-source.ts similarity index 52% rename from frontend/app/shared/state/schema-tag-converter.ts rename to frontend/app/shared/state/schema-tag-source.ts index 2869ce98c..0b2157d38 100644 --- a/frontend/app/shared/state/schema-tag-converter.ts +++ b/frontend/app/shared/state/schema-tag-source.ts @@ -5,39 +5,24 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Injectable, OnDestroy } from '@angular/core'; -import { Subscription } from 'rxjs'; +import { Injectable } from '@angular/core'; +import { map, shareReplay } from 'rxjs/operators'; import { Converter, TagValue } from '@app/framework'; import { SchemaDto } from './../services/schemas.service'; import { SchemasState } from './schemas.state'; -@Injectable() -export class SchemaTagConverter implements Converter, OnDestroy { - private schemasSubscription: Subscription; - private schemas: ReadonlyArray = []; - - public suggestions: ReadonlyArray = []; +class SchemaConverter implements Converter { + public suggestions: ReadonlyArray; constructor( - readonly schemasState: SchemasState + private readonly schemas: ReadonlyArray ) { - this.schemasSubscription = - schemasState.schemas.subscribe(schemas => { - this.schemas = schemas; - - this.suggestions = this.schemas.map(x => new TagValue(x.id, x.name, x.id)); - }); - - this.schemasState.loadIfNotLoaded(); - } - - public ngOnDestroy() { - this.schemasSubscription.unsubscribe(); + this.suggestions = schemas.map(x => new TagValue(x.id, x.name, x.id)); } - public convertInput(input: string): TagValue | null { + public convertInput(input: string) { const schema = this.schemas.find(x => x.name === input); if (schema) { @@ -47,7 +32,7 @@ export class SchemaTagConverter implements Converter, OnDestroy { return null; } - public convertValue(value: any): TagValue | null { + public convertValue(value: any) { const schema = this.schemas.find(x => x.id === value); if (schema) { @@ -56,4 +41,17 @@ export class SchemaTagConverter implements Converter, OnDestroy { return null; } +} + +@Injectable() +export class SchemaTagSource { + public converter = + this.schemasState.schemas.pipe( + map(x => new SchemaConverter(x), shareReplay(1))); + + constructor( + readonly schemasState: SchemasState + ) { + this.schemasState.loadIfNotLoaded(); + } } \ No newline at end of file