From 0d51e55866d13abe5249e8b7c7978573b2ea5385 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Z=C3=A9fling?= Date: Sun, 8 Dec 2024 12:35:00 +0100 Subject: [PATCH] Much stricter code rewriting --- package.json | 4 +- .../src/lib/select2-interfaces.ts | 17 +- .../src/lib/select2-utils.ts | 46 +-- .../src/lib/select2.component.html | 51 ++- .../src/lib/select2.component.ts | 292 ++++++++++-------- src/app/app-examples.component.html | 8 +- src/app/app-examples.component.ts | 69 ++--- src/app/app-gen.component.html | 24 +- src/app/app-gen.component.ts | 93 +++--- tsconfig.json | 13 +- 10 files changed, 324 insertions(+), 293 deletions(-) diff --git a/package.json b/package.json index 2dfb48d..b725780 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "@angular/platform-browser": "^19.0.0", "@angular/platform-browser-dynamic": "^19.0.0", "@angular/router": "^19.0.0", - "@ikilote/json2html": "0.4.2", + "@ikilote/json2html": "0.5.0", "bowser": "2.11.0", "core-js": "^2.5.4", "graphemer": "^1.4.0", @@ -74,4 +74,4 @@ "tslib": "^2.3.0", "typescript": "~5.6.3" } -} +} \ No newline at end of file diff --git a/projects/ng-select2-component/src/lib/select2-interfaces.ts b/projects/ng-select2-component/src/lib/select2-interfaces.ts index 07a6d30..9315042 100644 --- a/projects/ng-select2-component/src/lib/select2-interfaces.ts +++ b/projects/ng-select2-component/src/lib/select2-interfaces.ts @@ -1,7 +1,10 @@ import { TemplateRef } from '@angular/core'; + + import { Select2 } from './select2.component'; + export interface Select2Group { /** label of group */ label: string; @@ -36,9 +39,9 @@ export interface Select2Option { hide?: boolean; } -export type Select2Value = string | number | boolean | object; +export type Select2Value = string | number | boolean | object | null | undefined; -export type Select2UpdateValue = Select2Value | Select2Value[]; +export type Select2UpdateValue = Select2Value | Select2Value[] | undefined | null; export type Select2Data = (Select2Group | Select2Option)[]; @@ -46,9 +49,9 @@ export interface Select2UpdateEvent /** component */ readonly component: Select2; /** current selected value */ - readonly value: U; + readonly value: U | null; /** selected option */ - readonly options: Select2Option[]; + readonly options: Select2Option[] | null; } export interface Select2AutoCreateEvent { @@ -57,14 +60,14 @@ export interface Select2AutoCreateEvent { /** component */ readonly component: Select2; /** current selected value */ - readonly value: U; + readonly value: U | null; /** search text */ readonly search: string; /** current data source */ @@ -95,4 +98,4 @@ export interface Select2ScrollEvent { export type Select2SelectionOverride = string | ((params: { size: number; options: Select2Option[] | null }) => string); -export type Select2Template = TemplateRef | { [key: string]: TemplateRef }; +export type Select2Template = TemplateRef | { [key: string]: TemplateRef } | undefined; \ No newline at end of file diff --git a/projects/ng-select2-component/src/lib/select2-utils.ts b/projects/ng-select2-component/src/lib/select2-utils.ts index 5e690f9..5e41b6e 100644 --- a/projects/ng-select2-component/src/lib/select2-utils.ts +++ b/projects/ng-select2-component/src/lib/select2-utils.ts @@ -1,8 +1,9 @@ import { defaultMinCountForSearch, protectRegexp, unicodePatterns } from './select2-const'; import { Select2Data, Select2Group, Select2Option, Select2UpdateValue, Select2Value } from './select2-interfaces'; + export class Select2Utils { - static getOptionByValue(data: Select2Data, value: Select2Value | null | undefined) { + static getOptionByValue(data: Select2Data, value: Select2Value ) { if (Array.isArray(data)) { for (const groupOrOption of data) { const options = (groupOrOption as Select2Group).options; @@ -17,7 +18,7 @@ export class Select2Utils { } } } - return undefined; + return null; } static getOptionsByValue( @@ -36,7 +37,7 @@ export class Select2Utils { } return result; } - return Select2Utils.getOptionByValue(data, value as Select2Value | null | undefined); + return Select2Utils.getOptionByValue(data, value as Select2Value); } static getFirstAvailableOption(data: Select2Data) { @@ -60,7 +61,7 @@ export class Select2Utils { return null; } - static valueIsNotInFilteredData(filteredData: Select2Data, value: Select2Value | null | undefined) { + static valueIsNotInFilteredData(filteredData: Select2Data, value: Select2Value) { if (Select2Utils.isNullOrUndefined(value)) { return true; } @@ -79,7 +80,10 @@ export class Select2Utils { return true; } - static getPreviousOption(filteredData: Select2Data, hoveringValue: Select2Value | null | undefined) { + static getPreviousOption( + filteredData: Select2Data , + hoveringValue: Select2Value , + ): Select2Option | null { let findIt = Select2Utils.isNullOrUndefined(hoveringValue); for (let i = filteredData.length - 1; i >= 0; i--) { const groupOrOption = filteredData[i]; @@ -107,12 +111,23 @@ export class Select2Utils { return null; } - static getNextOption(filteredData: Select2Data, hoveringValue: Select2Value | null | undefined) { + static getNextOption(filteredData: Select2Data | null, hoveringValue: Select2Value) { let findIt = Select2Utils.isNullOrUndefined(hoveringValue); - for (const groupOrOption of filteredData) { - const options = (groupOrOption as Select2Group).options; - if (options) { - for (const option of options) { + if (filteredData) { + for (const groupOrOption of filteredData) { + const options = (groupOrOption as Select2Group).options; + if (options) { + for (const option of options) { + if (findIt) { + if (!option.disabled && !option.hide) { + return option; + } + } else if (!findIt) { + findIt = option.value === hoveringValue; + } + } + } else { + const option = groupOrOption as Select2Option; if (findIt) { if (!option.disabled && !option.hide) { return option; @@ -121,15 +136,6 @@ export class Select2Utils { findIt = option.value === hoveringValue; } } - } else { - const option = groupOrOption as Select2Option; - if (findIt) { - if (!option.disabled && !option.hide) { - return option; - } - } else if (!findIt) { - findIt = option.value === hoveringValue; - } } } return null; @@ -144,7 +150,7 @@ export class Select2Utils { for (const groupOrOption of data) { const options = (groupOrOption as Select2Group).options; if (options) { - const group = { + const group: Select2Group | Select2Option = { ...groupOrOption, options: [], }; diff --git a/projects/ng-select2-component/src/lib/select2.component.html b/projects/ng-select2-component/src/lib/select2.component.html index 8b44f3d..c75b9db 100644 --- a/projects/ng-select2-component/src/lib/select2.component.html +++ b/projects/ng-select2-component/src/lib/select2.component.html @@ -35,8 +35,8 @@ @if (resettable() && resetSelectedValue() !== value() && select2Option && !(disabled() || readonly())) { - × - } + × + } @if ( !multiple() && resettable() && resetSelectedValue() !== value() && select2Option && !(disabled || readonly()) ) { @@ -56,7 +56,7 @@ > } } - {{ + {{ placeholder() }} @@ -69,12 +69,12 @@
    @if (!autoCreate()) { {{ placeholder() }} } - @for (op of option || []; track trackBy($index, op)) { + @for (op of selectedOption || []; track op) {
  • - @if (showSelectAll() && multiple) { + @if (showSelectAll() && multiple()) {
  • {{ selectAllTest() ? removeAllText() : selectAllText() }} @@ -201,20 +201,20 @@
  • } - @for (groupOrOption of filteredData(); track trackBy(i, groupOrOption); let i = $index) { - @if (groupOrOption.options !== undefined) { + @for (groupOrOption of filteredData(); track groupOrOption; let i = $index) { + @let group = _toGroup(groupOrOption); + @if (group.options !== undefined) {
  • - @if (!hasTemplate(groupOrOption, 'group')) { + @if (!hasTemplate(group, 'group')) { } @else { - - + }
      - @for (option of groupOrOption.options; track trackBy(j, option); let j = $index) { + @for (option of group.options; track option; let j = $index) {
    • } @else { + @let option = _toOption(groupOrOption);
    • - @if (!hasTemplate(groupOrOption, 'option')) { -
      + @if (!hasTemplate(option, 'option')) { +
      } @else { - - + }
    • - - + } } diff --git a/projects/ng-select2-component/src/lib/select2.component.ts b/projects/ng-select2-component/src/lib/select2.component.ts index e6c7b09..bdb8b17 100644 --- a/projects/ng-select2-component/src/lib/select2.component.ts +++ b/projects/ng-select2-component/src/lib/select2.component.ts @@ -2,7 +2,6 @@ import { CdkConnectedOverlay, CdkOverlayOrigin, ConnectedOverlayPositionChange, - ConnectedPosition, VerticalConnectionPos, } from '@angular/cdk/overlay'; import { ViewportRuler } from '@angular/cdk/scrolling'; @@ -72,9 +71,9 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView readonly minCharForSearch = input(0, { transform: numberAttribute }); - readonly displaySearchStatus = input<'default' | 'hidden' | 'always'>(undefined); + readonly displaySearchStatus = input<'default' | 'hidden' | 'always' | undefined>(undefined); - readonly placeholder = input(undefined); + readonly placeholder = input(undefined); readonly limitSelection = input(0, { transform: numberAttribute }); @@ -113,13 +112,13 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView readonly noLabelTemplate = input(false, { transform: booleanAttribute }); /** use it for change the pattern of the filter search */ - readonly editPattern = input<(str: string) => string>(undefined); + readonly editPattern = input<((str: string) => string) | undefined>(undefined); /** template(s) for formatting */ readonly templates = input(undefined); /** template for formatting selected option */ - readonly templateSelection = input>(undefined); + readonly templateSelection = input | undefined>(undefined); /** the max height of the results container when opening the select */ readonly resultMaxHeight = input('200px'); @@ -169,7 +168,7 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView * * if string: `%size%` = total selected options * * if function: juste show the string */ - readonly selectionOverride = input(undefined); + readonly selectionOverride = input(undefined); /** force selection on one line */ readonly selectionNoWrap = input(false, { transform: booleanAttribute }); @@ -233,9 +232,9 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView // ----------------------- internal var - option: Select2Option | Select2Option[] | null = null; + selectedOption: Select2Option | Select2Option[] | null = null; isOpen = false; - searchStyle: string; + searchStyle: string | undefined; /** Whether the element is focused or not. */ focused = false; @@ -243,11 +242,11 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView filteredData = signal(undefined); get select2Options() { - return this.multiple() ? (this.option as Select2Option[]) : null; + return this.multiple() ? (this.selectedOption as Select2Option[]) : null; } get select2Option() { - return this.multiple() ? null : (this.option as Select2Option); + return this.multiple() ? null : (this.selectedOption as Select2Option); } get searchText() { @@ -262,24 +261,24 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView return this._control?.disabled ?? this._disabled; } - protected overlayWidth: number; - protected overlayHeight: number; - protected _triggerRect: DOMRect; - protected _dropdownRect: DOMRect; + protected overlayWidth: number | string = ''; + protected overlayHeight: number | string = ''; + protected _triggerRect: DOMRect | undefined; + protected _dropdownRect: DOMRect | undefined; - protected get _positions(): ConnectedPosition[] { + protected get _positions(): any { return this.listPosition() === 'auto' ? undefined : null; } - protected maxResultsExceeded: boolean; + protected maxResultsExceeded: boolean | undefined; private hoveringValue: Select2Value | null | undefined = null; private innerSearchText = ''; - private isSearchboxHidden: boolean; + private isSearchboxHidden: boolean | undefined; - private selectionElement: HTMLElement; + private selectionElement: HTMLElement | undefined; - private get resultsElement(): HTMLElement { + private get resultsElement(): HTMLElement | undefined { return this.resultContainer()?.nativeElement; } @@ -290,12 +289,18 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView return this.disabledState ? -1 : this.tabIndex(); } + private _data: Select2Data = []; + + protected get _id(): string { + return this.id() || this._uid; + } + private _disabled = false; - private _id: string; + private _uid = `select2-${nextUniqueId++}`; - private _value: Select2UpdateValue = null; - private _previousNativeValue: Select2UpdateValue; - private _overlayPosition: VerticalConnectionPos; + private _value: Select2UpdateValue | null = null; + private _previousNativeValue: Select2UpdateValue | undefined; + private _overlayPosition: VerticalConnectionPos | undefined; private toObservable = new Subscription(); constructor( @@ -320,6 +325,7 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView ); this.toObservable.add( toObservable(this.data).subscribe(_data => { + this._data = _data; this.updateFilteredData(); }), ); @@ -328,11 +334,6 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView this.updateSearchBox(); }), ); - this.toObservable.add( - toObservable(this.id).subscribe(id => { - this._id = id || this._uid; - }), - ); this.toObservable.add( toObservable(this.disabled).subscribe(disabled => { this._disabled = disabled; @@ -389,12 +390,12 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView }); const option = Select2Utils.getOptionsByValue( - this.data(), + this._data, this._control ? this._control.value : this.value, this.multiple(), ); if (option !== null) { - this.option = option; + this.selectedOption = option ?? null; } if (!Array.isArray(option)) { this.hoveringValue = this.value() as string | undefined; @@ -403,7 +404,7 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView } ngAfterViewInit() { - this.cdkConnectedOverlay().positionChange.subscribe((posChange: ConnectedOverlayPositionChange) => { + this.cdkConnectedOverlay()?.positionChange.subscribe((posChange: ConnectedOverlayPositionChange) => { if ( this.listPosition() === 'auto' && posChange.connectionPair?.originY && @@ -415,7 +416,7 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView } }); - this.selectionElement = this.selection().nativeElement; + this.selectionElement = this.selection()?.nativeElement; this.triggerRect(); } @@ -426,7 +427,11 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView if (this.overlayWidth !== this._triggerRect.width) { this.overlayWidth = this._triggerRect.width; } - if (this._dropdownRect?.height > 0 && this.overlayHeight !== this._dropdownRect.height) { + if ( + this._dropdownRect && + this._dropdownRect.height > 0 && + this.overlayHeight !== this._dropdownRect.height + ) { this.overlayHeight = this.listPosition() === 'auto' ? this._dropdownRect.height : 0; } } @@ -439,7 +444,7 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView updateSearchBox() { const hidden = this.customSearchEnabled() ? false - : Select2Utils.isSearchboxHidden(this.data(), this.minCountForSearch()); + : Select2Utils.isSearchboxHidden(this._data, this.minCountForSearch()); if (this.isSearchboxHidden !== hidden) { this.isSearchboxHidden = hidden; } @@ -447,7 +452,9 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView hideSearch(): boolean { const displaySearchStatus = - displaySearchStatusList.indexOf(this.displaySearchStatus()) > -1 ? this.displaySearchStatus() : 'default'; + displaySearchStatusList.indexOf(this.displaySearchStatus() || 'default') > -1 + ? this.displaySearchStatus() + : 'default'; return (displaySearchStatus === 'default' && this.isSearchboxHidden) || displaySearchStatus === 'hidden'; } @@ -473,12 +480,12 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView } reset(event?: MouseEvent) { - // const test = Select2Utils.getOptionByValue(this.data(), this.resetSelectedValue); + // const test = Select2Utils.getOptionByValue(this._data, this.resetSelectedValue); // debugger; const resetSelectedValue = this.resetSelectedValue(); this.select( resetSelectedValue !== undefined - ? (Select2Utils.getOptionByValue(this.data(), resetSelectedValue) ?? null) + ? (Select2Utils.getOptionByValue(this._data, resetSelectedValue) ?? null) : null, ); @@ -515,8 +522,10 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView this.keyDown(event); } else { setTimeout(() => { - if (this.option) { - const option = Array.isArray(this.option) ? this.option[0] : this.option; + if (this.selectedOption) { + const option = Array.isArray(this.selectedOption) + ? this.selectedOption[0] + : this.selectedOption; this.updateScrollFromOption(option); } else if (this.resultsElement) { this.resultsElement.scrollTop = 0; @@ -538,16 +547,16 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView } hasTemplate(option: Select2Option | Select2Group, defaultValue: string, select: boolean = false) { - const templates = this.templates(); - const templatesValue = this.templates(); + const templates: any = this.templates(); + const templatesValue: any = this.templates(); return ( (select - ? templates?.[(option as Select2Option).templateSelectionId] instanceof TemplateRef || + ? templates?.[(option as Select2Option).templateSelectionId ?? ''] instanceof TemplateRef || templates?.[`${defaultValue}Selection`] instanceof TemplateRef || templates?.[`templateSelection`] instanceof TemplateRef || this.templateSelection() instanceof TemplateRef : false) || - templatesValue?.[option.templateId] instanceof TemplateRef || + templatesValue?.[option.templateId ?? ''] instanceof TemplateRef || templatesValue?.[defaultValue] instanceof TemplateRef || templatesValue?.['template'] instanceof TemplateRef || templatesValue instanceof TemplateRef || @@ -556,16 +565,16 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView } getTemplate(option: Select2Option | Select2Group, defaultValue: string, select: boolean = false) { - const templates = this.templates(); - const templatesValue = this.templates(); + const templates: any = this.templates(); + const templatesValue: any = this.templates(); return this.hasTemplate(option, defaultValue, select) ? (select - ? templates?.[(option as Select2Option).templateSelectionId] || + ? templates?.[(option as Select2Option).templateSelectionId ?? ''] || templates?.[`${defaultValue}Selection`] || templates?.[`templateSelection`] || this.templateSelection() : undefined) || - templatesValue?.[option.templateId] || + templatesValue?.[option.templateId ?? ''] || templatesValue?.[defaultValue] || templatesValue?.['template'] || templatesValue @@ -573,7 +582,7 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView } triggerRect() { - this._triggerRect = this.selectionElement.getBoundingClientRect(); + this._triggerRect = this.selectionElement?.getBoundingClientRect(); const dropdown = this.dropdown(); this._dropdownRect = dropdown?.nativeElement ? dropdown.nativeElement.getBoundingClientRect() : undefined; } @@ -586,7 +595,7 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView if (this.multiple()) { if (!this.selectAllTest()) { const options: Select2Option[] = []; - this.data().forEach(e => { + this._data.forEach(e => { if ((e as Select2Group).options) { (e as Select2Group).options.forEach(f => { if (!f.disabled && !f.hide) { @@ -597,11 +606,11 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView options.push(e as Select2Option); } }); - this.option = options; - this._value = options.map(e => e.value); + this.selectedOption = options; + this.writeValue(options.map(e => e.value)); } else { - this.option = []; - this._value = []; + this.selectedOption = []; + this.writeValue([]); } this.isOpen = false; @@ -610,9 +619,9 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView } selectAllTest() { - if (this.multiple() && Array.isArray(this.option) && this.option.length) { + if (this.multiple() && Array.isArray(this.selectedOption) && this.selectedOption.length) { let options = 0; - this.data().forEach(e => { + this._data.forEach(e => { if ((e as Select2Group).options) { (e as Select2Group).options.forEach(f => { if (!f.disabled && !f.hide) { @@ -623,7 +632,7 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView options++; } }); - return this.option.length === options; + return this.selectedOption.length === options; } return false; } @@ -639,7 +648,7 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView ); } - private testValueChange(value1: Select2UpdateValue, value2: Select2UpdateValue) { + private testValueChange(value1: Select2UpdateValue | null, value2: Select2UpdateValue | undefined) { if ( ((value1 === null || value1 === undefined) && (value2 === null || value2 === undefined)) || value1 === value2 @@ -663,16 +672,15 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView return true; } - private updateFilteredData(writeValue = false) { - let result = this.data(); + private updateFilteredData() { + let result = this._data; + if (this.multiple() && this.hideSelectedItems()) { - result = Select2Utils.getFilteredSelectedData(result, this.option); + result = Select2Utils.getFilteredSelectedData(result, this.selectedOption); } - if (!this.customSearchEnabled() && this.searchText && this.searchText.length >= +this.minCharForSearch()) { result = Select2Utils.getFilteredData(result, this.searchText, this.editPattern()); } - if (this.maxResults() > 0) { const data = Select2Utils.getReduceData(result, +this.maxResults()); result = data.result; @@ -689,10 +697,10 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView // replace selected options when data change - if (this.multiple() && Array.isArray(this.option) && this.option.length) { + if (this.multiple() && Array.isArray(this.selectedOption) && this.selectedOption.length) { const options: Select2Option[] = []; - const value = this.option.map(e => e.value); - this.data().forEach(e => { + const value = this.selectedOption.map(e => e.value); + this._data.forEach(e => { if ((e as Select2Group).options) { (e as Select2Group).options.forEach(f => { if (value.includes(f.value)) { @@ -704,21 +712,21 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView } }); // preserve selection order - this.option = this.option.map(e => options.find(f => f.value === e.value)); - } else if (!Array.isArray(this.option) && this.option) { - let option: Select2Option = undefined; - this.data().forEach(e => { + this.selectedOption = this.selectedOption.map(e => options.find(f => f.value === e.value)!); + } else if (!Array.isArray(this.selectedOption) && this.selectedOption) { + let option: Select2Option | null = null; + this._data.forEach(e => { if ((e as Select2Group).options) { (e as Select2Group).options.forEach(f => { - if ((this.option as Select2Option).value === f.value) { + if ((this.selectedOption as Select2Option).value === f.value) { option = f; } }); - } else if ((this.option as Select2Option).value === (e as Select2Option).value) { + } else if ((this.selectedOption as Select2Option).value === (e as Select2Option).value) { option = e as Select2Option; } }); - this.option = option; + this.selectedOption = option; } this._changeDetectorRef.detectChanges(); } @@ -804,10 +812,11 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView select(option: Select2Option | null, emit: boolean = true) { let value: any; + if (option !== null && option !== undefined) { if (this.multiple()) { - this.option ??= []; - const options = this.option as Select2Option[]; + this.selectedOption ??= []; + const options = this.selectedOption as Select2Option[]; const index = options.findIndex(op => op.value === option.value); if (index === -1) { options.push(option); @@ -815,28 +824,28 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView options.splice(index, 1); } - value = (this.option as Select2Option[]).map(op => op.value); + value = (this.selectedOption as Select2Option[]).map(op => op.value); } else { - this.option = option; + this.selectedOption = option; if (this.isOpen) { this.isOpen = false; this.close.emit(this); this.selectionElement?.focus(); } - value = this.option.value; + value = this.selectedOption.value; if (!option && this._value === null) { this._value = value ?? null; } } } else { // when remove value - if (Array.isArray(this.option) ? this.option?.length : this.option) { + if (Array.isArray(this.selectedOption) ? this.selectedOption?.length : this.selectedOption) { value = ''; } - this.option = null; + this.selectedOption = null; } - if (this.multiple && this.hideSelectedItems()) { + if (this.multiple() && this.hideSelectedItems()) { this.updateFilteredData(); } @@ -852,41 +861,45 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView this.update.emit({ component: this, value: this._value, - options: Array.isArray(this.option) ? this.option : this.option ? [this.option] : null, + options: Array.isArray(this.selectedOption) + ? this.selectedOption + : this.selectedOption + ? [this.selectedOption] + : null, }); }); } } - private testDiffValue(val1: Select2UpdateValue, val2: any) { + private testDiffValue(val1: Select2UpdateValue | null, val2: any) { return Array.isArray(val1) ? (val1 as [])?.length !== val2?.length : val1 !== val2; } keyDown(event: KeyboardEvent, create = false) { - if (create && this._testKey(event, ['Enter', 13])) { + if (create && this._testKey(event, ['Enter'])) { this.createAndAdd(event); - } else if (this._testKey(event, ['ArrowDown', 40])) { + } else if (this._testKey(event, ['ArrowDown'])) { this.moveDown(); event.preventDefault(); - } else if (this._testKey(event, ['ArrowUp', 38])) { + } else if (this._testKey(event, ['ArrowUp'])) { this.moveUp(); event.preventDefault(); - } else if (this._testKey(event, ['Enter', 13])) { + } else if (this._testKey(event, ['Enter'])) { this.selectByEnter(); event.preventDefault(); - } else if (this._testKey(event, ['Escape', 'Tab', 9, 27]) && this.isOpen) { + } else if (this._testKey(event, ['Escape', 'Tab']) && this.isOpen) { this.toggleOpenAndClose(); this._focus(false); } } openKey(event: KeyboardEvent, create = false) { - if (create && this._testKey(event, ['Enter', 13])) { + if (create && this._testKey(event, ['Enter'])) { this.createAndAdd(event); - } else if (this._testKey(event, ['ArrowDown', 'ArrowUp', 'Enter', 40, 38, 13])) { + } else if (this._testKey(event, ['ArrowDown', 'ArrowUp', 'Enter'])) { this.toggleOpenAndClose(true, true, event); event.preventDefault(); - } else if (this._testKey(event, ['Escape', 'Tab', 9, 27])) { + } else if (this._testKey(event, ['Escape', 'Tab'])) { if (this.isOpen) { this.toggleOpenAndClose(false); this._onTouched(); @@ -906,7 +919,7 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView component: this, value: this._value, search: this.searchText, - data: this.data(), + data: this._data, filteredData: (data: Select2Data) => { this.filteredData.set(data); this._changeDetectorRef.markForCheck(); @@ -915,26 +928,22 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView } } - trackBy(_index: number, item: Select2Option): any { - return item.value; - } - isSelected(option: Select2Option) { - return Select2Utils.isSelected(this.option, option, this.multiple()); + return Select2Utils.isSelected(this.selectedOption, option, this.multiple()); } isDisabled(option: Select2Option) { return option.disabled ? 'true' : 'false'; } - removeSelection(e: MouseEvent | KeyboardEvent, option: Select2Option) { - Select2Utils.removeSelection(this.option, option); + removeSelection(e: MouseEvent | KeyboardEvent | Event, option: Select2Option) { + Select2Utils.removeSelection(this.selectedOption, option); if (this.multiple() && this.hideSelectedItems()) { this.updateFilteredData(); } - const value = (this.option as Select2Option[]).map(op => op.value); + const value = (this.selectedOption as Select2Option[]).map(op => op.value); if (this._control) { this._onChange(value); @@ -945,7 +954,11 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView this.update.emit({ component: this, value: value, - options: Array.isArray(this.option) ? this.option : this.option ? [this.option] : null, + options: Array.isArray(this.selectedOption) + ? this.selectedOption + : this.selectedOption + ? [this.selectedOption] + : null, }); this.removeOption.emit({ component: this, @@ -966,7 +979,7 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView * @param value */ writeValue(value: any) { - this.option = null; + this.selectedOption = null; this._setSelectionByValue(value); if (this.testValueChange(this._value, value)) { this._value = value; @@ -1009,7 +1022,7 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView component: this, way, search: this.innerSearchText, - data: this.data(), + data: this._data, }); } @@ -1025,25 +1038,38 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView if (typeof selectionOverride === 'function') { return selectionOverride({ size: this.optionsSize(), - options: Array.isArray(this.option) ? this.option : this.option ? [this.option] : null, + options: Array.isArray(this.selectedOption) + ? this.selectedOption + : this.selectedOption + ? [this.selectedOption] + : null, }); } else if (typeof selectionOverride === 'string') { return selectionOverride.replaceAll('%size%', `${this.optionsSize()}`); } + return undefined; + } + + _toGroup(group: Select2Option | Select2Group) { + return group as Select2Group; + } + + _toOption(option: Select2Option | Select2Group) { + return option as Select2Option; } private optionsSize() { - return Array.isArray(this.option) ? this.option.length : this.option ? 1 : 0; + return Array.isArray(this.selectedOption) ? this.selectedOption.length : this.selectedOption ? 1 : 0; } private addItem(value: string): Select2Option { - let item = Select2Utils.getOptionByValue(this.data(), value); + let item = Select2Utils.getOptionByValue(this._data, value); if (!item) { item = { value, label: value, }; - this.data().push(item); + this._data.push(item); } return item; } @@ -1051,27 +1077,31 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView private createAndAdd(e: KeyboardEvent) { const value = (e.target as HTMLInputElement).value; if (value.trim()) { + (e.target as HTMLInputElement).value = ''; const item = this.addItem(value.trim()); this.click(item); - (e.target as HTMLInputElement).value = ''; this.autoCreateItem.emit({ value: item, component: this, - options: Array.isArray(this.option) ? this.option : this.option ? [this.option] : null, + options: Array.isArray(this.selectedOption) + ? this.selectedOption + : this.selectedOption + ? [this.selectedOption] + : null, }); } this.stopEvent(e); } private moveUp() { - this.updateScrollFromOption(Select2Utils.getPreviousOption(this.filteredData(), this.hoveringValue)); + this.updateScrollFromOption(Select2Utils.getPreviousOption(this.filteredData()!, this.hoveringValue)); } private moveDown() { - this.updateScrollFromOption(Select2Utils.getNextOption(this.filteredData(), this.hoveringValue)); + this.updateScrollFromOption(Select2Utils.getNextOption(this.filteredData()!, this.hoveringValue)); } - private updateScrollFromOption(option: Select2Option) { + private updateScrollFromOption(option: Select2Option | null) { if (option) { this.hoveringValue = option.value; const domElement = this.results().find(r => r.nativeElement.innerText.trim() === option.label); @@ -1086,7 +1116,7 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView private selectByEnter() { if (this.hoveringValue) { - const option = Select2Utils.getOptionByValue(this.data(), this.hoveringValue); + const option = Select2Utils.getOptionByValue(this._data, this.hoveringValue) ?? null; this.select(option); } } @@ -1096,19 +1126,19 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView } private _getKey(event: KeyboardEvent): number | string { - let code: number | string; + let code: number | string | undefined; if (event.key !== undefined) { code = event.key; - } else if (event['keyIdentifier'] !== undefined) { - code = event['keyIdentifier']; - } else if (event['keyCode'] !== undefined) { - code = event['keyCode']; + } else if ((event as any)['keyIdentifier'] !== undefined) { + code = (event as any)['keyIdentifier']; + } else if ((event as any)['keyCode'] !== undefined) { + code = (event as any)['keyCode']; } else { event.preventDefault(); } - return code; + return code ?? ''; } private _isKey(code: number | string, refs: (number | string)[] = []): boolean { @@ -1120,18 +1150,18 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView * found with the designated value, the select trigger is cleared. */ private _setSelectionByValue(value: any | any[]): void { - if (this.option || (value !== undefined && value !== null)) { + if (this.selectedOption || (value !== undefined && value !== null)) { const isArray = Array.isArray(value); if (this.multiple() && value && !isArray) { throw new Error('Non array value.'); - } else if (this.data()) { + } else if (this._data) { if (this.multiple()) { - if (!Array.isArray(this.option)) { - this.option = []; // if value is null, then empty option and return + if (!Array.isArray(this.selectedOption)) { + this.selectedOption = []; // if value is null, then empty option and return } if (isArray) { // value is not null. Preselect value - (Select2Utils.getOptionsByValue(this.data(), value, this.multiple()) as []).forEach(item => + (Select2Utils.getOptionsByValue(this._data, value, this.multiple()) as []).forEach(item => this.select(item, false), ); this._value ??= value; @@ -1140,7 +1170,11 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView this.update.emit({ component: this, value: value, - options: Array.isArray(this.option) ? this.option : this.option ? [this.option] : null, + options: Array.isArray(this.selectedOption) + ? this.selectedOption + : this.selectedOption + ? [this.selectedOption] + : null, }); } } else if (value === null) { @@ -1154,13 +1188,17 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView this.update.emit({ component: this, value: this._value, - options: Array.isArray(this.option) ? this.option : this.option ? [this.option] : null, + options: Array.isArray(this.selectedOption) + ? this.selectedOption + : this.selectedOption + ? [this.selectedOption] + : null, }); } } } else { this._value = value; - this.select(Select2Utils.getOptionByValue(this.data(), this._value)); + this.select(Select2Utils.getOptionByValue(this._data, this._value)); } } else if (this._control) { this._control.viewToModelUpdate(value); @@ -1210,4 +1248,4 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView ? this._overlayPosition === 'top' : listPosition === 'above'; } -} +} \ No newline at end of file diff --git a/src/app/app-examples.component.html b/src/app/app-examples.component.html index cac02d8..d396759 100644 --- a/src/app/app-examples.component.html +++ b/src/app/app-examples.component.html @@ -59,7 +59,7 @@

      [data]="data6" [value]="value6" [minCountForSearch]="limit.value" - [displaySearchStatus]="status.value" + [displaySearchStatus]="$any(status.value)" (update)="update('value6', $event)" id="selec2-6" > @@ -148,7 +148,7 @@

      12. material style ({{ value12 }})

      13. boolean value ({{ value13 }})

      -

      14. FormControl ({{ fg.get('state').value }})

      +

      14. FormControl ({{ fg.get('state')!.value }})

      20. nostyle ({{ value20 }}) [value]="value20" (update)="update('value20', $event)" listPosition="above" - [styleMode]="select20?.value || 'noStyle'" + [styleMode]="$any(select20?.value) || 'noStyle'" id="selec2-20" > @@ -321,7 +321,7 @@

      26. infiniteScroll({{ value26 }})

      (scroll)="scroll26($event)" id="selec2-26" > -

      27. position ({{ value2 }})

      +

      27. position auto (overlay only) ({{ value2 }})

      { - return `Selection (${params.size}${(params.options?.length || 0) > 0 ? ': ' + params.options.map(e => e.label).join(', ') : ''}) `; + return `Selection (${params.size}${( + params.options?.length || 0) > 0 + ? ': ' + params.options!.map(e => e.label).join(', ') + : '' + }) `; }; limitSelection = 0; @@ -182,19 +159,19 @@ export class AppExamplesComponent { this.ctrlForm3.reset(); } - open(key: string, event: Event) { + open(key: string, event: Select2) { console.log(key, event); } - close(key: string, event: Event) { + close(key: string, event: Select2) { console.log(key, event); } - focus(key: string, event: Event) { + focus(key: string, event: Select2) { console.log(key, event); } - blur(key: string, event: Event) { + blur(key: string, event: Select2) { console.log(key, event); } @@ -248,7 +225,7 @@ export class AppExamplesComponent { update(key: string, event: Select2UpdateEvent) { console.log('update', event.component.id, event.value); - this[key] = event.value; + (this as any)[key] = event.value; } resetForm() { diff --git a/src/app/app-gen.component.html b/src/app/app-gen.component.html index 42f1407..f8d7828 100644 --- a/src/app/app-gen.component.html +++ b/src/app/app-gen.component.html @@ -54,11 +54,11 @@

      parameters

      -
      +
      -
      +
      -
      +
      @@ -111,11 +111,11 @@

      parameters

      -
      +
      -
      +
      @@ -143,7 +143,7 @@
      Selection
      type="text" formControlName="selectionOverrideString" id="selectionOverrideString" - [attr.disabled]="ctrlForm?.value?.selectionOverrideFunction === true ? 'disabled' : null" + [attr.disabled]="ctrlForm.value?.selectionOverrideFunction === true ? 'disabled' : null" />
      @@ -151,13 +151,13 @@
      Selection
      - @if (ctrlForm?.value?.multiple) { + @if (ctrlForm.value?.multiple) {
      Select all
      - @if (ctrlForm?.value?.showSelectAll) { + @if (ctrlForm.value?.showSelectAll) {
      @@ -265,7 +265,7 @@

      HTML render

      [removeAllText]="value?.removeAllText || 'Remove all'" [selectAllText]="value?.selectAllText || 'Select all'" [templates]=" - getTemplate( + $any(getTemplate( template, option, group, @@ -274,7 +274,7 @@

      HTML render

      optionSelection, template2Selection, template3Selection - ) + )) " [templateSelection]="getTemplateSelection(templateSelection)" (update)="_event('update', $event)" @@ -286,8 +286,8 @@

      HTML render

      (scroll)="_event('scroll', $event)" (removeOption)="_event('removeOption', $event)" > - - + + @if (data?.color) { {{ data?.color }} this.codeGeneration()); - this.ctrlForm.get('json').valueChanges.subscribe(value => this.changeJson(value)); + this.ctrlForm.get('json')?.valueChanges.subscribe(value => this.changeJson(value)); } ngAfterContentInit() { @@ -99,7 +95,7 @@ export class AppGenComponent implements AfterContentInit { } selectionOverride: Select2SelectionOverride = params => { - return `Selection (${params.size}${params.options.length > 0 ? ': ' + params.options.map(e => e.label).join(', ') : ''}) `; + return `Selection (${params.size}${params.options!.length > 0 ? ': ' + params.options!.map(e => e.label).join(', ') : ''}) `; }; getTemplate( @@ -166,6 +162,7 @@ export class AppGenComponent implements AfterContentInit { case 'ref': return template; } + return undefined; } _event(type: string, event: any) { @@ -183,7 +180,7 @@ export class AppGenComponent implements AfterContentInit { body: [], }; const value = this.ctrlForm.value; - const attrs = json.attrs; + const attrs: Json2htmlAttr = json.attrs!; const body = json.body as Json2htmlRef[]; // tags @@ -198,97 +195,97 @@ export class AppGenComponent implements AfterContentInit { // parameters if (value.disabled) { - attrs.disabled = this._testBoolean(value.disabled); + attrs['disabled'] = this._testBoolean(value.disabled); } if (value.overlay) { - attrs.overlay = this._testBoolean(value.overlay); + attrs['overlay'] = this._testBoolean(value.overlay); } if (value.minCharForSearch) { - attrs.minCharForSearch = value.minCharForSearch; + attrs['minCharForSearch'] = value.minCharForSearch; } if (value.minCountForSearch) { - attrs.minCountForSearch = value.minCountForSearch; + attrs['minCountForSearch'] = value.minCountForSearch; } if (value.displaySearchStatus) { - attrs.displaySearchStatus = value.displaySearchStatus; + attrs['displaySearchStatus'] = value.displaySearchStatus; } if (value.placeholder) { - attrs.placeholder = value.placeholder; + attrs['placeholder'] = value.placeholder; } if (value.search) { - attrs.customSearchEnabled = this._testBoolean(value.search); + attrs['customSearchEnabled'] = this._testBoolean(value.search); } if (value.multiple) { - attrs.multiple = this._testBoolean(value.multiple); + attrs['multiple'] = this._testBoolean(value.multiple); } else { if (value.resettable) { - attrs.resettable = this._testBoolean(value.resettable); + attrs['resettable'] = this._testBoolean(value.resettable); } if (value.resetSelectedValue) { - attrs.resetSelectedValue = value.resetSelectedValue; + attrs['resetSelectedValue'] = value.resetSelectedValue; } } if (value.autoCreate) { - attrs.autoCreate = this._testBoolean(value.autoCreate); + attrs['autoCreate'] = this._testBoolean(value.autoCreate); } if (value.limitSelection) { - attrs.limitSelection = value.limitSelection; + attrs['limitSelection'] = value.limitSelection; } if (value.hideSelectedItems) { - attrs.hideSelectedItems = this._testBoolean(value.hideSelectedItems); + attrs['hideSelectedItems'] = this._testBoolean(value.hideSelectedItems); } if (value.resultMaxHeight) { - attrs.resultMaxHeight = value.resultMaxHeight; + attrs['resultMaxHeight'] = value.resultMaxHeight; } if (value.listPosition) { - attrs.listPosition = value.listPosition; + attrs['listPosition'] = value.listPosition; } if (value.infiniteScroll) { - attrs.infiniteScroll = this._testBoolean(value.infiniteScroll); + attrs['infiniteScroll'] = this._testBoolean(value.infiniteScroll); } if (value.infiniteScrollDistance) { - attrs.infiniteScrollDistance = value.infiniteScrollDistance; + attrs['infiniteScrollDistance'] = value.infiniteScrollDistance; } if (value.infiniteScrollThrottle) { - attrs.infiniteScrollThrottle = value.infiniteScrollThrottle; + attrs['infiniteScrollThrottle'] = value.infiniteScrollThrottle; } if (value.grid) { - attrs.grid = value.grid; + attrs['grid'] = value.grid; } if (value.styleMode) { - attrs.styleMode = value.styleMode; + attrs['styleMode'] = value.styleMode; } if (value.noResultMessage) { - attrs.noResultMessage = value.noResultMessage; + attrs['noResultMessage'] = value.noResultMessage; } if (value.maxResultsMessage) { - attrs.maxResultsMessage = value.maxResultsMessage; + attrs['maxResultsMessage'] = value.maxResultsMessage; } if (value.maxResults) { - attrs.maxResults = value.maxResults; + attrs['maxResults'] = value.maxResults; } if (value.noLabelTemplate) { - attrs.noLabelTemplate = this._testBoolean(value.noLabelTemplate); + attrs['noLabelTemplate'] = this._testBoolean(value.noLabelTemplate); } if (value.noLabelTemplate) { - attrs.noLabelTemplate = this._testBoolean(value.noLabelTemplate); + attrs['noLabelTemplate'] = this._testBoolean(value.noLabelTemplate); } if (value.selectionOverrideFunction) { attrs['[selectionOverride]'] = 'selectionOverride'; } else if (value.selectionOverrideString) { - attrs.selectionOverride = value.selectionOverrideString; + attrs['selectionOverride'] = value.selectionOverrideString; } if (value.selectionNoWrap) { - attrs.selectionNoWrap = this._testBoolean(value.selectionNoWrap); + attrs['selectionNoWrap'] = this._testBoolean(value.selectionNoWrap); } if (value.showSelectAll) { - attrs.showSelectAll = this._testBoolean(value.showSelectAll); + attrs['showSelectAll'] = this._testBoolean(value.showSelectAll); if (value.removeAllText) { - attrs.removeAllText = this._testBoolean(value.removeAllText); + attrs['removeAllText'] = this._testBoolean(value.removeAllText); } if (value.removeAllText) { - attrs.selectAllText = this._testBoolean(value.selectAllText); + attrs['selectAllText'] = this._testBoolean(value.selectAllText); } } @@ -419,20 +416,20 @@ export class AppGenComponent implements AfterContentInit { try { this.data = JSON.parse(value); } catch (error) { - this.jsonError = this._parseJsonError(value, error); + this.jsonError = this._parseJsonError(value, error as Error); } } private _parseJsonError(value: string, error: Error): string { let returnMessage = ''; - const message = error.message.match(/[^\n]+/)[0]; + const message = error.message.match(/[^\n]+/)![0]; const browser = Bowser.getParser(window.navigator.userAgent).getResult(); if (browser.browser.name === 'Chrome') { if (message.match(/at position/)) { - const position = parseInt(message.match(/at position (\d+)/)[1], 10); + const position = parseInt(message.match(/at position (\d+)/)![1], 10); const lines = value.split(/\n/); let l = 1; for (const line of lines) { @@ -450,7 +447,7 @@ export class AppGenComponent implements AfterContentInit { } } else if (browser.browser.name === 'Firefox') { if (message.match(/at line/)) { - const [, line, column] = message.match(/at line (\d+) column (\d+)/); + const [, line, column] = message.match(/at line (\d+) column (\d+)/)!; const lines = value.split(/\n/); if (lines[+line - 1]) { returnMessage = @@ -468,7 +465,7 @@ export class AppGenComponent implements AfterContentInit { return returnMessage; } - private _testBoolean(value: any) { + private _testBoolean(value: any): null | undefined { return value ? null : undefined; } -} +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 722b599..9a21ccb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,13 @@ "esModuleInterop": true, "sourceMap": true, "declaration": false, + "skipLibCheck": true, + "strict": true, "moduleResolution": "node", + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitOverride": true, "module": "ES2022", "experimentalDecorators": true, "target": "ES2022", @@ -18,7 +24,12 @@ }, "useDefineForClassFields": false }, - + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + }, "include": ["src/**/*"], "exclude": ["node_modules"] }