mirror of
https://github.com/mempool/mempool.git
synced 2024-11-20 10:21:52 +01:00
Merge branch 'master' into nymkappa/feature/automatic-block-reindexing
This commit is contained in:
commit
567d4aebbc
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@ -34,7 +34,7 @@ jobs:
|
||||
|
||||
- name: Install (Prod dependencies only)
|
||||
if: ${{ matrix.flavor == 'prod'}}
|
||||
run: npm ci --prod --no-optional
|
||||
run: npm ci --omit=dev --omit=optional
|
||||
working-directory: ${{ matrix.flavor }}/backend
|
||||
|
||||
- name: Lint
|
||||
@ -70,7 +70,7 @@ jobs:
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install (Prod dependencies only)
|
||||
run: npm ci --prod --no-optional
|
||||
run: npm ci --omit=dev --omit=optional
|
||||
if: ${{ matrix.flavor == 'prod'}}
|
||||
working-directory: ${{ matrix.flavor }}/frontend
|
||||
|
||||
|
@ -8,7 +8,7 @@ COPY . .
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y build-essential python3 pkg-config
|
||||
RUN npm install
|
||||
RUN npm install --omit=dev --omit=optional
|
||||
RUN npm run build
|
||||
|
||||
FROM node:16.15.0-buster-slim
|
||||
|
@ -8,7 +8,7 @@ WORKDIR /build
|
||||
COPY . .
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y build-essential rsync
|
||||
RUN npm i
|
||||
RUN npm install --omit=dev --omit=optional
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:1.17.8-alpine
|
||||
|
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;
|
||||
}
|
@ -1295,18 +1295,6 @@ if [ "${BITCOIN_MAINNET_ENABLE}" = ON ];then
|
||||
echo "[*] Installing Bitcoin Mainnet electrs start script"
|
||||
osSudo "${ROOT_USER}" install -c -o "${BITCOIN_USER}" -g "${BITCOIN_GROUP}" -m 755 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/electrs-start-mainnet" "${BITCOIN_ELECTRS_HOME}"
|
||||
|
||||
echo "[*] Installing Bitcoin crontab"
|
||||
case $OS in
|
||||
FreeBSD)
|
||||
echo [*] FIXME: must only crontab enabled daemons
|
||||
osSudo "${ROOT_USER}" crontab -u "${BITCOIN_USER}" "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/bitcoin.crontab"
|
||||
osSudo "${ROOT_USER}" crontab -u "${MINFEE_USER}" "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/minfee.crontab"
|
||||
;;
|
||||
Debian)
|
||||
(crontab -l ; echo "@reboot sleep 30 ; screen -dmS mainnet /bitcoin/electrs/electrs-start-mainnet") | osSudo "${ROOT_USER}" crontab -u "${BITCOIN_USER}" -
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "[*] Configuring Bitcoin Mainnet RPC credentials in electrs start script"
|
||||
osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-mainnet"
|
||||
osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-mainnet"
|
||||
@ -1321,13 +1309,6 @@ if [ "${BITCOIN_TESTNET_ENABLE}" = ON ];then
|
||||
echo "[*] Installing Bitcoin Testnet electrs start script"
|
||||
osSudo "${ROOT_USER}" install -c -o "${BITCOIN_USER}" -g "${BITCOIN_GROUP}" -m 755 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/electrs-start-testnet" "${BITCOIN_ELECTRS_HOME}"
|
||||
|
||||
case $OS in
|
||||
Debian)
|
||||
echo "[*] Installing Bitcoin-testnet crontab"
|
||||
(crontab -l ; echo "@reboot sleep 70 ; screen -dmS testnet /bitcoin/electrs/electrs-start-testnet") | osSudo "${ROOT_USER}" crontab -u "${BITCOIN_USER}" -
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "[*] Configuring Bitcoin Testnet RPC credentials in electrs start script"
|
||||
osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-testnet"
|
||||
osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-testnet"
|
||||
@ -1342,13 +1323,6 @@ if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then
|
||||
echo "[*] Installing Bitcoin Signet electrs start script"
|
||||
osSudo "${ROOT_USER}" install -c -o "${BITCOIN_USER}" -g "${BITCOIN_GROUP}" -m 755 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/electrs-start-signet" "${BITCOIN_ELECTRS_HOME}"
|
||||
|
||||
case $OS in
|
||||
Debian)
|
||||
echo "[*] Installing Bitcoin-signet crontab"
|
||||
(crontab -l ; echo "@reboot sleep 90 ; screen -dmS signet /bitcoin/electrs/electrs-start-signet") | osSudo "${ROOT_USER}" crontab -u "${BITCOIN_USER}" -
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "[*] Configuring Bitcoin Signet RPC credentials in electrs start script"
|
||||
osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-signet"
|
||||
osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-signet"
|
||||
@ -1369,9 +1343,6 @@ if [ "${ELEMENTS_LIQUID_ENABLE}" = ON ];then
|
||||
echo [*] FIXME: must only crontab enabled daemons
|
||||
osSudo "${ROOT_USER}" crontab -u "${ELEMENTS_USER}" "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/elements.crontab"
|
||||
;;
|
||||
Debian)
|
||||
(crontab -l ; echo "6 * * * * cd $HOME/asset_registry_db && git pull origin master >/dev/null 2>&1") | osSudo "${ROOT_USER}" crontab -u "${ELEMENTS_USER}" -
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "[*] Configuring Elements Liquid RPC credentials in electrs start script"
|
||||
@ -1388,13 +1359,6 @@ if [ "${ELEMENTS_LIQUIDTESTNET_ENABLE}" = ON ];then
|
||||
echo "[*] Installing Elements Liquid Testnet electrs start script"
|
||||
osSudo "${ROOT_USER}" install -c -o "${ELEMENTS_USER}" -g "${ELEMENTS_GROUP}" -m 755 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/electrs-start-liquidtestnet" "${ELEMENTS_ELECTRS_HOME}"
|
||||
|
||||
case $OS in
|
||||
Debian)
|
||||
echo "[*] Installing Elements-testnet crontab"
|
||||
(crontab -l ; echo "6 * * * * cd $HOME/asset_registry_testnet_db && git pull origin master >/dev/null 2>&1") | osSudo "${ROOT_USER}" crontab -u "${ELEMENTS_USER}" -
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "[*] Installing Elements Liquid Testnet RPC credentials"
|
||||
osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${ELEMENTS_HOME}/elements.conf"
|
||||
osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${ELEMENTS_HOME}/elements.conf"
|
||||
@ -1407,6 +1371,45 @@ if [ "${ELEMENTS_LIQUIDTESTNET_ENABLE}" = ON ];then
|
||||
osSudo "${ROOT_USER}" sed -i.orig "s!__ELECTRS_DATA_ROOT__!${ELECTRS_DATA_ROOT}!" "${ELEMENTS_ELECTRS_HOME}/electrs-start-liquidtestnet"
|
||||
fi
|
||||
|
||||
################################
|
||||
# Install all Electrs Cronjobs #
|
||||
################################
|
||||
echo "[*] Installing crontabs"
|
||||
case $OS in
|
||||
FreeBSD)
|
||||
echo [*] FIXME: must only crontab enabled daemons
|
||||
osSudo "${ROOT_USER}" crontab -u "${BITCOIN_USER}" "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/bitcoin.crontab"
|
||||
osSudo "${ROOT_USER}" crontab -u "${MINFEE_USER}" "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/minfee.crontab"
|
||||
;;
|
||||
Debian)
|
||||
crontab_bitcoin=()
|
||||
if [ "${BITCOIN_MAINNET_ENABLE}" = ON ];then
|
||||
echo [*] Installing Electrs Mainnet Cronjob
|
||||
crontab_bitcoin+="@reboot sleep 30 ; screen -dmS mainnet /bitcoin/electrs/electrs-start-mainnet\n"
|
||||
fi
|
||||
if [ "${BITCOIN_TESTNET_ENABLE}" = ON ];then
|
||||
echo [*] Installing Electrs Testnet Cronjob
|
||||
crontab_bitcoin+="@reboot sleep 70 ; screen -dmS testnet /bitcoin/electrs/electrs-start-testnet\n"
|
||||
fi
|
||||
if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then
|
||||
echo [*] Installing Electrs Signet Cronjob
|
||||
crontab_bitcoin+="@reboot sleep 90 ; screen -dmS signet /bitcoin/electrs/electrs-start-signet\n"
|
||||
fi
|
||||
echo "${crontab_bitcoin}" | crontab -u "${BITCOIN_USER}" -
|
||||
|
||||
crontab_elements=()
|
||||
if [ "${ELEMENTS_LIQUID_ENABLE}" = ON ];then
|
||||
echo [*] Installing Liquid Asset Mainnet Cronjob
|
||||
crontab_elements+="6 * * * * cd $HOME/asset_registry_db && git pull origin master >/dev/null 2>&1\n"
|
||||
fi
|
||||
if [ "${ELEMENTS_LIQUIDTESTNET_ENABLE}" = ON ];then
|
||||
echo [*] Installing Liquid Asset Testnet Cronjob
|
||||
crontab_elements+="6 * * * * cd $HOME/asset_registry_testnet_db && git pull origin master >/dev/null 2>&1\n"
|
||||
fi
|
||||
echo "${crontab_elements}" | crontab -u "${ELEMENTS_USER}" -
|
||||
;;
|
||||
esac
|
||||
|
||||
#####################################
|
||||
# Bisq instance for Bitcoin Mainnet #
|
||||
#####################################
|
||||
@ -1553,6 +1556,29 @@ case $OS in
|
||||
;;
|
||||
esac
|
||||
|
||||
##### OS set Linux user ulimits
|
||||
|
||||
echo "[*] Setting ulimits for users"
|
||||
case $OS in
|
||||
|
||||
FreeBSD)
|
||||
;;
|
||||
|
||||
Debian)
|
||||
cat >> /etc/security/limits.conf <<EOF
|
||||
* soft nproc 200000
|
||||
* hard nproc 200000
|
||||
* soft nofile 200000
|
||||
* hard nofile 200000
|
||||
EOF
|
||||
echo "session required pam_limits.so" >> /etc/pam.d/common-session
|
||||
;;
|
||||
esac
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
##### OS services
|
||||
|
||||
#if [ "${BITCOIN_MAINNET_ENABLE}" = ON ];then
|
||||
@ -1628,6 +1654,8 @@ esac
|
||||
|
||||
##### finish
|
||||
|
||||
echo 'Please reboot to start all the services.'
|
||||
|
||||
echo '[*] Done!'
|
||||
|
||||
exit 0
|
||||
|
@ -56,7 +56,7 @@ build_frontend()
|
||||
if [ ! -e "mempool-frontend-config.json" ];then
|
||||
cp "${HOME}/mempool/production/mempool-frontend-config.${site}.json" "mempool-frontend-config.json"
|
||||
fi
|
||||
npm install --prod --no-optional || exit 1
|
||||
npm install --omit=dev --omit=optional || exit 1
|
||||
npm run build || exit 1
|
||||
}
|
||||
|
||||
@ -75,7 +75,7 @@ build_backend()
|
||||
-e "s!__ELEMENTS_RPC_PASS__!${ELEMENTS_RPC_PASS}!" \
|
||||
"mempool-config.json"
|
||||
fi
|
||||
npm install --prod --no-optional || exit 1
|
||||
npm install --omit=dev --omit=optional || exit 1
|
||||
npm run build || exit 1
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user