UNPKG

@jchinc/ng-select

Version:

Control de selección de elementos, con opción de seleccionar varios

659 lines (621 loc) 23.9 kB
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. */ @keyframes 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'],] },], };