@jchinc/ng-multiselect
Version:
Control de selección múltiple de elementos
559 lines (526 loc) • 20.1 kB
JavaScript
import { Component, Input, Output, EventEmitter, ViewChild, HostListener, Renderer2 } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { NgMultiselectItem } from './ng-multiselect.models';
export class NgMultiselectComponent {
constructor(_renderer, _formBuilder) {
this._renderer = _renderer;
this._formBuilder = _formBuilder;
this.dropdownVisible = false;
this.filteredItems = [];
this.selectedItemsKeys = '';
this._hoveredItemIndex = -1;
this._selectedItems = [];
/**
* Altura de los elementos de la caja de selección.
*/
this._listItemHeight = 44;
this.source = [];
this.top = 0;
/**
* Para permitir que funcione como un select de 1 sólo registro.
*/
this.onlyOneRow = false;
this.inputSearchPlaceHolder = 'Buscar';
this.disabled = false;
this.toggleButtonText = '';
this.noRowsText = 'No existen registros';
this.accentInsensitive = false;
this.toggleButtonClasses = [];
this.selectedItemsChanged = new EventEmitter();
this.selectedItemsKeysChanged = new EventEmitter();
this._createForm();
}
set term(value) {
this.selectForm.get('term').setValue(value);
}
get term() {
return this.selectForm.get('term').value;
}
/**
* Evento click del documento para determinar si se oculta el dropdown de elementos.
* @param event Evento click del mouse
*/
documentClick(event) {
if (!event.target) {
return;
}
// Verifica si el elemento donde se realizó el evento está contenido en el elemento HOST de la directiva.
let contains = this._containerRef.nativeElement.contains(event.target);
if (!contains) {
this._hideDropdown();
}
}
/**
* Evento tecla ESCAPE del documento para determinar si se oculta el dropdown de elementos.
* @param event Evento tecla
*/
documentKeyup(event) {
// Tecla SCAPE.
if (event.keyCode === 27) {
this._hideDropdown();
}
}
ngOnChanges(changes) {
// Si cambia el origen de datos del control se reinicia.
if (changes['source']) {
this._initialize();
}
}
ngOnInit() {
this._initialize();
}
/**
* Selecciona el elemento indicado
* @param item Elemento a seleccionar
*/
selectItem(item) {
// Deselecciona cualquier elemento seleccionado.
if (this.onlyOneRow) {
this.source.forEach(item => {
item.selected = false;
});
}
// Elemento seleccionado.
item.selected = !item.selected;
// Elementos seleccionados.
this._setSelectedItems();
// Inicializa el item sombreado con el teclado. Si se hubiese indicado alguno.
this._hoveredItemIndex = -1;
this.hoveredItem = null;
if (this.onlyOneRow) {
this._hideDropdown();
}
}
toggleButtonClick() {
if (this.dropdownVisible) {
this._hideDropdown();
}
else {
this._showDropdown();
}
}
/**
* Selecciona/desselecciona todos los registros filtrados.
*/
selectUnselectAll() {
this.itemAll.selected = !this.itemAll.selected;
this.filteredItems.forEach(item => {
item.selected = this.itemAll.selected;
});
// Elementos seleccionados.
this._setSelectedItems();
}
clearTerm() {
this.term = '';
this._inputRef.nativeElement.focus();
}
inputKeyup(event) {
let itemsLength = this.filteredItems.length;
if (itemsLength === 0) {
return;
}
switch (event.keyCode) {
case 13:// ENTER
event.preventDefault();
if (this.filteredItems.length > 0 && this._hoveredItemIndex !== -1) {
this.selectItem(this.hoveredItem);
}
break;
case 38:// UP
if (this._hoveredItemIndex > 0) {
// Seleciona el elemento anterior.
this._hoveredItemIndex -= 1;
}
else {
// Selecciona último elemento.
this._hoveredItemIndex = itemsLength - 1;
}
this.hoveredItem = this.filteredItems[this._hoveredItemIndex];
this._scrollToView(this._hoveredItemIndex);
break;
case 40:// DOWN
if (this._hoveredItemIndex < (itemsLength - 1)) {
// Selecciona siguiente elemento.
this._hoveredItemIndex += 1;
}
else {
// Selecciona primer elemento.
this._hoveredItemIndex = 0;
}
this.hoveredItem = this.filteredItems[this._hoveredItemIndex];
this._scrollToView(this._hoveredItemIndex);
break;
}
}
_initialize() {
this.itemAll = new NgMultiselectItem('0', 'Seleccionar todo');
this.term = '';
this.selectedItemsKeys = this.toggleButtonText;
// Inicializa los registros filtrados. Todos los items.
this._filterData();
// Marca o asigna elementos seleccionados.
this._setSelectedItems();
}
_createForm() {
// Control para captura. Término de búsqueda.
let term = this._formBuilder.control('');
term.valueChanges
.debounceTime(50)
.subscribe(value => {
this._filterData(value);
});
// Formulario.
this.selectForm = this._formBuilder.group({
term: term
});
}
_filterData(term) {
// Límite de registros filtrados.
let top = (this.top > 0) ? this.top : this.source.length;
// En caso de que el término de búsqueda sea vacío, devuelve toda la lista original (limitado por top si aplica).
if (!term) {
this.filteredItems = this.source.slice(0, top);
return;
}
this.filteredItems.length = 0;
for (let item of this.source) {
// Verifica si el término de búsqueda corresponde con alguno de los campos (value y campos adicionales de búsqueda).
if (this._match(item, term)) {
this.filteredItems.push(item);
}
// Verifica si los registros filtrados han llegado al límite establecido.
// Dejaría de verificar registros coincidentes.
if (this.filteredItems.length === top) {
break;
}
}
}
_match(item, term) {
// Por defecto el registro no coincide.
let match = false;
// Variable local para optimizar proceso.
const localTerm = this.accentInsensitive ? _.deburr(term.toLowerCase()) : term.toLowerCase();
// Verifica si corresponde con el campo value del item.
const localItemValue = this.accentInsensitive ? _.deburr(item.value.toLowerCase()) : item.value.toLowerCase();
if (localItemValue.indexOf(localTerm) !== -1) {
match = true;
}
else if (item.filters) {
for (let filter of item.filters) {
// No considera valores vacíos.
if (!filter) {
continue;
}
const filterValue = (this.accentInsensitive ? _.deburr(filter.toLowerCase()) : filter.toLowerCase());
if (filterValue.indexOf(localTerm) !== -1) {
// Retornaría que sí coincide la búsqueda en caso de que algún valor de filtro coíncida.
match = true;
break;
}
}
}
return match;
}
_showDropdown() {
this.dropdownVisible = true;
this._renderer.setStyle(this._dropdownRef.nativeElement, 'display', 'flex');
this._inputRef.nativeElement.focus();
}
_hideDropdown() {
this.dropdownVisible = false;
this._renderer.setStyle(this._dropdownRef.nativeElement, 'display', 'none');
}
_setSelectedItems() {
// Número de registros seleccionados previamente.
let selectedItemsLength = this._selectedItems.length;
this._selectedItems = [];
this.selectedItemsKeys = '';
// Elementos seleccionados.
this.source
.filter(item => item.selected)
.forEach(item => {
this._selectedItems.push(item);
if (this.onlyOneRow) {
// Visualiza el texto del elemento seleccionado.
this.selectedItemsKeys = item.value;
selectedItemsLength = 0;
}
else {
// Visualiza los registros seleccionados.
this.selectedItemsKeys =
this.selectedItemsKeys +
(this.selectedItemsKeys.length ? ',' : '') +
item.key;
}
});
// En caso de que no haya elemento seleccionado se coloca el texto especificado para el botón toggle.
if (this._selectedItems.length === 0) {
this.selectedItemsKeys = this.toggleButtonText;
}
// Indica un cambio de registros seleccionados.
if (selectedItemsLength !== this._selectedItems.length) {
this.selectedItemsChanged.emit(this._selectedItems);
}
}
/**
* Ajusta el scroll para visualizar el elemento actualmente seleccionado
*/
_scrollToView(index) {
if (!this._dropdownItemsRef) {
return;
}
const dropdownItems = this._dropdownItemsRef.nativeElement;
const scrollTop = dropdownItems.scrollTop;
const viewport = scrollTop + dropdownItems.offsetHeight;
const scrollOffset = this._listItemHeight * index;
// scrollOffset < scrollTop : Cuando el elemento seleccionado esté por arriba del espacio desplazado.
// (scrollOffset + this.listItemHeight) > viewport : Cuando el elemento seleccionado esté por abajo del espacio desplazado + altura del espacio de visualización.
if (scrollOffset < scrollTop || (scrollOffset + this._listItemHeight) > viewport) {
dropdownItems.scrollTop = scrollOffset;
}
}
}
NgMultiselectComponent.decorators = [
{ type: Component, args: [{
selector: 'ng-multiselect',
template: `
<div #container
class="ng-multiselect"
[class.ng-multiselect--disabled]="disabled">
<!-- Botón toggle -->
<div class="ng-multiselect__toggle-button"
[ngClass]="toggleButtonClasses"
(click)="toggleButtonClick()">
<span class="ng-multiselect__toggle-button-value">{{selectedItemsKeys}}</span>
<span class="ng-multiselect__toggle-button-caret"></span>
</div>
<!-- Elementos -->
<ul #dropdown
class="ng-multiselect__dropdown ng-multiselect__dropdown--raised">
<!-- Elemento: Seleccionar todo -->
<li *ngIf="!onlyOneRow"
class="ng-multiselect__item ng-multiselect__item--bordered ng-multiselect__item--accent"
(click)="selectUnselectAll()">
<i *ngIf="itemAll.selected"
class="material-icons ng-multiselect__icon">check_box</i>
<i *ngIf="!itemAll.selected"
class="material-icons ng-multiselect__icon">check_box_outline_blank</i>
<div class="ng-multiselect__item-values">
{{itemAll.value}}
</div>
</li>
<!-- Campo búsqueda -->
<form [formGroup]="selectForm">
<li class="ng-multiselect__item ng-multiselect__item--bordered ng-multiselect__item--accent">
<i class="material-icons ng-multiselect__icon">search</i>
<input #input
type="text"
class="ng-multiselect__search"
[placeholder]="inputSearchPlaceHolder"
(keyup)="inputKeyup($event)"
formControlName="term">
<i [style.display]="term?'inherit':'none'"
class="material-icons ng-multiselect__icon ng-multiselect__icon--close"
(click)="clearTerm()">close</i>
</li>
</form>
<!-- Elementos -->
<div #dropdownItems
class="ng-multiselect__items">
<!-- No existen registros -->
<li *ngIf="!filteredItems.length"
class="ng-multiselect__item ng-multiselect__item--no-rows">
{{noRowsText}}
</li>
<!-- Registros filtrados -->
<li *ngFor="let item of filteredItems"
class="ng-multiselect__item"
[class.ng-multiselect__item--selected]="!onlyOneRow && item===hoveredItem"
[class.ng-multiselect__item--selected]="onlyOneRow && (item.selected || item===hoveredItem)"
(click)="selectItem(item, $event)">
<!-- Elemento seleccionado -->
<i *ngIf="item.selected && !onlyOneRow"
class="material-icons ng-multiselect__icon">check_box</i>
<!-- Elemento NO seleccionado -->
<i *ngIf="!item.selected && !onlyOneRow"
class="material-icons ng-multiselect__icon">check_box_outline_blank</i>
<!-- Texto -->
<div class="ng-multiselect__item-values">
<span class="ng-multiselect__item-value"
[title]="item.value">
{{item.value}}
</span>
<span *ngIf="item.valueSecondary"
class="ng-multiselect__item-value ng-multiselect__item-value--secondary"
[title]="item.valueSecondary">
{{item.valueSecondary}}
</span>
</div>
</li>
</div>
</ul>
</div>
`,
styles: [`
.ng-multiselect {
color: #59595A;
position: relative;
font-family: Roboto, "Helvetica Neue", Helvetica, Arial, sans-serif;
width: 100%;
}
.ng-multiselect--disabled {
pointer-events: none;
background-color: rgb(235, 235, 228);
}
.ng-multiselect__toggle-button {
display: flex;
align-items: center;
justify-content: space-between;
height: 30px;
padding: 5px;
border: 1px solid #CCC;
border-radius: 2px;
cursor: pointer;
}
.ng-multiselect__toggle-button-value {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ng-multiselect__toggle-button-caret {
width: 0;
height: 0;
border-top: 4px solid;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
margin-left: 11px;
margin-right: 6px;
}
.ng-multiselect__dropdown {
position: absolute;
display: none;
flex-direction: column;
background-color: #FFFFFF;
width: 100%;
min-width: 240px;
max-height: 360px;
list-style-type: none;
margin-top: 2px;
padding: 0;
text-align: left;
font-weight: 500;
border: 1px solid #CCCCCC;
animation: slideDown .1s;
cursor: default;
z-index: 1000;
}
.ng-multiselect__dropdown-container {
display: flex;
flex-direction: column;
}
.ng-multiselect__dropdown--raised {
box-shadow: 0 6px 12px rgba(0, 0, 0, .175);
}
.ng-multiselect__items {
overflow-y: auto;
}
.ng-multiselect__item {
display: flex;
align-items: center;
flex-shrink: 0;
padding: 0 10px;
height: 44px;
font-size: 15px;
font-weight: 500;
overflow: hidden;
}
.ng-multiselect__item--selected {
background-color: #338FFF;
font-weight: 700;
color: white;
}
.ng-multiselect__item--bordered {
border-bottom: 1px solid #CCCCCC;
}
.ng-multiselect__item--accent {
background-color: rgba(0, 0, 0, 0.04);
}
.ng-multiselect__item--no-rows {
justify-content: center;
}
.ng-multiselect__item-values {
display: flex;
flex-direction: column;
width: 100%;
padding-left: 10px;
padding-right: 23px;
}
.ng-multiselect__item-value {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ng-multiselect__item-value--secondary {
font-size: 0.8em;
opacity: 0.6;
}
.ng-multiselect__item:hover:not(.ng-multiselect__item--no-rows):not(.ng-multiselect__item--selected) {
background-color: rgba(0, 0, 0, .12);
}
.ng-multiselect__icon {
font-size: 22px;
pointer-events: none;
}
.ng-multiselect__icon--close {
cursor: pointer;
pointer-events: initial;
opacity: 0.8;
}
.ng-multiselect__search {
width: 100%;
padding: 10px 3px 10px 10px;
border: none;
background-color: transparent;
outline-style: none;
color: inherit;
font-size: inherit;
font-weight: 400;
}
/*
Animación al visualizar el dropdown.
*/
slideDown {
0% {
transform: translateY(-10px);
}
100% {
transform: translateY(0px);
}
}
`]
},] },
];
/** @nocollapse */
NgMultiselectComponent.ctorParameters = () => [
{ type: Renderer2, },
{ type: FormBuilder, },
];
NgMultiselectComponent.propDecorators = {
'source': [{ type: Input },],
'top': [{ type: Input },],
'onlyOneRow': [{ type: Input },],
'inputSearchPlaceHolder': [{ type: Input },],
'disabled': [{ type: Input },],
'toggleButtonText': [{ type: Input },],
'noRowsText': [{ type: Input },],
'accentInsensitive': [{ type: Input },],
'toggleButtonClasses': [{ type: Input },],
'selectedItemsChanged': [{ type: Output },],
'selectedItemsKeysChanged': [{ type: Output },],
'_containerRef': [{ type: ViewChild, args: ['container',] },],
'_dropdownRef': [{ type: ViewChild, args: ['dropdown',] },],
'_dropdownItemsRef': [{ type: ViewChild, args: ['dropdownItems',] },],
'_inputRef': [{ type: ViewChild, args: ['input',] },],
'documentClick': [{ type: HostListener, args: ['document:click', ['$event'],] },],
'documentKeyup': [{ type: HostListener, args: ['document:keyup', ['$event'],] },],
};