|
|
@ -13,52 +13,103 @@ import { distinctUntilChanged, map, tap } from 'rxjs/operators'; |
|
|
|
|
|
|
|
|
import { Keys, StatefulControlComponent, Types } from '@app/framework/internal'; |
|
|
import { Keys, StatefulControlComponent, Types } from '@app/framework/internal'; |
|
|
|
|
|
|
|
|
|
|
|
export const CONVERSION_FAILED = {}; |
|
|
|
|
|
|
|
|
|
|
|
export class TagValue<T = any> { |
|
|
|
|
|
public readonly lowerCaseName: string; |
|
|
|
|
|
|
|
|
|
|
|
constructor( |
|
|
|
|
|
public readonly id: any, |
|
|
|
|
|
public readonly name: string, |
|
|
|
|
|
public readonly value: T |
|
|
|
|
|
) { |
|
|
|
|
|
this.lowerCaseName = name.toLowerCase(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public toString() { |
|
|
|
|
|
return this.name; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
export interface Converter { |
|
|
export interface Converter { |
|
|
convert(input: string): any; |
|
|
convertInput(input: string): TagValue | null; |
|
|
|
|
|
|
|
|
isValidInput(input: string): boolean; |
|
|
convertValue(value: any): TagValue | null; |
|
|
isValidValue(value: any): boolean; |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
export class IntConverter implements Converter { |
|
|
export class IntConverter implements Converter { |
|
|
public isValidInput(input: string): boolean { |
|
|
private static ZERO = new TagValue(0, '0', 0); |
|
|
return !!parseInt(input, 10) || input === '0'; |
|
|
|
|
|
} |
|
|
public convertInput(input: string): TagValue<number> | null { |
|
|
|
|
|
if (input === '0') { |
|
|
|
|
|
return IntConverter.ZERO; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const parsed = parseInt(input, 10); |
|
|
|
|
|
|
|
|
|
|
|
if (parsed) { |
|
|
|
|
|
return new TagValue(parsed, input, parsed); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
public isValidValue(value: any): boolean { |
|
|
return null; |
|
|
return Types.isNumber(value); |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
public convert(input: string): any { |
|
|
public convertValue(value: any): TagValue<number> | null { |
|
|
return parseInt(input, 10) || 0; |
|
|
if (Types.isNumber(value)) { |
|
|
|
|
|
return new TagValue(value, `${value}`, value); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return null; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
export class FloatConverter implements Converter { |
|
|
export class FloatConverter implements Converter { |
|
|
public isValidInput(input: string): boolean { |
|
|
private static ZERO = new TagValue(0, '0', 0); |
|
|
return !!parseFloat(input) || input === '0'; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public isValidValue(value: any): boolean { |
|
|
public convertInput(input: string): TagValue<number> | null { |
|
|
return Types.isNumber(value); |
|
|
if (input === '0') { |
|
|
|
|
|
return FloatConverter.ZERO; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const parsed = parseFloat(input); |
|
|
|
|
|
|
|
|
|
|
|
if (parsed) { |
|
|
|
|
|
return new TagValue(parsed, input, parsed); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return null; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
public convert(input: string): any { |
|
|
public convertValue(value: any): TagValue<number> | null { |
|
|
return parseFloat(input) || 0; |
|
|
if (Types.isNumber(value)) { |
|
|
|
|
|
return new TagValue(value, `${value}`, value); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return null; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
export class StringConverter implements Converter { |
|
|
export class StringConverter implements Converter { |
|
|
public isValidInput(input: string): boolean { |
|
|
public convertInput(input: string): TagValue<string> | null { |
|
|
return input.trim().length > 0; |
|
|
if (input) { |
|
|
} |
|
|
const trimmed = input.trim(); |
|
|
|
|
|
|
|
|
public isValidValue(value: any): boolean { |
|
|
if (trimmed.length > 0) { |
|
|
return Types.isString(value); |
|
|
return new TagValue(trimmed, trimmed, trimmed); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return null; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
public convert(input: string): any { |
|
|
public convertValue(value: any): TagValue<string> | null { |
|
|
return input.trim(); |
|
|
if (Types.isString(value)) { |
|
|
|
|
|
const trimmed = value.trim(); |
|
|
|
|
|
|
|
|
|
|
|
return new TagValue(trimmed, trimmed, trimmed); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return null; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -73,10 +124,10 @@ let CACHED_FONT: string; |
|
|
interface State { |
|
|
interface State { |
|
|
hasFocus: boolean; |
|
|
hasFocus: boolean; |
|
|
|
|
|
|
|
|
suggestedItems: string[]; |
|
|
suggestedItems: TagValue[]; |
|
|
suggestedIndex: number; |
|
|
suggestedIndex: number; |
|
|
|
|
|
|
|
|
items: any[]; |
|
|
items: TagValue[]; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@Component({ |
|
|
@Component({ |
|
|
@ -93,6 +144,9 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i |
|
|
@ViewChild('input', { static: false }) |
|
|
@ViewChild('input', { static: false }) |
|
|
public inputElement: ElementRef<HTMLInputElement>; |
|
|
public inputElement: ElementRef<HTMLInputElement>; |
|
|
|
|
|
|
|
|
|
|
|
@Input() |
|
|
|
|
|
public suggestedValues: TagValue[] = []; |
|
|
|
|
|
|
|
|
@Input() |
|
|
@Input() |
|
|
public converter: Converter = new StringConverter(); |
|
|
public converter: Converter = new StringConverter(); |
|
|
|
|
|
|
|
|
@ -105,9 +159,6 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i |
|
|
@Input() |
|
|
@Input() |
|
|
public allowDuplicates = true; |
|
|
public allowDuplicates = true; |
|
|
|
|
|
|
|
|
@Input() |
|
|
|
|
|
public suggestions: string[] = []; |
|
|
|
|
|
|
|
|
|
|
|
@Input() |
|
|
@Input() |
|
|
public singleLine = false; |
|
|
public singleLine = false; |
|
|
|
|
|
|
|
|
@ -120,6 +171,15 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i |
|
|
@Input() |
|
|
@Input() |
|
|
public inputName = 'tag-editor'; |
|
|
public inputName = 'tag-editor'; |
|
|
|
|
|
|
|
|
|
|
|
@Input() |
|
|
|
|
|
public set suggestions(value: string[]) { |
|
|
|
|
|
if (value) { |
|
|
|
|
|
this.suggestedValues = value.map(x => new TagValue(x, x, x)); |
|
|
|
|
|
} else { |
|
|
|
|
|
this.suggestedValues = []; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
@Input() |
|
|
@Input() |
|
|
public set disabled(value: boolean) { |
|
|
public set disabled(value: boolean) { |
|
|
this.setDisabledState(value); |
|
|
this.setDisabledState(value); |
|
|
@ -161,8 +221,8 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i |
|
|
}), |
|
|
}), |
|
|
distinctUntilChanged(), |
|
|
distinctUntilChanged(), |
|
|
map(query => { |
|
|
map(query => { |
|
|
if (Types.isArray(this.suggestions) && query && query.length > 0) { |
|
|
if (Types.isArray(this.suggestedValues) && query && query.length > 0) { |
|
|
return this.suggestions.filter(s => s.toLowerCase().indexOf(query) >= 0 && this.snapshot.items.indexOf(s) < 0); |
|
|
return this.suggestedValues.filter(s => s.lowerCaseName.indexOf(query) >= 0 && !this.snapshot.items.find(x => x.id === s.id)); |
|
|
} else { |
|
|
} else { |
|
|
return []; |
|
|
return []; |
|
|
} |
|
|
} |
|
|
@ -180,11 +240,23 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i |
|
|
this.resetForm(); |
|
|
this.resetForm(); |
|
|
this.resetSize(); |
|
|
this.resetSize(); |
|
|
|
|
|
|
|
|
if (this.converter && Types.isArrayOf(obj, v => this.converter.isValidValue(v))) { |
|
|
const items: any[] = []; |
|
|
this.next(s => ({ ...s, items: obj })); |
|
|
|
|
|
} else { |
|
|
if (this.converter && Types.isArray(obj)) { |
|
|
this.next(s => ({ ...s, items: [] })); |
|
|
for (let value of obj) { |
|
|
|
|
|
if (Types.is(value, TagValue)) { |
|
|
|
|
|
items.push(value); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const converted = this.converter.convertValue(obj); |
|
|
|
|
|
|
|
|
|
|
|
if (converted) { |
|
|
|
|
|
items.push(value); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
this.next(s => ({ ...s, items })); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
public setDisabledState(isDisabled: boolean): void { |
|
|
public setDisabledState(isDisabled: boolean): void { |
|
|
@ -296,16 +368,22 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i |
|
|
return true; |
|
|
return true; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
public selectValue(value: string, noFocus?: boolean) { |
|
|
public selectValue(value: TagValue | string, noFocus?: boolean) { |
|
|
if (!noFocus) { |
|
|
if (!noFocus) { |
|
|
this.inputElement.nativeElement.focus(); |
|
|
this.inputElement.nativeElement.focus(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (value && this.converter.isValidInput(value)) { |
|
|
let tagValue: TagValue | null; |
|
|
const converted = this.converter.convert(value); |
|
|
|
|
|
|
|
|
if (Types.isString(value)) { |
|
|
|
|
|
tagValue = this.converter.convertInput(value); |
|
|
|
|
|
} else { |
|
|
|
|
|
tagValue = value; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
if (this.allowDuplicates || this.snapshot.items.indexOf(converted) < 0) { |
|
|
if (tagValue) { |
|
|
this.updateItems([...this.snapshot.items, converted]); |
|
|
if (this.allowDuplicates || !this.snapshot.items.find(x => x.id === tagValue!.id)) { |
|
|
|
|
|
this.updateItems([...this.snapshot.items, tagValue]); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
this.resetForm(); |
|
|
this.resetForm(); |
|
|
@ -363,7 +441,7 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i |
|
|
public onCopy(event: ClipboardEvent) { |
|
|
public onCopy(event: ClipboardEvent) { |
|
|
if (!this.hasSelection()) { |
|
|
if (!this.hasSelection()) { |
|
|
if (event.clipboardData) { |
|
|
if (event.clipboardData) { |
|
|
event.clipboardData.setData('text/plain', this.snapshot.items.filter(x => !!x).join(',')); |
|
|
event.clipboardData.setData('text/plain', this.snapshot.items.map(x => x.name).join(',')); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
event.preventDefault(); |
|
|
event.preventDefault(); |
|
|
@ -380,7 +458,7 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i |
|
|
const values = [...this.snapshot.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.convertInput(part); |
|
|
|
|
|
|
|
|
if (converted) { |
|
|
if (converted) { |
|
|
values.push(converted); |
|
|
values.push(converted); |
|
|
@ -401,13 +479,13 @@ export class TagEditorComponent extends StatefulControlComponent<State, any[]> i |
|
|
return s && e && (e - s) > 0; |
|
|
return s && e && (e - s) > 0; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
private updateItems(items: any[]) { |
|
|
private updateItems(items: TagValue[]) { |
|
|
this.next(s => ({ ...s, 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(items); |
|
|
this.callChange(items.map(x => x.value)); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
this.resetSize(); |
|
|
this.resetSize(); |
|
|
|