0)">
-
- {{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