@jchinc/ng-select
Version:
Control de selección de elementos, con opción de seleccionar varios
659 lines (621 loc) • 23.9 kB
JavaScript
import { Component, Input, Output, EventEmitter, ViewChild, HostListener, Renderer2 } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { NgSelectItem } from './ng-select.models';
import { classes } from './ng-select.constants';
export class NgSelectComponent {
/**
* ---------------------------------------------------------------------------------
* Sección del COMPONENTE
* ---------------------------------------------------------------------------------
*/
constructor(_renderer, _formBuilder) {
this._renderer = _renderer;
this._formBuilder = _formBuilder;
this.filteredItems = [];
this.selectedItemsKeys = '';
this._selectedItems = [];
/**
* Altura de los elementos de la caja de selección.
*/
this._listItemHeight = 44;
/**
* ---------------------------------------------------------------------------------
* Variables INPUT
* ---------------------------------------------------------------------------------
*/
this.source = [];
this.top = 0;
/**
* Para permitir que funcione como un select de 1 sólo registro.
*/
this.isMultiselect = false;
this.inputSearchPlaceHolder = 'Buscar';
this.disabled = false;
this.toggleButtonText = '';
this.noRowsText = 'No existen registros';
this.accentInsensitive = false;
this.toggleButtonClasses = [];
this.maxItemsVisible = 7;
/**
* ---------------------------------------------------------------------------------
* Variables OUTPUT
* ---------------------------------------------------------------------------------
*/
this.selectedItemChanged = new EventEmitter();
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;
}
get _container() {
return this._containerRef.nativeElement;
}
get _input() {
return this._inputRef.nativeElement;
}
get _dropdown() {
return this._dropdownRef.nativeElement;
}
get _dropdownItems() {
return this._dropdownItemsRef.nativeElement;
}
/**
* ---------------------------------------------------------------------------------
* Eventos del HOST
* ---------------------------------------------------------------------------------
*/
/**
* 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._container.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'] || changes['isMultiselect']) {
this._initialize();
}
if (changes['maxItemsVisible']) {
if (!this.maxItemsVisible) {
this.maxItemsVisible = 7;
}
this._setContainerMaxHeight();
}
}
ngOnInit() {
this._initialize();
}
/**
* Selecciona el elemento indicado
* @param item Elemento a seleccionar
*/
selectItem(item) {
// Selección única
if (!this.isMultiselect) {
// Visualiza el texto del elemento seleccionado.
this.selectedItemsKeys = item.value;
// Oculta dropdown.
this._hideDropdown();
// Indica que se seleccionó otro elemento.
if (item !== this.selectedItem) {
this.selectedItemChanged.emit(item);
}
// Item seleccionado.
this.selectedItem = item;
}
else {
// Elemento seleccionado.
item.selected = !item.selected;
// Elementos seleccionados.
this._setSelectedItems();
}
}
toggleButtonClick() {
if (this._dropdown.classList.contains(classes.DROPDOWN_SHOWN)) {
this._hideDropdown();
}
else {
this._showDropdown();
}
}
/**
* Selecciona/desselecciona todos los registros filtrados.
*/
selectUnselectAll() {
this.itemSelectAll.selected = !this.itemSelectAll.selected;
this.filteredItems.forEach(item => {
item.selected = this.itemSelectAll.selected;
});
// Elementos seleccionados.
this._setSelectedItems();
}
clearTerm() {
this.term = '';
this._input.focus();
}
inputKeyup(event) {
let itemsLength = this.filteredItems.length;
if (itemsLength === 0) {
return;
}
let hoveredItemIndex = -1;
if (this.hoveredItem) {
hoveredItemIndex = this.filteredItems.indexOf(this.hoveredItem);
}
switch (event.keyCode) {
case 13:// ENTER
event.preventDefault();
if (this.filteredItems.length > 0 && this.hoveredItem) {
this.selectItem(this.hoveredItem);
}
break;
case 38:// UP
if (hoveredItemIndex > 0) {
// Seleciona el elemento anterior.
hoveredItemIndex -= 1;
}
else {
// Selecciona último elemento.
hoveredItemIndex = itemsLength - 1;
}
this.hoveredItem = this.filteredItems[hoveredItemIndex];
this._scrollToView(hoveredItemIndex);
break;
case 40:// DOWN
if (hoveredItemIndex < (itemsLength - 1)) {
// Selecciona siguiente elemento.
hoveredItemIndex += 1;
}
else {
// Selecciona primer elemento.
hoveredItemIndex = 0;
}
this.hoveredItem = this.filteredItems[hoveredItemIndex];
this._scrollToView(hoveredItemIndex);
break;
}
}
_initialize() {
this.term = '';
this._setContainerMaxHeight();
this.selectedItemsKeys = this.toggleButtonText;
// Inicializa los registros filtrados. Todos los items.
this._filterData();
if (this.isMultiselect) {
this.itemSelectAll = new NgSelectItem('0', 'Seleccionar todo');
this.source.forEach(item => {
item.selected = false;
});
// Marca o asigna elementos seleccionados.
this._setSelectedItems();
}
else {
this.selectedItem = null;
}
}
_setContainerMaxHeight() {
this._renderer.setStyle(this._dropdown, 'max-height', `${this._listItemHeight * (this.maxItemsVisible + 1)}px`);
}
_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._renderer.addClass(this._dropdown, classes.DROPDOWN_SHOWN);
this._input.focus();
setTimeout(() => {
// Cuando NO sea mutiselect, se visualiza el elemento seleccionado.
if (!this.isMultiselect && this.selectedItem) {
let index = this.filteredItems.indexOf(this.selectedItem);
this._scrollToView(index);
//
this.hoveredItem = this.selectedItem;
}
}, 0);
}
_hideDropdown() {
// Inicializa el item sombreado con el teclado. Si se hubiese indicado alguno.
this.hoveredItem = null;
// Oculta el contenedor de items.
this._renderer.removeClass(this._dropdown, classes.DROPDOWN_SHOWN);
}
_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);
// 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) {
const dropdownItems = this._dropdownItems;
// Posición o distancia recorrida del scroll (parte superior del contenido).
const scrollTop = dropdownItems.scrollTop;
// Altura del contenedor + distancia recorrida del scroll.
// Para verificar si el elemento seleccionado está por debajo de éste (incluyendo la altura del item).
const viewport = dropdownItems.offsetHeight + scrollTop;
// Posición superior del elemento seleccionado con respecto a su índice.
const selectedItemTop = this._listItemHeight * index;
// Posición inferior del elemento seleccionado con respecto a su índice.
const selectedItemBottom = selectedItemTop + this._listItemHeight;
// Cuando el elemento seleccionado esté por arriba del espacio desplazado.
if (selectedItemTop < scrollTop) {
dropdownItems.scrollTop = selectedItemTop;
}
else if (selectedItemBottom > viewport) {
dropdownItems.scrollTop = selectedItemBottom - dropdownItems.offsetHeight - 1;
}
}
}
NgSelectComponent.decorators = [
{ type: Component, args: [{
selector: 'ng-select',
template: `
<div #container
class="ng-select"
[class.ng-select--disabled]="disabled">
<!-- Botón toggle -->
<div class="ng-select__toggle-button"
[ngClass]="toggleButtonClasses"
(click)="toggleButtonClick()">
<span class="ng-select__toggle-button-value">{{selectedItemsKeys}}</span>
<span class="ng-select__toggle-button-caret"></span>
</div>
<!-- Elementos -->
<ul #dropdown
class="ng-select__dropdown ng-select__dropdown--raised">
<!-- Elemento: Seleccionar todo -->
<li *ngIf="isMultiselect"
class="ng-select__item ng-select__item--bordered ng-select__item--accent"
(click)="selectUnselectAll()">
<i *ngIf="itemSelectAll.selected"
class="material-icons ng-select__icon">check_box</i>
<i *ngIf="!itemSelectAll.selected"
class="material-icons ng-select__icon">check_box_outline_blank</i>
<div class="ng-select__item-values">
{{itemSelectAll.value}}
</div>
</li>
<!-- Campo búsqueda -->
<form [formGroup]="selectForm">
<li class="ng-select__item ng-select__item--bordered ng-select__item--accent">
<i class="material-icons ng-select__icon">search</i>
<input #input
type="text"
class="ng-select__search"
[placeholder]="inputSearchPlaceHolder"
(keyup)="inputKeyup($event)"
formControlName="term">
<i [style.display]="term?'inherit':'none'"
class="material-icons ng-select__icon ng-select__icon--close"
(click)="clearTerm()">close</i>
</li>
</form>
<!-- Elementos -->
<div #dropdownItems
class="ng-select__items">
<!-- No existen registros -->
<li *ngIf="!filteredItems.length"
class="ng-select__item ng-select__item--no-rows">
{{noRowsText}}
</li>
<!-- Multiselect: Registros filtrados -->
<ng-container *ngIf="isMultiselect">
<li *ngFor="let item of filteredItems"
class="ng-select__item"
[class.ng-select__item--hovered]="item===hoveredItem"
(click)="selectItem(item, $event)">
<!-- Elemento seleccionado -->
<i *ngIf="item.selected"
class="material-icons ng-select__icon">check_box</i>
<!-- Elemento NO seleccionado -->
<i *ngIf="!item.selected"
class="material-icons ng-select__icon">check_box_outline_blank</i>
<!-- Texto -->
<div class="ng-select__item-values">
<span class="ng-select__item-value"
[title]="item.value">
{{item.value}}
</span>
<span *ngIf="item.valueSecondary"
class="ng-select__item-value ng-select__item-value--secondary"
[title]="item.valueSecondary">
{{item.valueSecondary}}
</span>
</div>
</li>
</ng-container>
<!-- Select: Registros filtrados -->
<ng-container *ngIf="!isMultiselect">
<li *ngFor="let item of filteredItems"
class="ng-select__item"
[class.ng-select__item--hovered]="item===hoveredItem && item!==selectedItem"
[class.ng-select__item--selected]="item===selectedItem"
(click)="selectItem(item, $event)">
<!-- Texto -->
<div class="ng-select__item-values">
<span class="ng-select__item-value"
[title]="item.value">
{{item.value}}
</span>
<span *ngIf="item.valueSecondary"
class="ng-select__item-value ng-select__item-value--secondary"
[title]="item.valueSecondary">
{{item.valueSecondary}}
</span>
</div>
</li>
</ng-container>
</div>
</ul>
</div>
`,
styles: [`
.ng-select {
color: #59595A;
position: relative;
font-family: Roboto, "Helvetica Neue", Helvetica, Arial, sans-serif;
width: 100%;
}
.ng-select--disabled {
pointer-events: none;
background-color: rgb(235, 235, 228);
}
.ng-select__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-select__toggle-button-value {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ng-select__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-select__dropdown {
position: absolute;
display: none;
flex-direction: column;
background-color: #FFFFFF;
width: 100%;
min-width: 240px;
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-select__dropdown-container {
display: flex;
flex-direction: column;
}
.ng-select__dropdown--raised {
box-shadow: 0 6px 12px rgba(0, 0, 0, .175);
}
.ng-select__dropdown--shown {
display: flex;
}
.ng-select__items {
overflow-y: auto;
}
.ng-select__item {
display: flex;
align-items: center;
flex-shrink: 0;
padding: 0 10px;
height: 44px;
font-size: 15px;
font-weight: 500;
overflow: hidden;
}
.ng-select__item--selected {
background-color: #338FFF;
font-weight: 700;
color: white;
}
.ng-select__item--bordered {
border-bottom: 1px solid #CCCCCC;
}
.ng-select__item--accent {
background-color: rgba(0, 0, 0, 0.04);
}
.ng-select__item--no-rows {
justify-content: center;
}
.ng-select__item-values {
display: flex;
flex-direction: column;
width: 100%;
padding-left: 10px;
padding-right: 23px;
}
.ng-select__item-value {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.ng-select__item-value--secondary {
font-size: 0.8em;
opacity: 0.6;
}
.ng-select__item--hovered {
background-color: rgba(0, 0, 0, .12);
}
.ng-select__item:hover:not(.ng-select__item--no-rows):not(.ng-select__item--selected) {
background-color: rgba(0, 0, 0, .12);
}
.ng-select__icon {
font-size: 22px;
pointer-events: none;
}
.ng-select__icon--close {
cursor: pointer;
pointer-events: initial;
opacity: 0.8;
}
.ng-select__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 */
NgSelectComponent.ctorParameters = () => [
{ type: Renderer2, },
{ type: FormBuilder, },
];
NgSelectComponent.propDecorators = {
'source': [{ type: Input },],
'top': [{ type: Input },],
'isMultiselect': [{ type: Input },],
'inputSearchPlaceHolder': [{ type: Input },],
'disabled': [{ type: Input },],
'toggleButtonText': [{ type: Input },],
'noRowsText': [{ type: Input },],
'accentInsensitive': [{ type: Input },],
'toggleButtonClasses': [{ type: Input },],
'maxItemsVisible': [{ type: Input },],
'selectedItemChanged': [{ type: Output },],
'selectedItemsChanged': [{ type: Output },],
'selectedItemsKeysChanged': [{ type: Output },],
'_containerRef': [{ type: ViewChild, args: ['container',] },],
'_inputRef': [{ type: ViewChild, args: ['input',] },],
'_dropdownRef': [{ type: ViewChild, args: ['dropdown',] },],
'_dropdownItemsRef': [{ type: ViewChild, args: ['dropdownItems',] },],
'documentClick': [{ type: HostListener, args: ['document:click', ['$event'],] },],
'documentKeyup': [{ type: HostListener, args: ['document:keyup', ['$event'],] },],
};