sorting tree view filters

- callList
 - refactored agendaSort
This commit is contained in:
Maximilian Krambach 2019-05-16 19:05:54 +02:00
parent 5e33c500c3
commit 37f0baf165
7 changed files with 439 additions and 167 deletions

View File

@ -103,6 +103,8 @@ export class SortingTreeComponent<T extends Identifiable & Displayable> implemen
/**
* Function that will be used for filtering the nodes.
* Should return false for an item is to be visible
* TODO this inverts all other 'sorting' conventions elsewhere!
*/
private activeFilter: (node: T) => boolean;

View File

@ -31,8 +31,8 @@
<span class="sort-grid">
<div class="hint">{{ 'Visibility' | translate }}</div>
<div>
<mat-checkbox *ngFor="let option of filterOptions" [(ngModel)]="option.state" (change)="onFilterChange(option.name)">
<mat-icon matTooltip="{{ option.name | translate }}">{{ getIcon(option.name) }}</mat-icon> {{ option.name | translate }}
<mat-checkbox *ngFor="let option of filterOptions" [(ngModel)]="option.state" (change)="onFilterChange(option.id)">
<mat-icon matTooltip="{{ option.label | translate }}">{{ getIcon(option.label) }}</mat-icon> {{ option.label | translate }}
</mat-checkbox>
</div>
</span>

View File

@ -1,17 +1,15 @@
import { Component, ViewChild, EventEmitter, OnInit } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { MatSnackBar } from '@angular/material';
import { BehaviorSubject, Observable } from 'rxjs';
import { TranslateService } from '@ngx-translate/core';
import { Observable, BehaviorSubject } from 'rxjs';
import { ItemRepositoryService } from 'app/core/repositories/agenda/item-repository.service';
import { BaseViewComponent } from '../../../base/base-view';
import { SortingTreeComponent } from 'app/shared/components/sorting-tree/sorting-tree.component';
import { ViewItem } from '../../models/view-item';
import { PromptService } from 'app/core/ui-services/prompt.service';
import { CanComponentDeactivate } from 'app/shared/utils/watch-sorting-tree.guard';
import { itemVisibilityChoices } from 'app/shared/models/agenda/item';
import { ItemRepositoryService } from 'app/core/repositories/agenda/item-repository.service';
import { SortTreeViewComponent, SortTreeFilterOption } from 'app/site/base/sort-tree.component';
import { PromptService } from 'app/core/ui-services/prompt.service';
import { ViewItem } from '../../models/view-item';
/**
* Sort view for the agenda.
@ -21,58 +19,25 @@ import { itemVisibilityChoices } from 'app/shared/models/agenda/item';
templateUrl: './agenda-sort.component.html',
styleUrls: ['./agenda-sort.component.scss']
})
export class AgendaSortComponent extends BaseViewComponent implements CanComponentDeactivate, OnInit {
export class AgendaSortComponent extends SortTreeViewComponent<ViewItem> implements OnInit {
/**
* Reference to the view child
* All agendaItems sorted by their weight {@link ViewItem.weight}
*/
@ViewChild('osSortedTree')
public osSortTree: SortingTreeComponent<ViewItem>;
/**
* Emitter to emit if the nodes should expand or collapse.
*/
public readonly changeState: EventEmitter<Boolean> = new EventEmitter<Boolean>();
/**
* Emitter who emits the filters to the sorting tree.
*/
public readonly changeFilter: EventEmitter<(item: ViewItem) => boolean> = new EventEmitter<
(item: ViewItem) => boolean
>();
public itemsObservable: Observable<ViewItem[]>;
/**
* These are the available options for filtering the nodes.
* Adds the property `state` to identify if the option is marked as active.
* When reset the filters, the option `state` will be set to `false`.
*/
public filterOptions = itemVisibilityChoices.map(item => {
return { ...item, state: false };
public filterOptions: SortTreeFilterOption[] = itemVisibilityChoices.map(item => {
return { label: item.name, id: item.key, state: false };
});
/**
* BehaviourSubject to get informed every time the filters change.
*/
private activeFilters: BehaviorSubject<string[]> = new BehaviorSubject<string[]>([]);
/**
* Boolean to check if changes has been made.
*/
public hasChanged = false;
/**
* Boolean to check if filters are active, so they could be removed.
*/
public hasActiveFilter = false;
/**
* Array, that holds the number of visible nodes and amount of available nodes.
*/
public seenNodes: [number, number] = [0, 0];
/**
* All agendaItems sorted by their weight {@link ViewItem.weight}
*/
public itemsObservable: Observable<ViewItem[]>;
protected activeFilters: BehaviorSubject<number[]> = new BehaviorSubject<number[]>([]);
/**
* Updates the incoming/changing agenda items.
@ -87,12 +52,30 @@ export class AgendaSortComponent extends BaseViewComponent implements CanCompone
translate: TranslateService,
matSnackBar: MatSnackBar,
private agendaRepo: ItemRepositoryService,
private promptService: PromptService
promptService: PromptService
) {
super(title, translate, matSnackBar);
super(title, translate, matSnackBar, promptService);
this.itemsObservable = this.agendaRepo.getViewModelListObservable();
}
/**
* Function to emit the active filters.
* Filters will be stored in an array to prevent duplicated options.
* Furthermore if the option is already included in this array, then it will be deleted.
* This array will be emitted.
*
* @param filter Is the filter that was activated by the user.
*/
public onFilterChange(filter: number): void {
const value = this.activeFilters.value;
if (!value.includes(filter)) {
value.push(filter);
} else {
value.splice(value.indexOf(filter), 1);
}
this.activeFilters.next(value);
}
/**
* OnInit method
*/
@ -100,64 +83,17 @@ export class AgendaSortComponent extends BaseViewComponent implements CanCompone
/**
* Passes the active filters as an array to the subject.
*/
const filter = this.activeFilters.subscribe((value: string[]) => {
const filter = this.activeFilters.subscribe((value: number[]) => {
this.hasActiveFilter = value.length === 0 ? false : true;
this.changeFilter.emit(
(item: ViewItem): boolean => {
return !(value.includes(item.verboseType) || value.length === 0);
return !(value.includes(item.type) || value.length === 0);
}
);
});
this.subscriptions.push(filter);
}
/**
* Function to save the tree by click.
*/
public async onSave(): Promise<void> {
await this.agendaRepo
.sortItems(this.osSortTree.getTreeData())
.then(() => this.osSortTree.setSubscription(), this.raiseError);
}
/**
* Function to restore the old state.
*/
public async onCancel(): Promise<void> {
if (await this.canDeactivate()) {
this.osSortTree.setSubscription();
}
}
/**
* Function to get an info if changes has been made.
*
* @param hasChanged Boolean received from the tree to see that changes has been made.
*/
public receiveChanges(hasChanged: boolean): void {
this.hasChanged = hasChanged;
}
/**
* Function to receive the new number of visible nodes when the filter has changed.
*
* @param nextNumberOfSeenNodes is an array with two indices:
* The first gives the number of currently shown nodes.
* The second tells how many nodes available.
*/
public onChangeAmountOfItems(nextNumberOfSeenNodes: [number, number]): void {
this.seenNodes = nextNumberOfSeenNodes;
}
/**
* Function to emit if the nodes should be expanded or collapsed.
*
* @param nextState Is the next state, expanded or collapsed, the nodes should be.
*/
public onStateChange(nextState: boolean): void {
this.changeState.emit(nextState);
}
/**
* Function to set the active filters to null.
*/
@ -169,36 +105,21 @@ export class AgendaSortComponent extends BaseViewComponent implements CanCompone
}
/**
* Function to emit the active filters.
* Filters will be stored in an array to prevent duplicated options.
* Furthermore if the option is already included in this array, then it will be deleted.
* This array will be emitted.
*
* @param filter Is the filter that was activated by the user.
* Function to save the tree by click.
*/
public onFilterChange(filter: string): void {
const value = this.activeFilters.value;
if (!value.includes(filter)) {
value.push(filter);
} else {
value.splice(value.indexOf(filter), 1);
}
this.activeFilters.next(value);
public async onSave(): Promise<void> {
await this.agendaRepo
.sortItems(this.osSortTree.getTreeData())
.then(() => this.osSortTree.setSubscription(), this.raiseError);
}
/**
* Function to open a prompt dialog,
* so the user will be warned if he has made changes and not saved them.
* Function to emit if the nodes should be expanded or collapsed.
*
* @returns The result from the prompt dialog.
* @param nextState Is the next state, expanded or collapsed, the nodes should be.
*/
public async canDeactivate(): Promise<boolean> {
if (this.hasChanged) {
const title = this.translate.instant('Do you really want to exit this page?');
const content = this.translate.instant('You made changes.');
return await this.promptService.open(title, content);
}
return true;
public onStateChange(nextState: boolean): void {
this.changeState.emit(nextState);
}
/**

View File

@ -0,0 +1,127 @@
import { ViewChild, EventEmitter } from '@angular/core';
import { MatSnackBar } from '@angular/material';
import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { BaseViewModel } from './base-view-model';
import { BaseViewComponent } from './base-view';
import { CanComponentDeactivate } from 'app/shared/utils/watch-sorting-tree.guard';
import { Identifiable } from 'app/shared/models/base/identifiable';
import { PromptService } from 'app/core/ui-services/prompt.service';
import { SortingTreeComponent } from 'app/shared/components/sorting-tree/sorting-tree.component';
export interface SortTreeFilterOption extends Identifiable {
label: string;
id: number;
state: boolean;
}
/**
* Abstract Sort view for hierarchic item trees
*/
export abstract class SortTreeViewComponent<V extends BaseViewModel> extends BaseViewComponent
implements CanComponentDeactivate {
/**
* Reference to the view child
*/
@ViewChild('osSortedTree')
public osSortTree: SortingTreeComponent<V>;
/**
* Emitter to emit if the nodes should expand or collapse.
*/
public readonly changeState: EventEmitter<Boolean> = new EventEmitter<Boolean>();
/**
* Emitter that emits the filters to the sorting tree.
* TODO note that the boolean function currently requires false if the item
* is to be visible!
*/
public readonly changeFilter: EventEmitter<(item: V) => boolean> = new EventEmitter<(item: V) => boolean>();
/**
* Boolean to check if changes has been made.
*/
public hasChanged = false;
/**
* Boolean to check if filters are active, so they could be removed.
*/
public hasActiveFilter = false;
/**
* Array that holds the number of visible nodes(0) and amount of available
* nodes(1).
*/
public seenNodes: [number, number] = [0, 0];
/**
* Updates the incoming/changing agenda items.
* @param title
* @param translate
* @param matSnackBar
* @param promptService
*/
public constructor(
title: Title,
public translate: TranslateService,
matSnackBar: MatSnackBar,
protected promptService: PromptService
) {
super(title, translate, matSnackBar);
}
/**
* Function to restore the old state.
*/
public async onCancel(): Promise<void> {
if (await this.canDeactivate()) {
this.osSortTree.setSubscription();
}
}
/**
* Function to set an info if changes has been made.
*
* @param hasChanged Boolean received from the tree to see that changes has been made.
*/
public receiveChanges(hasChanged: boolean): void {
this.hasChanged = hasChanged;
}
/**
* Function to receive the new number of visible nodes when the filter has changed.
*
* @param nextNumberOfSeenNodes is an array with two indices:
* The first gives the number of currently shown nodes.
* The second tells how many nodes available.
*/
public onChangeAmountOfItems(nextNumberOfSeenNodes: [number, number]): void {
this.seenNodes = nextNumberOfSeenNodes;
}
/**
* Function to emit if the nodes should be expanded or collapsed.
*
* @param nextState Is the next state, expanded or collapsed, the nodes should be.
*/
public onStateChange(nextState: boolean): void {
this.changeState.emit(nextState);
}
/**
* Function to open a prompt dialog, so the user will be warned if they have
* made changes and not saved them.
*
* @returns The result from the prompt dialog.
*/
public async canDeactivate(): Promise<boolean> {
if (this.hasChanged) {
const title = this.translate.instant('Do you really want to exit this page?');
const content = this.translate.instant('You made changes.');
return await this.promptService.open(title, content);
}
return true;
}
}

View File

@ -1,10 +1,4 @@
<os-head-bar
prevUrl="../.."
[nav]="false"
[editMode]="hasChanged"
(mainEvent)="onCancel()"
(saveEvent)="onSave()">
<os-head-bar prevUrl="../.." [nav]="false" [editMode]="hasChanged" (mainEvent)="onCancel()" (saveEvent)="onSave()">
<!-- Title -->
<div class="title-slot">
<h2 translate>Call list</h2>
@ -12,19 +6,83 @@
<!-- Export menu -->
<div class="menu-slot">
<button type="button" mat-icon-button [matMenuTriggerFor]="downloadMenu">
<button type="button" mat-icon-button [matMenuTriggerFor]="mainMenu">
<mat-icon>more_vert</mat-icon>
</button>
</div>
</os-head-bar>
<div class="custom-table-header flex-spaced on-transition-fade">
<div class="filter-count">
<span> {{ seenNodes[0] }}&nbsp;</span><span translate>of</span>
<span>&nbsp;{{ seenNodes[1] }}</span>
</div>
<div class="current-filters" *ngIf="hasActiveFilter">
<div><span translate>Active filters</span>:&nbsp;</div>
<div>
<button mat-button (click)="resetFilters()">
<mat-icon inline>cancel</mat-icon>
<span translate>Clear all</span>
</button>
</div>
</div>
<div class="flex-spaced margin-right-5">
<div *ngIf="categoryFilterOptions.length">
<button mat-button [matMenuTriggerFor]="catFilterMenu">
<span
class="upper"
[matBadge]="activeCatFilterCount > 0 ? activeCatFilterCount : null"
matBadgeColor="accent"
[matBadgeOverlap]="false"
translate
>
Categories
</span>
</button>
</div>
<div *ngIf="tagFilterOptions.length">
<button mat-button [matMenuTriggerFor]="tagFilterMenu">
<span
class="upper"
[matBadge]="activeTagFilterCount > 0 ? activeTagFilterCount : null"
matBadgeColor="accent"
[matBadgeOverlap]="false"
translate
>
Tags
</span>
</button>
</div>
</div>
</div>
<mat-menu #catFilterMenu="matMenu">
<div *ngFor="let catOption of categoryFilterOptions">
<button mat-menu-item (click)="onFilterChange('category', catOption.id)">
<mat-icon *ngIf="catOption.state">checked</mat-icon>
<span>{{ catOption.label }}</span>
</button>
</div>
</mat-menu>
<mat-menu #tagFilterMenu="matMenu">
<div *ngFor="let tagOption of tagFilterOptions">
<button mat-menu-item (click)="onFilterChange('tag', tagOption.id)">
<mat-icon *ngIf="tagOption.state">checked</mat-icon>
<span>{{ tagOption.label }}</span>
</button>
</div>
</mat-menu>
<mat-card>
<os-sorting-tree
#osSortedTree
parentKey="sort_parent_id"
weightKey="weight"
(visibleNodes)="onChangeAmountOfItems($event)"
(hasChanged)="receiveChanges($event)"
[model]="motionsObservable">
[model]="motionsObservable"
[filterChange]="changeFilter"
>
<ng-template #innerNode let-item="item">
<div class="line">
<div class="left">
@ -41,7 +99,7 @@
</os-sorting-tree>
</mat-card>
<mat-menu #downloadMenu="matMenu">
<mat-menu #mainMenu="matMenu">
<button mat-menu-item (click)="pdfExportCallList()">
<mat-icon>picture_as_pdf</mat-icon>
<span translate>Export as PDF</span>

View File

@ -20,3 +20,6 @@
white-space: nowrap;
}
}
.margin-right-5 {
margin-right: 5px !important;
}

View File

@ -1,18 +1,18 @@
import { Component, ViewChild } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { MatSnackBar } from '@angular/material';
import { Observable, BehaviorSubject } from 'rxjs';
import { TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs';
import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service';
import { BaseViewComponent } from 'app/site/base/base-view';
import { CanComponentDeactivate } from 'app/shared/utils/watch-sorting-tree.guard';
import { CategoryRepositoryService } from 'app/core/repositories/motions/category-repository.service';
import { FlatNode } from 'app/core/ui-services/tree.service';
import { MotionCsvExportService } from 'app/site/motions/services/motion-csv-export.service';
import { MotionPdfExportService } from 'app/site/motions/services/motion-pdf-export.service';
import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service';
import { PromptService } from 'app/core/ui-services/prompt.service';
import { SortingTreeComponent } from 'app/shared/components/sorting-tree/sorting-tree.component';
import { SortTreeViewComponent, SortTreeFilterOption } from 'app/site/base/sort-tree.component';
import { TagRepositoryService } from 'app/core/repositories/tags/tag-repository.service';
import { ViewMotion } from 'app/site/motions/models/view-motion';
import { ViewTag } from 'app/site/tags/models/view-tag';
@ -22,15 +22,12 @@ import { ViewTag } from 'app/site/tags/models/view-tag';
@Component({
selector: 'os-call-list',
templateUrl: './call-list.component.html',
styleUrls: ['./call-list.component.scss']
styleUrls: [
'./call-list.component.scss',
'../../../../shared/components/sort-filter-bar/sort-filter-bar.component.scss'
]
})
export class CallListComponent extends BaseViewComponent implements CanComponentDeactivate {
/**
* Reference to the sorting tree.
*/
@ViewChild('osSortedTree')
private osSortTree: SortingTreeComponent<ViewMotion>;
export class CallListComponent extends SortTreeViewComponent<ViewMotion> implements OnInit {
/**
* All motions sorted first by weight, then by id.
*/
@ -47,11 +44,46 @@ export class CallListComponent extends BaseViewComponent implements CanComponent
public hasChanged = false;
/**
* Updates the motions member, and sorts it.
* A (dynamically updating) list of available and active filters by tag
*/
public tagFilterOptions: SortTreeFilterOption[] = [];
/**
* The current amount of tag filters active
*/
public activeTagFilterCount = 0;
/**
* A (dynamically updating) list of available and active filters by category
*/
public categoryFilterOptions: SortTreeFilterOption[] = [];
/**
* The current amount of category filters active
*/
public activeCatFilterCount = 0;
/**
* BehaviorSubject to get informed every time the tag filter changes.
*/
protected activeTagFilters: BehaviorSubject<number[]> = new BehaviorSubject<number[]>([]);
/**
* BehaviourSubject to get informed every time the category filter changes.
*/
protected activeCatFilters: BehaviorSubject<number[]> = new BehaviorSubject<number[]>([]);
/**
* constructor. Subscribes to the motions
*
* @param title
* @param translate
* @param matSnackBar
* @param motionRepo
* @param motionCsvExport
* @param motionPdfExport
* @param tagRepo
* @param categoryRepo
* @param promptService
*/
public constructor(
@ -61,9 +93,11 @@ export class CallListComponent extends BaseViewComponent implements CanComponent
private motionRepo: MotionRepositoryService,
private motionCsvExport: MotionCsvExportService,
private motionPdfExport: MotionPdfExportService,
private promptService: PromptService
private tagRepo: TagRepositoryService,
private categoryRepo: CategoryRepositoryService,
promptService: PromptService
) {
super(title, translate, matSnackBar);
super(title, translate, matSnackBar, promptService);
this.motionsObservable = this.motionRepo.getViewModelListObservable();
this.motionsObservable.subscribe(motions => {
@ -72,6 +106,75 @@ export class CallListComponent extends BaseViewComponent implements CanComponent
});
}
/**
* Initializes filters and filter subscriptions
*/
public ngOnInit(): void {
this.subscriptions.push(
this.activeTagFilters.subscribe((value: number[]) => this.onSubscribedFilterChange('tag', value))
);
this.subscriptions.push(
this.activeCatFilters.subscribe((value: number[]) => this.onSubscribedFilterChange('category', value))
);
this.subscriptions.push(
this.tagRepo.getViewModelListBehaviorSubject().subscribe(tags => {
if (tags && tags.length) {
this.tagFilterOptions = tags.map(tag => {
return {
label: tag.name,
id: tag.id,
state:
this.tagFilterOptions &&
this.tagFilterOptions.some(tagfilter => {
return tagfilter.id === tag.id && tagfilter.state === true;
})
};
});
this.tagFilterOptions.push({
label: this.translate.instant('No tags'),
id: 0,
state:
this.tagFilterOptions &&
this.tagFilterOptions.some(tagfilter => {
return tagfilter.id === 0 && tagfilter.state === true;
})
});
} else {
this.tagFilterOptions = [];
}
})
);
this.subscriptions.push(
this.categoryRepo.getViewModelListBehaviorSubject().subscribe(categories => {
if (categories && categories.length) {
this.categoryFilterOptions = categories.map(cat => {
return {
label: cat.prefixedName,
id: cat.id,
state:
this.categoryFilterOptions &&
this.categoryFilterOptions.some(catfilter => {
return catfilter.id === cat.id && catfilter.state === true;
})
};
});
this.categoryFilterOptions.push({
label: this.translate.instant('No category'),
id: 0,
state:
this.categoryFilterOptions &&
this.categoryFilterOptions.some(catfilter => {
return catfilter.id === 0 && catfilter.state === true;
})
});
} else {
this.categoryFilterOptions = [];
}
})
);
}
/**
* Function to save changes on click.
*/
@ -91,7 +194,7 @@ export class CallListComponent extends BaseViewComponent implements CanComponent
}
/**
* Function to get an info if changes has been made.
* Function to register the info that changes has been made.
*
* @param hasChanged Boolean received from the tree to see that changes has been made.
*/
@ -113,21 +216,6 @@ export class CallListComponent extends BaseViewComponent implements CanComponent
this.motionPdfExport.exportPdfCallList(this.motions);
}
/**
* Function to open a prompt dialog,
* so the user will be warned if he has made changes and not saved them.
*
* @returns The result from the prompt dialog.
*/
public async canDeactivate(): Promise<boolean> {
if (this.hasChanged) {
const title = this.translate.instant('Do you really want to exit this page?');
const content = this.translate.instant('You made changes.');
return await this.promptService.open(title, content);
}
return true;
}
/**
* Get the tags associated with the motion of a sorting item
*
@ -138,4 +226,77 @@ export class CallListComponent extends BaseViewComponent implements CanComponent
const motion = this.motionRepo.getViewModel(item.id);
return motion ? motion.tags : [];
}
/**
* Toggles a filter. An array with the filter ids array will be emitted
* as active/model/Filters
*
* @param model either 'tag' or 'category' for the filter that changed
* @param filter the filter that is supposed to be toggled.
*/
public onFilterChange(model: 'tag' | 'category', filter: number): void {
const value = model === 'tag' ? this.activeTagFilters.value : this.activeCatFilters.value;
if (!value.includes(filter)) {
value.push(filter);
} else {
value.splice(value.indexOf(filter), 1);
}
if (model === 'tag') {
this.activeTagFilters.next(value);
} else {
this.activeCatFilters.next(value);
}
}
/**
* Function to unset all active filters.
*/
public resetFilters(): void {
this.activeTagFilters.next([]);
this.activeCatFilters.next([]);
}
/**
* Helper to trigger an update of the filter itself and the information about
* the state of filters
*
* @param model the property/model the filter is for
* @param value
*/
private onSubscribedFilterChange(model: 'tag' | 'category', value: number[]): void {
if (model === 'tag') {
this.activeTagFilterCount = value.length;
this.tagFilterOptions.forEach(filterOption => {
filterOption.state = value && value.some(v => v === filterOption.id);
});
} else {
this.activeCatFilterCount = value.length;
this.categoryFilterOptions.forEach(filterOption => {
filterOption.state = value && value.some(v => v === filterOption.id);
});
}
this.hasActiveFilter = this.activeCatFilterCount > 0 || this.activeTagFilterCount > 0;
const currentTagFilters = this.tagFilterOptions.filter(option => option.state === true);
const currentCategoryFilters = this.categoryFilterOptions.filter(option => option.state === true);
this.changeFilter.emit(
// TODO this is ugly and potentially reversed
(item: ViewMotion): boolean => {
if (currentTagFilters.length) {
if (!item.tags || !item.tags.length) {
if (!currentTagFilters.some(filter => filter.id === 0)) {
return true;
}
} else if (!item.tags.some(tag => currentTagFilters.some(filter => filter.id === tag.id))) {
return true;
}
}
if (currentCategoryFilters.length) {
const category_id = item.category_id || 0;
return !currentCategoryFilters.some(filter => filter.id === category_id);
}
return false;
}
);
}
}