Moving ngx-bootrap-multiselect to the project
This commit is contained in:
		
							parent
							
								
									6ead907e08
								
							
						
					
					
						commit
						5867c79a1f
					
				
							
								
								
									
										22
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										22
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -36,7 +36,6 @@ | ||||
|         "echarts": "~5.3.2", | ||||
|         "express": "^4.17.1", | ||||
|         "lightweight-charts": "~3.8.0", | ||||
|         "ngx-bootrap-multiselect": "^2.0.0", | ||||
|         "ngx-echarts": "8.0.1", | ||||
|         "ngx-infinite-scroll": "^10.0.1", | ||||
|         "qrcode": "1.5.0", | ||||
| @ -12788,19 +12787,6 @@ | ||||
|       "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", | ||||
|       "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" | ||||
|     }, | ||||
|     "node_modules/ngx-bootrap-multiselect": { | ||||
|       "version": "2.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/ngx-bootrap-multiselect/-/ngx-bootrap-multiselect-2.0.0.tgz", | ||||
|       "integrity": "sha512-GV/2MigCS5oi6P+zWtFSmq1TLWW1kcKsJNAXLP3hHXxmY3HgMKeUPk57o3T+YHje73JRp5reXMhEIlYuoOmoRg==", | ||||
|       "dependencies": { | ||||
|         "tslib": "^2.0.0" | ||||
|       }, | ||||
|       "peerDependencies": { | ||||
|         "@angular/common": "^10.0.6", | ||||
|         "@angular/core": "^10.0.6", | ||||
|         "@angular/forms": "^10.0.6" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/ngx-echarts": { | ||||
|       "version": "8.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/ngx-echarts/-/ngx-echarts-8.0.1.tgz", | ||||
| @ -27418,14 +27404,6 @@ | ||||
|       "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", | ||||
|       "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" | ||||
|     }, | ||||
|     "ngx-bootrap-multiselect": { | ||||
|       "version": "2.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/ngx-bootrap-multiselect/-/ngx-bootrap-multiselect-2.0.0.tgz", | ||||
|       "integrity": "sha512-GV/2MigCS5oi6P+zWtFSmq1TLWW1kcKsJNAXLP3hHXxmY3HgMKeUPk57o3T+YHje73JRp5reXMhEIlYuoOmoRg==", | ||||
|       "requires": { | ||||
|         "tslib": "^2.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "ngx-echarts": { | ||||
|       "version": "8.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/ngx-echarts/-/ngx-echarts-8.0.1.tgz", | ||||
|  | ||||
| @ -90,7 +90,6 @@ | ||||
|     "echarts": "~5.3.2", | ||||
|     "express": "^4.17.1", | ||||
|     "lightweight-charts": "~3.8.0", | ||||
|     "ngx-bootrap-multiselect": "^2.0.0", | ||||
|     "ngx-echarts": "8.0.1", | ||||
|     "ngx-infinite-scroll": "^10.0.1", | ||||
|     "qrcode": "1.5.0", | ||||
|  | ||||
| @ -3,7 +3,7 @@ | ||||
| 
 | ||||
|   <div class="d-block float-right" id="filter"> | ||||
|     <form [formGroup]="radioGroupForm"> | ||||
|       <ngx-bootrap-multiselect [options]="txTypeOptions" [settings]="txTypeDropdownSettings" [texts]="txTypeDropdownTexts" formControlName="txTypes"></ngx-bootrap-multiselect> | ||||
|       <ngx-bootstrap-multiselect [options]="txTypeOptions" [settings]="txTypeDropdownSettings" [texts]="txTypeDropdownTexts" formControlName="txTypes"></ngx-bootstrap-multiselect> | ||||
|     </form> | ||||
|   </div> | ||||
| 
 | ||||
|  | ||||
| @ -7,7 +7,7 @@ import { BisqApiService } from '../bisq-api.service'; | ||||
| import { SeoService } from 'src/app/services/seo.service'; | ||||
| import { FormGroup, FormBuilder } from '@angular/forms'; | ||||
| import { Router, ActivatedRoute } from '@angular/router'; | ||||
| import { IMultiSelectOption, IMultiSelectSettings, IMultiSelectTexts } from 'ngx-bootrap-multiselect'; | ||||
| import { IMultiSelectOption, IMultiSelectSettings, IMultiSelectTexts } from 'src/app/components/ngx-bootstrap-multiselect/types' | ||||
| import { WebsocketService } from 'src/app/services/websocket.service'; | ||||
| 
 | ||||
| @Component({ | ||||
|  | ||||
| @ -1,7 +1,6 @@ | ||||
| import { NgModule } from '@angular/core'; | ||||
| import { BisqRoutingModule } from './bisq.routing.module'; | ||||
| import { SharedModule } from '../shared/shared.module'; | ||||
| import { NgxBootstrapMultiselectModule } from 'ngx-bootrap-multiselect'; | ||||
| 
 | ||||
| import { LightweightChartsComponent } from './lightweight-charts/lightweight-charts.component'; | ||||
| import { LightweightChartsAreaComponent } from './lightweight-charts-area/lightweight-charts-area.component'; | ||||
| @ -24,6 +23,10 @@ import { BisqStatsComponent } from './bisq-stats/bisq-stats.component'; | ||||
| import { BsqAmountComponent } from './bsq-amount/bsq-amount.component'; | ||||
| import { BisqTradesComponent } from './bisq-trades/bisq-trades.component'; | ||||
| import { CommonModule } from '@angular/common'; | ||||
| import { AutofocusDirective } from '../components/ngx-bootstrap-multiselect/autofocus.directive'; | ||||
| import { MultiSelectSearchFilter } from '../components/ngx-bootstrap-multiselect/search-filter.pipe'; | ||||
| import { OffClickDirective } from '../components/ngx-bootstrap-multiselect/off-click.directive'; | ||||
| import { NgxDropdownMultiselectComponent } from '../components/ngx-bootstrap-multiselect/ngx-bootstrap-multiselect.component'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|   declarations: [ | ||||
| @ -44,16 +47,21 @@ import { CommonModule } from '@angular/common'; | ||||
|     BisqMarketComponent, | ||||
|     BisqTradesComponent, | ||||
|     BisqMainDashboardComponent, | ||||
|     NgxDropdownMultiselectComponent, | ||||
|     AutofocusDirective, | ||||
|     OffClickDirective, | ||||
|   ], | ||||
|   imports: [ | ||||
|     CommonModule, | ||||
|     BisqRoutingModule, | ||||
|     SharedModule, | ||||
|     FontAwesomeModule, | ||||
|     NgxBootstrapMultiselectModule, | ||||
|   ], | ||||
|   providers: [ | ||||
|     BisqApiService, | ||||
|     MultiSelectSearchFilter, | ||||
|     AutofocusDirective, | ||||
|     OffClickDirective, | ||||
|   ] | ||||
| }) | ||||
| export class BisqModule { | ||||
|  | ||||
| @ -0,0 +1,41 @@ | ||||
| import { Directive, ElementRef, Host, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; | ||||
| 
 | ||||
| @Directive({ | ||||
|   selector: '[ssAutofocus]' | ||||
| }) | ||||
| export class AutofocusDirective implements OnInit, OnChanges { | ||||
| 
 | ||||
|   /** | ||||
|    * Will set focus if set to falsy value or not set at all | ||||
|    */ | ||||
|   @Input() ssAutofocus: any; | ||||
| 
 | ||||
|   get element(): { focus?: Function } { | ||||
|     return this.elemRef.nativeElement; | ||||
|   } | ||||
| 
 | ||||
|   constructor( | ||||
|     @Host() private elemRef: ElementRef, | ||||
|   ) { } | ||||
| 
 | ||||
|   ngOnInit() { | ||||
|     this.focus(); | ||||
|   } | ||||
| 
 | ||||
|   ngOnChanges(changes: SimpleChanges) { | ||||
|     const ssAutofocusChange = changes.ssAutofocus; | ||||
| 
 | ||||
|     if (ssAutofocusChange && !ssAutofocusChange.isFirstChange()) { | ||||
|       this.focus(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   focus() { | ||||
|     if (this.ssAutofocus) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     this.element.focus && this.element.focus(); | ||||
|   } | ||||
| 
 | ||||
| } | ||||
| @ -0,0 +1,48 @@ | ||||
| a { | ||||
|   outline: none !important; | ||||
| } | ||||
| 
 | ||||
| .dropdown-inline { | ||||
|   display: inline-block; | ||||
| } | ||||
| 
 | ||||
| .dropdown-toggle .caret { | ||||
|   margin-left: 4px; | ||||
|   white-space: nowrap; | ||||
|   display: inline-block; | ||||
| } | ||||
| 
 | ||||
| .chunkydropdown-menu { | ||||
|   min-width: 20em; | ||||
| } | ||||
| 
 | ||||
| .chunkyrow { | ||||
|   line-height: 2; | ||||
|   margin-left: 1em; | ||||
|   font-size: 2em; | ||||
| } | ||||
| 
 | ||||
| .slider { | ||||
|   width:3.8em; | ||||
|   height:3.8em; | ||||
|   display:block; | ||||
|   -webkit-transition: all 0.125s linear; | ||||
|   -moz-transition: all 0.125s linear; | ||||
|   -o-transition: all 0.125s linear; | ||||
|   transition: all 0.125s linear; | ||||
|   margin-left: 0.125em; | ||||
|   margin-top: auto; | ||||
| } | ||||
| 
 | ||||
| .slideron { | ||||
|   margin-left: 1.35em; | ||||
| } | ||||
| 
 | ||||
| .content_wrapper{ | ||||
|   display: table-cell; | ||||
|   vertical-align: middle; | ||||
| } | ||||
| 
 | ||||
| .search-container { | ||||
|   padding: 0px 5px 5px 5px; | ||||
| } | ||||
| @ -0,0 +1,72 @@ | ||||
| <div *ngIf="options" class="dropdown" [ngClass]="settings.containerClasses" [class.open]="isVisible" (offClick)="clickedOutside()"> | ||||
|   <button type="button" class="dropdown-toggle" [ngClass]="settings.buttonClasses" (click)="toggleDropdown($event)" [disabled]="disabled" | ||||
|     [ssAutofocus]="!focusBack"> | ||||
|     {{ title }} | ||||
|     <span class="caret"></span> | ||||
|   </button> | ||||
|   <div #scroller *ngIf="isVisible" class="dropdown-menu" [ngClass]="{'chunkydropdown-menu': settings.checkedStyle == 'visual' }" | ||||
|     (scroll)="settings.isLazyLoad ? checkScrollPosition($event) : null" (wheel)="settings.stopScrollPropagation ? checkScrollPropagation($event, scroller) : null" | ||||
|     [class.pull-right]="settings.pullRight" [class.dropdown-menu-right]="settings.pullRight" [style.max-height]="settings.maxHeight" | ||||
|     style="display: block; height: auto; overflow-y: auto;" (keydown.tab)="focusItem(1, $event)" (keydown.shift.tab)="focusItem(-1, $event)"> | ||||
|     <div class="input-group search-container" *ngIf="settings.enableSearch && (renderFilteredOptions.length > 1 || filterControl.value.length > 0)"> | ||||
|       <div class="input-group-prepend"> | ||||
|         <span class="input-group-text" id="basic-addon1"> | ||||
|           <i class="fa fa-search" aria-hidden="true"></i> | ||||
|         </span> | ||||
|       </div> | ||||
|       <input type="text" class="form-control" ssAutofocus [formControl]="filterControl" [placeholder]="texts.searchPlaceholder" | ||||
|         class="form-control"> | ||||
|       <div class="input-group-append" *ngIf="filterControl.value.length>0"> | ||||
|         <button class="btn btn-default btn-secondary" type="button" (click)="clearSearch($event)"> | ||||
|           <i class="fa fa-times"></i> | ||||
|         </button> | ||||
|       </div> | ||||
|     </div> | ||||
|     <a role="menuitem" href="javascript:;" tabindex="-1" class="dropdown-item check-control check-control-check" *ngIf="settings.showCheckAll && !disabledSelection && renderFilteredOptions.length > 1" | ||||
|       (click)="checkAll()"> | ||||
|       <span style="width: 16px;"><span [ngClass]="{'glyphicon glyphicon-ok': settings.checkedStyle !== 'fontawesome','fa fa-check': settings.checkedStyle === 'fontawesome'}"></span></span> | ||||
|       {{ texts.checkAll }} | ||||
|     </a> | ||||
|     <a role="menuitem" href="javascript:;" tabindex="-1" class="dropdown-item check-control check-control-uncheck" *ngIf="settings.showUncheckAll && !disabledSelection && renderFilteredOptions.length > 1" | ||||
|       (click)="uncheckAll()"> | ||||
|       <span style="width: 16px;"><span [ngClass]="{'glyphicon glyphicon-remove': settings.checkedStyle !== 'fontawesome','fa fa-times': settings.checkedStyle === 'fontawesome'}"></span></span> | ||||
|       {{ texts.uncheckAll }} | ||||
|     </a> | ||||
|     <a *ngIf="settings.showCheckAll || settings.showUncheckAll" href="javascript:;" class="dropdown-divider divider"></a> | ||||
|     <a *ngIf="!renderItems" href="javascript:;" class="dropdown-item empty">{{ texts.searchNoRenderText }}</a> | ||||
|     <a *ngIf="renderItems && !renderFilteredOptions.length" href="javascript:;" class="dropdown-item empty">{{ texts.searchEmptyResult }}</a> | ||||
|     <a class="dropdown-item" href="javascript:;" *ngFor="let option of renderFilteredOptions; trackBy: trackById" [class.active]="isSelected(option)" | ||||
|       [ngStyle]="getItemStyle(option)" [ngClass]="option.classes" [class.dropdown-header]="option.isLabel" [ssAutofocus]="option !== focusedItem" | ||||
|       tabindex="-1" (click)="setSelected($event, option)" (keydown.space)="setSelected($event, option)" (keydown.enter)="setSelected($event, option)"> | ||||
|       <span *ngIf="!option.isLabel; else label" role="menuitem" tabindex="-1" [style.padding-left]="this.parents.length>0&&this.parents.indexOf(option.id)<0&&'30px'" | ||||
|         [ngStyle]="getItemStyleSelectionDisabled()"> | ||||
|         <ng-container [ngSwitch]="settings.checkedStyle"> | ||||
|           <input *ngSwitchCase="'checkboxes'" type="checkbox" [checked]="isSelected(option)" (click)="preventCheckboxCheck($event, option)" | ||||
|             [disabled]="isCheckboxDisabled(option)" [ngStyle]="getItemStyleSelectionDisabled()" /> | ||||
|           <span *ngSwitchCase="'glyphicon'" style="width: 16px;" class="glyphicon" [class.glyphicon-ok]="isSelected(option)" [class.glyphicon-lock]="isCheckboxDisabled(option)"></span> | ||||
|           <span *ngSwitchCase="'fontawesome'" style="width: 16px;display: inline-block;"> | ||||
|             <span *ngIf="isSelected(option)"><i class="fa fa-check" aria-hidden="true"></i></span> | ||||
|             <span *ngIf="isCheckboxDisabled(option)"><i class="fa fa-lock" aria-hidden="true"></i></span> | ||||
|           </span> | ||||
|           <span *ngSwitchCase="'visual'" style="display:block;float:left; border-radius: 0.2em; border: 0.1em solid rgba(44, 44, 44, 0.63);background:rgba(0, 0, 0, 0.1);width: 5.5em;"> | ||||
|             <div class="slider" [ngClass]="{'slideron': isSelected(option)}"> | ||||
|               <img *ngIf="option.image != null" [src]="option.image" style="height: 100%; width: 100%; object-fit: contain" /> | ||||
|               <div *ngIf="option.image == null" style="height: 100%; width: 100%;text-align: center; display: table; background-color:rgba(0, 0, 0, 0.74)"> | ||||
|                 <div class="content_wrapper"> | ||||
|                   <span style="font-size:3em;color:white" class="glyphicon glyphicon-eye-close"></span> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </span> | ||||
|         </ng-container> | ||||
|         <span [ngClass]="{'chunkyrow': settings.checkedStyle == 'visual' }" [class.disabled]="isCheckboxDisabled(option)" [ngClass]="settings.itemClasses" | ||||
|           [style.font-weight]="this.parents.indexOf(option.id)>=0?'bold':'normal'"> | ||||
|           {{ option.name }} | ||||
|         </span> | ||||
|       </span> | ||||
|       <ng-template #label> | ||||
|         <span [class.disabled]="isCheckboxDisabled(option)">{{ option.name }}</span> | ||||
|       </ng-template> | ||||
|     </a> | ||||
|   </div> | ||||
| </div> | ||||
| @ -0,0 +1,710 @@ | ||||
| import { | ||||
|   ChangeDetectionStrategy, | ||||
|   ChangeDetectorRef, | ||||
|   Component, | ||||
|   DoCheck, | ||||
|   EventEmitter, | ||||
|   forwardRef, | ||||
|   Input, | ||||
|   IterableDiffers, | ||||
|   OnChanges, | ||||
|   OnDestroy, | ||||
|   OnInit, | ||||
|   Output, | ||||
|   SimpleChanges, | ||||
| } from '@angular/core'; | ||||
| 
 | ||||
| import { | ||||
|   AbstractControl, | ||||
|   ControlValueAccessor, | ||||
|   FormBuilder, | ||||
|   FormControl, | ||||
|   NG_VALUE_ACCESSOR, | ||||
|   Validator, | ||||
| } from '@angular/forms'; | ||||
| 
 | ||||
| import { takeUntil } from 'rxjs/operators'; | ||||
| import { MultiSelectSearchFilter } from './search-filter.pipe'; | ||||
| import { IMultiSelectOption, IMultiSelectSettings, IMultiSelectTexts, } from './types'; | ||||
| import { Subject, Observable } from 'rxjs'; | ||||
| 
 | ||||
| const MULTISELECT_VALUE_ACCESSOR: any = { | ||||
|   provide: NG_VALUE_ACCESSOR, | ||||
|   useExisting: forwardRef(() => NgxDropdownMultiselectComponent), | ||||
|   multi: true, | ||||
| }; | ||||
| 
 | ||||
| // tslint:disable-next-line: no-conflicting-lifecycle
 | ||||
| @Component({ | ||||
|   selector: 'ngx-bootstrap-multiselect', | ||||
|   templateUrl: './ngx-bootstrap-multiselect.component.html', | ||||
|   styleUrls: ['./ngx-bootstrap-multiselect.component.css'], | ||||
|   providers: [MULTISELECT_VALUE_ACCESSOR, MultiSelectSearchFilter], | ||||
|   changeDetection: ChangeDetectionStrategy.OnPush | ||||
| }) | ||||
| export class NgxDropdownMultiselectComponent implements OnInit, | ||||
|   OnChanges, | ||||
|   DoCheck, | ||||
|   OnDestroy, | ||||
|   ControlValueAccessor, | ||||
|   Validator { | ||||
| 
 | ||||
|   private localIsVisible = false; | ||||
|   private workerDocClicked = false; | ||||
| 
 | ||||
|   filterControl: FormControl = this.fb.control(''); | ||||
| 
 | ||||
|   @Input() options: Array<IMultiSelectOption>; | ||||
|   @Input() settings: IMultiSelectSettings; | ||||
|   @Input() texts: IMultiSelectTexts; | ||||
|   @Input() disabled = false; | ||||
|   @Input() disabledSelection = false; | ||||
|   @Input() searchFunction: (str: string) => RegExp = this._escapeRegExp; | ||||
| 
 | ||||
|   @Output() selectionLimitReached = new EventEmitter(); | ||||
|   @Output() dropdownClosed = new EventEmitter(); | ||||
|   @Output() dropdownOpened = new EventEmitter(); | ||||
|   @Output() added = new EventEmitter(); | ||||
|   @Output() removed = new EventEmitter(); | ||||
|   @Output() lazyLoad = new EventEmitter(); | ||||
|   @Output() filter: Observable<string> = this.filterControl.valueChanges; | ||||
| 
 | ||||
|   get focusBack(): boolean { | ||||
|     return this.settings.focusBack && this._focusBack; | ||||
|   } | ||||
| 
 | ||||
|   destroyed$ = new Subject<any>(); | ||||
| 
 | ||||
|   filteredOptions: IMultiSelectOption[] = []; | ||||
|   lazyLoadOptions: IMultiSelectOption[] = []; | ||||
|   renderFilteredOptions: IMultiSelectOption[] = []; | ||||
|   model: any[] = []; | ||||
|   prevModel: any[] = []; | ||||
|   parents: any[]; | ||||
|   title: string; | ||||
|   differ: any; | ||||
|   numSelected = 0; | ||||
|   set isVisible(val: boolean) { | ||||
|     this.localIsVisible = val; | ||||
|     this.workerDocClicked = val ? false : this.workerDocClicked; | ||||
|   } | ||||
|   get isVisible(): boolean { | ||||
|     return this.localIsVisible; | ||||
|   } | ||||
|   renderItems = true; | ||||
|   checkAllSearchRegister = new Set(); | ||||
|   checkAllStatus = false; | ||||
|   loadedValueIds = []; | ||||
|   _focusBack = false; | ||||
|   focusedItem: IMultiSelectOption | undefined; | ||||
| 
 | ||||
|   defaultSettings: IMultiSelectSettings = { | ||||
|     closeOnClickOutside: true, | ||||
|     pullRight: false, | ||||
|     enableSearch: false, | ||||
|     searchRenderLimit: 0, | ||||
|     searchRenderAfter: 1, | ||||
|     searchMaxLimit: 0, | ||||
|     searchMaxRenderedItems: 0, | ||||
|     checkedStyle: 'checkboxes', | ||||
|     buttonClasses: 'btn btn-primary dropdown-toggle', | ||||
|     containerClasses: 'dropdown-inline', | ||||
|     selectionLimit: 0, | ||||
|     minSelectionLimit: 0, | ||||
|     closeOnSelect: false, | ||||
|     autoUnselect: false, | ||||
|     showCheckAll: false, | ||||
|     showUncheckAll: false, | ||||
|     fixedTitle: false, | ||||
|     dynamicTitleMaxItems: 3, | ||||
|     maxHeight: '300px', | ||||
|     isLazyLoad: false, | ||||
|     stopScrollPropagation: false, | ||||
|     loadViewDistance: 1, | ||||
|     selectAddedValues: false, | ||||
|     ignoreLabels: false, | ||||
|     maintainSelectionOrderInTitle: false, | ||||
|     focusBack: true | ||||
|   }; | ||||
|   defaultTexts: IMultiSelectTexts = { | ||||
|     checkAll: 'Select all', | ||||
|     uncheckAll: 'Unselect all', | ||||
|     checked: 'selected', | ||||
|     checkedPlural: 'selected', | ||||
|     searchPlaceholder: 'Search...', | ||||
|     searchEmptyResult: 'Nothing found...', | ||||
|     searchNoRenderText: 'Type in search box to see results...', | ||||
|     defaultTitle: 'Select', | ||||
|     allSelected: 'All selected', | ||||
|   }; | ||||
| 
 | ||||
|   get searchLimit(): number | undefined { | ||||
|     return this.settings.searchRenderLimit; | ||||
|   } | ||||
| 
 | ||||
|   get searchRenderAfter(): number | undefined { | ||||
|     return this.settings.searchRenderAfter; | ||||
|   } | ||||
| 
 | ||||
|   get searchLimitApplied(): boolean { | ||||
|     return this.searchLimit > 0 && this.options.length > this.searchLimit; | ||||
|   } | ||||
| 
 | ||||
|   constructor( | ||||
|     private fb: FormBuilder, | ||||
|     private searchFilter: MultiSelectSearchFilter, | ||||
|     differs: IterableDiffers, | ||||
|     private cdRef: ChangeDetectorRef | ||||
|   ) { | ||||
|     this.differ = differs.find([]).create(null); | ||||
|     this.settings = this.defaultSettings; | ||||
|     this.texts = this.defaultTexts; | ||||
|   } | ||||
| 
 | ||||
|   clickedOutside(): void { | ||||
|     if (!this.isVisible || !this.settings.closeOnClickOutside) { return; } | ||||
| 
 | ||||
|     this.isVisible = false; | ||||
|     this._focusBack = true; | ||||
|     this.dropdownClosed.emit(); | ||||
|   } | ||||
| 
 | ||||
|   getItemStyle(option: IMultiSelectOption): any { | ||||
|     const style = {}; | ||||
|     if (!option.isLabel) { | ||||
|       style['cursor'] = 'pointer'; | ||||
|     } | ||||
|     if (option.disabled) { | ||||
|       style['cursor'] = 'default'; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   getItemStyleSelectionDisabled(): any { | ||||
|     if (this.disabledSelection) { | ||||
|       return { cursor: 'default' }; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     this.title = this.texts.defaultTitle || ''; | ||||
| 
 | ||||
|     this.filterControl.valueChanges.pipe(takeUntil(this.destroyed$)).subscribe(() => { | ||||
|       this.updateRenderItems(); | ||||
|       if (this.settings.isLazyLoad) { | ||||
|         this.load(); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   ngOnChanges(changes: SimpleChanges) { | ||||
|     if (changes['options']) { | ||||
|       this.options = this.options || []; | ||||
|       this.parents = this.options | ||||
|         .filter(option => typeof option.parentId === 'number') | ||||
|         .map(option => option.parentId); | ||||
|       this.updateRenderItems(); | ||||
| 
 | ||||
|       if ( | ||||
|         this.settings.isLazyLoad && | ||||
|         this.settings.selectAddedValues && | ||||
|         this.loadedValueIds.length === 0 | ||||
|       ) { | ||||
|         this.loadedValueIds = this.loadedValueIds.concat( | ||||
|           changes.options.currentValue.map(value => value.id) | ||||
|         ); | ||||
|       } | ||||
|       if ( | ||||
|         this.settings.isLazyLoad && | ||||
|         this.settings.selectAddedValues && | ||||
|         changes.options.previousValue | ||||
|       ) { | ||||
|         const addedValues = changes.options.currentValue.filter( | ||||
|           value => this.loadedValueIds.indexOf(value.id) === -1 | ||||
|         ); | ||||
|         this.loadedValueIds.concat(addedValues.map(value => value.id)); | ||||
|         if (this.checkAllStatus) { | ||||
|           this.addChecks(addedValues); | ||||
|         } else if (this.checkAllSearchRegister.size > 0) { | ||||
|           this.checkAllSearchRegister.forEach((searchValue: string) => | ||||
|             this.addChecks(this.applyFilters(addedValues, searchValue)) | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       if (this.texts) { | ||||
|         this.updateTitle(); | ||||
|       } | ||||
| 
 | ||||
|       this.fireModelChange(); | ||||
|     } | ||||
| 
 | ||||
|     if (changes['settings']) { | ||||
|       this.settings = { ...this.defaultSettings, ...this.settings }; | ||||
|     } | ||||
| 
 | ||||
|     if (changes['texts']) { | ||||
|       this.texts = { ...this.defaultTexts, ...this.texts }; | ||||
|       if (!changes['texts'].isFirstChange()) { this.updateTitle(); } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   ngOnDestroy() { | ||||
|     this.destroyed$.next(false); | ||||
|   } | ||||
| 
 | ||||
|   updateRenderItems() { | ||||
|     this.renderItems = | ||||
|       !this.searchLimitApplied || | ||||
|       this.filterControl.value.length >= this.searchRenderAfter; | ||||
|     this.filteredOptions = this.applyFilters( | ||||
|       this.options, | ||||
|       this.settings.isLazyLoad ? '' : this.filterControl.value | ||||
|     ); | ||||
|     this.renderFilteredOptions = this.renderItems ? this.filteredOptions : []; | ||||
|     this.focusedItem = undefined; | ||||
|   } | ||||
| 
 | ||||
|   applyFilters(options: IMultiSelectOption[], value: string): IMultiSelectOption[] { | ||||
|     return this.searchFilter.transform( | ||||
|       options, | ||||
|       value, | ||||
|       this.settings.searchMaxLimit, | ||||
|       this.settings.searchMaxRenderedItems, | ||||
|       this.searchFunction | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   fireModelChange(): void { | ||||
|     if (this.model != this.prevModel) { | ||||
|       this.prevModel = this.model; | ||||
|       this.onModelChange(this.model); | ||||
|       this.onModelTouched(); | ||||
|       this.cdRef.markForCheck(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   onModelChange: Function = (_: any) => { }; | ||||
|   onModelTouched: Function = () => { }; | ||||
| 
 | ||||
|   writeValue(value: any): void { | ||||
|     if (value !== undefined && value !== null) { | ||||
|       this.model = Array.isArray(value) ? value : [value]; | ||||
|       this.ngDoCheck(); | ||||
|     } else { | ||||
|       this.model = []; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   registerOnChange(fn: Function): void { | ||||
|     this.onModelChange = fn; | ||||
|   } | ||||
| 
 | ||||
|   registerOnTouched(fn: Function): void { | ||||
|     this.onModelTouched = fn; | ||||
|   } | ||||
| 
 | ||||
|   setDisabledState(isDisabled: boolean) { | ||||
|     this.disabled = isDisabled; | ||||
|   } | ||||
| 
 | ||||
|   ngDoCheck() { | ||||
|     const changes = this.differ.diff(this.model); | ||||
|     if (changes) { | ||||
|       this.updateNumSelected(); | ||||
|       this.updateTitle(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   validate(_c: AbstractControl): { [key: string]: any } { | ||||
|     if (this.model && this.model.length) { | ||||
|       return { | ||||
|         required: { | ||||
|           valid: false | ||||
|         } | ||||
|       }; | ||||
|     } | ||||
| 
 | ||||
|     if (this.options.filter(o => this.model.indexOf(o.id) && !o.disabled).length === 0) { | ||||
|       return { | ||||
|         selection: { | ||||
|           valid: false | ||||
|         } | ||||
|       }; | ||||
|     } | ||||
| 
 | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   registerOnValidatorChange(_fn: () => void): void { | ||||
|     throw new Error('Method not implemented.'); | ||||
|   } | ||||
| 
 | ||||
|   clearSearch(event: Event) { | ||||
|     this.maybeStopPropagation(event); | ||||
|     this.filterControl.setValue(''); | ||||
|   } | ||||
| 
 | ||||
|   toggleDropdown(e?: Event) { | ||||
|     if (this.isVisible) { | ||||
|       this._focusBack = true; | ||||
|     } | ||||
| 
 | ||||
|     this.isVisible = !this.isVisible; | ||||
|     this.isVisible ? this.dropdownOpened.emit() : this.dropdownClosed.emit(); | ||||
|     this.focusedItem = undefined; | ||||
|   } | ||||
| 
 | ||||
|   closeDropdown(e?: Event) { | ||||
|     this.isVisible = true; | ||||
|     this.toggleDropdown(e); | ||||
|   } | ||||
| 
 | ||||
|   isSelected(option: IMultiSelectOption): boolean { | ||||
|     return this.model && this.model.indexOf(option.id) > -1; | ||||
|   } | ||||
| 
 | ||||
|   setSelected(_event: Event, option: IMultiSelectOption) { | ||||
|     if (option.isLabel) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     if (option.disabled) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     if (this.disabledSelection) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     setTimeout(() => { | ||||
|       this.maybeStopPropagation(_event); | ||||
|       this.maybePreventDefault(_event); | ||||
|       const index = this.model.indexOf(option.id); | ||||
|       const isAtSelectionLimit = | ||||
|         this.settings.selectionLimit > 0 && | ||||
|         this.model.length >= this.settings.selectionLimit; | ||||
|       const removeItem = (idx, id): void => { | ||||
|         this.model.splice(idx, 1); | ||||
|         this.removed.emit(id); | ||||
|         if ( | ||||
|           this.settings.isLazyLoad && | ||||
|           this.lazyLoadOptions.some(val => val.id === id) | ||||
|         ) { | ||||
|           this.lazyLoadOptions.splice( | ||||
|             this.lazyLoadOptions.indexOf( | ||||
|               this.lazyLoadOptions.find(val => val.id === id) | ||||
|             ), | ||||
|             1 | ||||
|           ); | ||||
|         } | ||||
|       }; | ||||
| 
 | ||||
|       if (index > -1) { | ||||
|         if ( | ||||
|           this.settings.minSelectionLimit === undefined || | ||||
|           this.numSelected > this.settings.minSelectionLimit | ||||
|         ) { | ||||
|           removeItem(index, option.id); | ||||
|         } | ||||
|         const parentIndex = | ||||
|           option.parentId && this.model.indexOf(option.parentId); | ||||
|         if (parentIndex > -1) { | ||||
|           removeItem(parentIndex, option.parentId); | ||||
|         } else if (this.parents.indexOf(option.id) > -1) { | ||||
|           this.options | ||||
|             .filter( | ||||
|               child => | ||||
|                 this.model.indexOf(child.id) > -1 && | ||||
|                 child.parentId === option.id | ||||
|             ) | ||||
|             .forEach(child => | ||||
|               removeItem(this.model.indexOf(child.id), child.id) | ||||
|             ); | ||||
|         } | ||||
|       } else if (isAtSelectionLimit && !this.settings.autoUnselect) { | ||||
|         this.selectionLimitReached.emit(this.model.length); | ||||
|         return; | ||||
|       } else { | ||||
|         const addItem = (id): void => { | ||||
|           this.model.push(id); | ||||
|           this.added.emit(id); | ||||
|           if ( | ||||
|             this.settings.isLazyLoad && | ||||
|             !this.lazyLoadOptions.some(val => val.id === id) | ||||
|           ) { | ||||
|             this.lazyLoadOptions.push(option); | ||||
|           } | ||||
|         }; | ||||
| 
 | ||||
|         addItem(option.id); | ||||
|         if (!isAtSelectionLimit) { | ||||
|           if (option.parentId && !this.settings.ignoreLabels) { | ||||
|             const children = this.options.filter( | ||||
|               child => | ||||
|                 child.id !== option.id && child.parentId === option.parentId | ||||
|             ); | ||||
|             if (children.every(child => this.model.indexOf(child.id) > -1)) { | ||||
|               addItem(option.parentId); | ||||
|             } | ||||
|           } else if (this.parents.indexOf(option.id) > -1) { | ||||
|             const children = this.options.filter( | ||||
|               child => | ||||
|                 this.model.indexOf(child.id) < 0 && child.parentId === option.id | ||||
|             ); | ||||
|             children.forEach(child => addItem(child.id)); | ||||
|           } | ||||
|         } else { | ||||
|           removeItem(0, this.model[0]); | ||||
|         } | ||||
|       } | ||||
|       if (this.settings.closeOnSelect) { | ||||
|         this.toggleDropdown(); | ||||
|       } | ||||
|       this.model = this.model.slice(); | ||||
|       this.fireModelChange(); | ||||
| 
 | ||||
|     }, 0) | ||||
|   } | ||||
| 
 | ||||
|   updateNumSelected() { | ||||
|     this.numSelected = | ||||
|       this.model.filter(id => this.parents.indexOf(id) < 0).length || 0; | ||||
|   } | ||||
| 
 | ||||
|   updateTitle() { | ||||
|     let numSelectedOptions = this.options.length; | ||||
|     if (this.settings.ignoreLabels) { | ||||
|       numSelectedOptions = this.options.filter( | ||||
|         (option: IMultiSelectOption) => !option.isLabel | ||||
|       ).length; | ||||
|     } | ||||
|     if (this.numSelected === 0 || this.settings.fixedTitle) { | ||||
|       this.title = this.texts ? this.texts.defaultTitle : ''; | ||||
|     } else if ( | ||||
|       this.settings.displayAllSelectedText && | ||||
|       this.model.length === numSelectedOptions | ||||
|     ) { | ||||
|       this.title = this.texts ? this.texts.allSelected : ''; | ||||
|     } else if ( | ||||
|       this.settings.dynamicTitleMaxItems && | ||||
|       this.settings.dynamicTitleMaxItems >= this.numSelected | ||||
|     ) { | ||||
|       const useOptions = | ||||
|         this.settings.isLazyLoad && this.lazyLoadOptions.length | ||||
|           ? this.lazyLoadOptions | ||||
|           : this.options; | ||||
| 
 | ||||
|       let titleSelections: Array<IMultiSelectOption>; | ||||
| 
 | ||||
|       if (this.settings.maintainSelectionOrderInTitle) { | ||||
|         const optionIds = useOptions.map((selectOption: IMultiSelectOption, idx: number) => selectOption.id); | ||||
|         titleSelections = this.model | ||||
|           .map((selectedId) => optionIds.indexOf(selectedId)) | ||||
|           .filter((optionIndex) => optionIndex > -1) | ||||
|           .map((optionIndex) => useOptions[optionIndex]); | ||||
|       } else { | ||||
|         titleSelections = useOptions.filter((option: IMultiSelectOption) => this.model.indexOf(option.id) > -1); | ||||
|       } | ||||
| 
 | ||||
|       this.title = titleSelections.map((option: IMultiSelectOption) => option.name).join(', '); | ||||
|     } else { | ||||
|       this.title = | ||||
|         this.numSelected + | ||||
|         ' ' + | ||||
|         (this.numSelected === 1 | ||||
|           ? this.texts.checked | ||||
|           : this.texts.checkedPlural); | ||||
|     } | ||||
|     this.cdRef.markForCheck(); | ||||
|   } | ||||
| 
 | ||||
|   searchFilterApplied() { | ||||
|     return ( | ||||
|       this.settings.enableSearch && | ||||
|       this.filterControl.value && | ||||
|       this.filterControl.value.length > 0 | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   addChecks(options) { | ||||
|     const checkedOptions = options | ||||
|       .filter((option: IMultiSelectOption) => { | ||||
|         if ( | ||||
|           !option.disabled && | ||||
|           ( | ||||
|             this.model.indexOf(option.id) === -1 && | ||||
|             !(this.settings.ignoreLabels && option.isLabel) | ||||
|           ) | ||||
|         ) { | ||||
|           this.added.emit(option.id); | ||||
|           return true; | ||||
|         } | ||||
|         return false; | ||||
|       }) | ||||
|       .map((option: IMultiSelectOption) => option.id); | ||||
| 
 | ||||
|     this.model = this.model.concat(checkedOptions); | ||||
|   } | ||||
| 
 | ||||
|   checkAll(): void { | ||||
|     if (!this.disabledSelection) { | ||||
|       this.addChecks( | ||||
|         !this.searchFilterApplied() ? this.options : this.filteredOptions | ||||
|       ); | ||||
|       if (this.settings.isLazyLoad && this.settings.selectAddedValues) { | ||||
|         if (this.searchFilterApplied() && !this.checkAllStatus) { | ||||
|           this.checkAllSearchRegister.add(this.filterControl.value); | ||||
|         } else { | ||||
|           this.checkAllSearchRegister.clear(); | ||||
|           this.checkAllStatus = true; | ||||
|         } | ||||
|         this.load(); | ||||
|       } | ||||
|       this.fireModelChange(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   uncheckAll(): void { | ||||
|     if (!this.disabledSelection) { | ||||
|       const checkedOptions = this.model; | ||||
|       let unCheckedOptions = !this.searchFilterApplied() | ||||
|         ? this.model | ||||
|         : this.filteredOptions.map((option: IMultiSelectOption) => option.id); | ||||
|       // set unchecked options only to the ones that were checked
 | ||||
|       unCheckedOptions = checkedOptions.filter(item => unCheckedOptions.indexOf(item) > -1); | ||||
|       this.model = this.model.filter((id: number) => { | ||||
|         if ( | ||||
|           (unCheckedOptions.indexOf(id) < 0 && | ||||
|             this.settings.minSelectionLimit === undefined) || | ||||
|           unCheckedOptions.indexOf(id) < this.settings.minSelectionLimit | ||||
|         ) { | ||||
|           return true; | ||||
|         } else { | ||||
|           this.removed.emit(id); | ||||
|           return false; | ||||
|         } | ||||
|       }); | ||||
|       if (this.settings.isLazyLoad && this.settings.selectAddedValues) { | ||||
|         if (this.searchFilterApplied()) { | ||||
|           if (this.checkAllSearchRegister.has(this.filterControl.value)) { | ||||
|             this.checkAllSearchRegister.delete(this.filterControl.value); | ||||
|             this.checkAllSearchRegister.forEach(function(searchTerm) { | ||||
|               const filterOptions = this.applyFilters(this.options.filter(option => unCheckedOptions.indexOf(option.id) > -1), searchTerm); | ||||
|               this.addChecks(filterOptions); | ||||
|             }); | ||||
|           } | ||||
|         } else { | ||||
|           this.checkAllSearchRegister.clear(); | ||||
|           this.checkAllStatus = false; | ||||
|         } | ||||
|         this.load(); | ||||
|       } | ||||
|       this.fireModelChange(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   preventCheckboxCheck(event: Event, option: IMultiSelectOption): void { | ||||
|     if ( | ||||
|       option.disabled || | ||||
|       ( | ||||
|         this.settings.selectionLimit && | ||||
|         !this.settings.autoUnselect && | ||||
|         this.model.length >= this.settings.selectionLimit && | ||||
|         this.model.indexOf(option.id) === -1 && | ||||
|         this.maybePreventDefault(event) | ||||
|       ) | ||||
|     ) { | ||||
|       this.maybePreventDefault(event); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   isCheckboxDisabled(option?: IMultiSelectOption): boolean { | ||||
|     return this.disabledSelection || option && option.disabled; | ||||
|   } | ||||
| 
 | ||||
|   checkScrollPosition(ev): void { | ||||
|     const scrollTop = ev.target.scrollTop; | ||||
|     const scrollHeight = ev.target.scrollHeight; | ||||
|     const scrollElementHeight = ev.target.clientHeight; | ||||
|     const roundingPixel = 1; | ||||
|     const gutterPixel = 1; | ||||
| 
 | ||||
|     if ( | ||||
|       scrollTop >= | ||||
|       scrollHeight - | ||||
|       (1 + this.settings.loadViewDistance) * scrollElementHeight - | ||||
|       roundingPixel - | ||||
|       gutterPixel | ||||
|     ) { | ||||
|       this.load(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   checkScrollPropagation(ev, element): void { | ||||
|     const scrollTop = element.scrollTop; | ||||
|     const scrollHeight = element.scrollHeight; | ||||
|     const scrollElementHeight = element.clientHeight; | ||||
| 
 | ||||
|     if ( | ||||
|       (ev.deltaY > 0 && scrollTop + scrollElementHeight >= scrollHeight) || | ||||
|       (ev.deltaY < 0 && scrollTop <= 0) | ||||
|     ) { | ||||
|       ev = ev || window.event; | ||||
|       this.maybePreventDefault(ev); | ||||
|       ev.returnValue = false; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   trackById(idx: number, selectOption: IMultiSelectOption): void { | ||||
|     return selectOption.id; | ||||
|   } | ||||
| 
 | ||||
|   load(): void { | ||||
|     this.lazyLoad.emit({ | ||||
|       length: this.options.length, | ||||
|       filter: this.filterControl.value, | ||||
|       checkAllSearches: this.checkAllSearchRegister, | ||||
|       checkAllStatus: this.checkAllStatus, | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   focusItem(dir: number, e?: Event): void { | ||||
|     if (!this.isVisible) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     this.maybePreventDefault(e); | ||||
| 
 | ||||
|     const idx = this.filteredOptions.indexOf(this.focusedItem); | ||||
| 
 | ||||
|     if (idx === -1) { | ||||
|       this.focusedItem = this.filteredOptions[0]; | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     const nextIdx = idx + dir; | ||||
|     const newIdx = | ||||
|       nextIdx < 0 | ||||
|         ? this.filteredOptions.length - 1 | ||||
|         : nextIdx % this.filteredOptions.length; | ||||
| 
 | ||||
|     this.focusedItem = this.filteredOptions[newIdx]; | ||||
|   } | ||||
| 
 | ||||
|   private maybePreventDefault(e?: Event): void { | ||||
|     if (e && e.preventDefault) { | ||||
|       e.preventDefault(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private maybeStopPropagation(e?: Event): void { | ||||
|     if (e && e.stopPropagation) { | ||||
|       e.stopPropagation(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private _escapeRegExp(str: string): RegExp { | ||||
|     const regExpStr = str.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&'); | ||||
|     return new RegExp(regExpStr, 'i'); | ||||
|   } | ||||
| } | ||||
| @ -0,0 +1,39 @@ | ||||
| import { Directive, HostListener } from '@angular/core'; | ||||
| import { EventEmitter } from '@angular/core'; | ||||
| import { Output } from '@angular/core'; | ||||
| 
 | ||||
| @Directive({ | ||||
|   // tslint:disable-next-line:directive-selector
 | ||||
|   selector: '[offClick]', | ||||
| }) | ||||
| 
 | ||||
| export class OffClickDirective { | ||||
|   @Output('offClick') onOffClick = new EventEmitter<any>(); | ||||
| 
 | ||||
|   private _clickEvent: MouseEvent; | ||||
|   private _touchEvent: TouchEvent; | ||||
| 
 | ||||
|   @HostListener('click', ['$event'])  | ||||
|   public onClick(event: MouseEvent): void { | ||||
|     this._clickEvent = event; | ||||
|   } | ||||
| 
 | ||||
|   @HostListener('touchstart', ['$event']) | ||||
|   public onTouch(event: TouchEvent): void { | ||||
|     this._touchEvent = event; | ||||
|   } | ||||
| 
 | ||||
|   @HostListener('document:click', ['$event'])  | ||||
|   public onDocumentClick(event: MouseEvent): void { | ||||
|     if (event !== this._clickEvent) { | ||||
|       this.onOffClick.emit(event); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   @HostListener('document:touchstart', ['$event']) | ||||
|   public onDocumentTouch(event: TouchEvent): void { | ||||
|     if (event !== this._touchEvent) { | ||||
|       this.onOffClick.emit(event); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -0,0 +1,130 @@ | ||||
| import { Pipe, PipeTransform } from '@angular/core'; | ||||
| import { IMultiSelectOption } from './types'; | ||||
| 
 | ||||
| interface StringHashMap<T> { | ||||
|   [k: string]: T; | ||||
| } | ||||
| 
 | ||||
| @Pipe({ | ||||
|   name: 'searchFilter' | ||||
| }) | ||||
| export class MultiSelectSearchFilter implements PipeTransform { | ||||
| 
 | ||||
|   private _lastOptions: IMultiSelectOption[]; | ||||
|   private _searchCache: StringHashMap<IMultiSelectOption[]> = {}; | ||||
|   private _searchCacheInclusive: StringHashMap<boolean | number> = {}; | ||||
|   private _prevSkippedItems: StringHashMap<number> = {}; | ||||
| 
 | ||||
|   transform( | ||||
|     options: IMultiSelectOption[], | ||||
|     str = '', | ||||
|     limit = 0, | ||||
|     renderLimit = 0, | ||||
|     searchFunction: (str: string) => RegExp, | ||||
|   ): IMultiSelectOption[] { | ||||
|     str = str.toLowerCase(); | ||||
| 
 | ||||
|     // Drop cache because options were updated
 | ||||
|     if (options !== this._lastOptions) { | ||||
|       this._lastOptions = options; | ||||
|       this._searchCache = {}; | ||||
|       this._searchCacheInclusive = {}; | ||||
|       this._prevSkippedItems = {}; | ||||
|     } | ||||
| 
 | ||||
|     const filteredOpts = this._searchCache.hasOwnProperty(str) | ||||
|       ? this._searchCache[str] | ||||
|       : this._doSearch(options, str, limit, searchFunction); | ||||
| 
 | ||||
|     const isUnderLimit = options.length <= limit; | ||||
| 
 | ||||
|     return isUnderLimit | ||||
|       ? filteredOpts | ||||
|       : this._limitRenderedItems(filteredOpts, renderLimit); | ||||
|   } | ||||
| 
 | ||||
|   private _getSubsetOptions( | ||||
|     options: IMultiSelectOption[], | ||||
|     prevOptions: IMultiSelectOption[], | ||||
|     prevSearchStr: string | ||||
|   ) { | ||||
|     const prevInclusiveOrIdx = this._searchCacheInclusive[prevSearchStr]; | ||||
| 
 | ||||
|     if (prevInclusiveOrIdx === true) { | ||||
|       // If have previous results and it was inclusive, do only subsearch
 | ||||
|       return prevOptions; | ||||
|     } else if (typeof prevInclusiveOrIdx === 'number') { | ||||
|       // Or reuse prev results with unchecked ones
 | ||||
|       return [...prevOptions, ...options.slice(prevInclusiveOrIdx)]; | ||||
|     } | ||||
| 
 | ||||
|     return options; | ||||
|   } | ||||
| 
 | ||||
|   private _doSearch(options: IMultiSelectOption[], str: string, limit: number, searchFunction: (str: string) => RegExp) { | ||||
|     const prevStr = str.slice(0, -1); | ||||
|     const prevResults = this._searchCache[prevStr]; | ||||
|     const prevResultShift = this._prevSkippedItems[prevStr] || 0; | ||||
| 
 | ||||
|     if (prevResults) { | ||||
|       options = this._getSubsetOptions(options, prevResults, prevStr); | ||||
|     } | ||||
| 
 | ||||
|     const optsLength = options.length; | ||||
|     const maxFound = limit > 0 ? Math.min(limit, optsLength) : optsLength; | ||||
|     const regexp = searchFunction(str); | ||||
|     const filteredOpts: IMultiSelectOption[] = []; | ||||
| 
 | ||||
|     let i = 0, founded = 0, removedFromPrevResult = 0; | ||||
| 
 | ||||
|     const doesOptionMatch = (option: IMultiSelectOption) => regexp.test(option.name); | ||||
|     const getChildren = (option: IMultiSelectOption) => | ||||
|       options.filter(child => child.parentId === option.id); | ||||
|     const getParent = (option: IMultiSelectOption) => | ||||
|       options.find(parent => option.parentId === parent.id); | ||||
|     const foundFn = (item: any) => { filteredOpts.push(item); founded++; }; | ||||
|     const notFoundFn = prevResults ? () => removedFromPrevResult++ : () => { }; | ||||
| 
 | ||||
|     for (; i < optsLength && founded < maxFound; ++i) { | ||||
|       const option = options[i]; | ||||
|       const directMatch = doesOptionMatch(option); | ||||
| 
 | ||||
|       if (directMatch) { | ||||
|         foundFn(option); | ||||
|         continue; | ||||
|       } | ||||
| 
 | ||||
|       if (typeof option.parentId === 'undefined') { | ||||
|         const childrenMatch = getChildren(option).some(doesOptionMatch); | ||||
| 
 | ||||
|         if (childrenMatch) { | ||||
|           foundFn(option); | ||||
|           continue; | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       if (typeof option.parentId !== 'undefined') { | ||||
|         const parentMatch = doesOptionMatch(getParent(option)); | ||||
| 
 | ||||
|         if (parentMatch) { | ||||
|           foundFn(option); | ||||
|           continue; | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       notFoundFn(); | ||||
|     } | ||||
| 
 | ||||
|     const totalIterations = i + prevResultShift; | ||||
| 
 | ||||
|     this._searchCache[str] = filteredOpts; | ||||
|     this._searchCacheInclusive[str] = i === optsLength || totalIterations; | ||||
|     this._prevSkippedItems[str] = removedFromPrevResult + prevResultShift; | ||||
| 
 | ||||
|     return filteredOpts; | ||||
|   } | ||||
| 
 | ||||
|   private _limitRenderedItems<T>(items: T[], limit: number): T[] { | ||||
|     return items.length > limit && limit > 0 ? items.slice(0, limit) : items; | ||||
|   } | ||||
| } | ||||
| @ -0,0 +1,82 @@ | ||||
| export interface IMultiSelectOption { | ||||
|   id: any; | ||||
|   name: string; | ||||
|   disabled?: boolean; | ||||
|   isLabel?: boolean; | ||||
|   parentId?: any; | ||||
|   params?: any; | ||||
|   classes?: string; | ||||
|   image?: string; | ||||
| } | ||||
| 
 | ||||
| export interface IMultiSelectSettings { | ||||
|   pullRight?: boolean; | ||||
|   enableSearch?: boolean; | ||||
|   closeOnClickOutside?: boolean; | ||||
|   /** | ||||
|    * 0 - By default | ||||
|    * If `enableSearch=true` and total amount of items more then `searchRenderLimit` (0 - No limit) | ||||
|    * then render items only when user typed more then or equal `searchRenderAfter` charachters | ||||
|    */ | ||||
|   searchRenderLimit?: number; | ||||
|   /** | ||||
|    * 3 - By default | ||||
|    */ | ||||
|   searchRenderAfter?: number; | ||||
|   /** | ||||
|    * 0 - By default | ||||
|    * If >0 will render only N first items | ||||
|    */ | ||||
|   searchMaxLimit?: number; | ||||
|   /** | ||||
|    * 0 - By default | ||||
|    * Used with searchMaxLimit to further limit rendering for optimization | ||||
|    * Should be less than searchMaxLimit to take effect | ||||
|    */ | ||||
|   searchMaxRenderedItems?: number; | ||||
|   checkedStyle?: 'checkboxes' | 'glyphicon' | 'fontawesome' | 'visual'; | ||||
|   buttonClasses?: string; | ||||
|   itemClasses?: string; | ||||
|   containerClasses?: string; | ||||
|   selectionLimit?: number; | ||||
|   minSelectionLimit?: number; | ||||
|   closeOnSelect?: boolean; | ||||
|   autoUnselect?: boolean; | ||||
|   showCheckAll?: boolean; | ||||
|   showUncheckAll?: boolean; | ||||
|   fixedTitle?: boolean; | ||||
|   dynamicTitleMaxItems?: number; | ||||
|   maxHeight?: string; | ||||
|   displayAllSelectedText?: boolean; | ||||
|   isLazyLoad?: boolean; | ||||
|   loadViewDistance?: number; | ||||
|   stopScrollPropagation?: boolean; | ||||
|   selectAddedValues?: boolean; | ||||
|   /** | ||||
|    * false - By default | ||||
|    * If activated label IDs don't count and won't be written to the model. | ||||
|    */ | ||||
|   ignoreLabels?: boolean; | ||||
|   /** | ||||
|    * false - By default | ||||
|    * If activated, the title will show selections in the order they were selected. | ||||
|    */ | ||||
|   maintainSelectionOrderInTitle?: boolean; | ||||
|   /** | ||||
|    * @default true | ||||
|    * Set the focus back to the input control when the dropdown closed | ||||
|    */ | ||||
|   focusBack?: boolean; | ||||
| } | ||||
| 
 | ||||
| export interface IMultiSelectTexts { | ||||
|   checkAll?: string; | ||||
|   uncheckAll?: string; | ||||
|   checked?: string; | ||||
|   checkedPlural?: string; | ||||
|   searchPlaceholder?: string; | ||||
|   searchEmptyResult?: string; | ||||
|   searchNoRenderText?: string; | ||||
|   defaultTitle?: string; | ||||
|   allSelected?: string; | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user