mempool/frontend/src/app/components/ngx-bootstrap-multiselect/ngx-bootstrap-multiselect.component.ts
2022-11-28 16:53:17 +09:00

711 lines
20 KiB
TypeScript

import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
DoCheck,
EventEmitter,
forwardRef,
Input,
IterableDiffers,
OnChanges,
OnDestroy,
OnInit,
Output,
SimpleChanges,
} from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
UntypedFormBuilder,
UntypedFormControl,
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: UntypedFormControl = 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: UntypedFormBuilder,
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');
}
}