import './style.scss'; import ngModule from '../../module'; import Component from '../../lib/component'; import ArrayModel from '../array-model/array-model'; import CrudModel from '../crud-model/crud-model'; import {mergeWhere} from 'vn-loopback/common/filter.js'; /** * @event select Thrown when model item is selected * @event change Thrown when model data is ready */ export default class DropDown extends Component { constructor($element, $scope, $transclude, $timeout, $translate, $http, $q, $filter) { super($element, $scope); this.$transclude = $transclude; this.$timeout = $timeout; this.$translate = $translate; this.$http = $http; this.$q = $q; this.$filter = $filter; this.valueField = 'id'; this.showField = 'name'; this._search = undefined; this._activeOption = -1; this.showLoadMore = true; this.showFilter = true; this.docKeyDownHandler = e => this.onDocKeyDown(e); } $postLink() { super.$postLink(); this.input = this.element.querySelector('.search input'); this.ul = this.element.querySelector('ul'); this.list = this.element.querySelector('.list'); this.list.addEventListener('scroll', e => this.onScroll(e)); } get shown() { return this.$.popover && this.$.popover.shown; } set shown(value) { this.$.popover.shown = value; } get search() { return this._search; } set search(value) { let oldValue = this._search; this._search = value; if (!this.shown) return; value = value == '' || value == null ? null : value; oldValue = oldValue == '' || oldValue == null ? null : oldValue; if (value === oldValue && this.modelData != null) return; if (value != null) this._activeOption = 0; this.$timeout.cancel(this.searchTimeout); if (this.model) { this.model.clear(); this.searchTimeout = this.$timeout(() => { this.refreshModel(); this.searchTimeout = null; }, 350); } this.buildList(); } get statusText() { let model = this.model; let data = this.modelData; if (!model) return 'No data'; if (model.isLoading || this.searchTimeout) return 'Loading...'; if (data == null) return 'No data'; if (model.moreRows) return 'Load More'; if (data.length === 0) return 'No results found'; return null; } get activeOption() { return this._activeOption; } /** * Shows the drop-down. If a parent is specified it is shown in a visible * relative position to it. * * @param {HTMLElement} parent The parent element to show drop down relative to * @param {String} search The initial search term or %null */ show(parent, search) { this._activeOption = -1; this.search = search; this.buildList(); this.$.popover.show(parent || this.parent); } /** * Hides the drop-down. */ hide() { this.$.popover.hide(); } /** * Activates an option and scrolls the drop-down to that option. * * @param {Number} option The option index */ moveToOption(option) { this.activateOption(option); let list = this.list; let li = this.activeLi; if (!li) return; let liRect = li.getBoundingClientRect(); let listRect = list.getBoundingClientRect(); if (liRect.bottom > listRect.bottom) list.scrollTop += liRect.bottom - listRect.bottom; else if (liRect.top < listRect.top) list.scrollTop -= listRect.top - liRect.top; } /** * Activates an option. * * @param {Number} option The option index */ activateOption(option) { this._activeOption = option; if (this.activeLi) this.activeLi.className = ''; let data = this.modelData; if (option >= 0 && data && option < data.length) { this.activeLi = this.ul.children[option]; this.activeLi.className = 'active'; } } /** * Selects an option. * * @param {Number} option The option index */ selectOption(option) { let data = this.modelData; let item = option != -1 && data ? data[option] : null; if (item) { let value = item[this.valueField]; if (this.multiple) { if (!Array.isArray(this.selection)) { this.selection = []; this.field = []; } this.selection.push(item); this.field.push(value); } else { this.selection = item; this.field = value; } this.emit('select', {value: value}); } if (!this.multiple) this.$.popover.hide(); } onOpen() { this.document.addEventListener('keydown', this.docKeyDownHandler); this.list.scrollTop = 0; this.input.focus(); } onClose() { this.document.removeEventListener('keydown', this.docKeyDownHandler); } onClearClick() { this.search = null; } onScroll() { let list = this.list; let shouldLoad = list.scrollTop + list.clientHeight >= (list.scrollHeight - 40) && !this.model.isLoading; if (shouldLoad) this.model.loadMore(); } onLoadMoreClick(event) { event.preventDefault(); this.model.loadMore(); } onContainerClick(event) { if (event.defaultPrevented) return; let index = getPosition(this.ul, event); if (index != -1) this.selectOption(index); } onDocKeyDown(event) { if (event.defaultPrevented) return; let data = this.modelData; let option = this.activeOption; let nOpts = data ? data.length - 1 : 0; switch (event.keyCode) { case 9: // Tab this.selectOption(option); return; case 13: // Enter this.selectOption(option); break; case 38: // Up this.moveToOption(option <= 0 ? nOpts : option - 1); break; case 40: // Down this.moveToOption(option >= nOpts ? 0 : option + 1); break; case 35: // End this.moveToOption(nOpts); break; case 36: // Start this.moveToOption(0); break; default: return; } event.preventDefault(); this.$.$applyAsync(); } buildList() { if (!this.shown) return; this.destroyList(); let hasTemplate = this.$transclude && this.$transclude.isSlotFilled('tplItem'); let fragment = this.document.createDocumentFragment(); let data = this.modelData; if (data) { for (let i = 0; i < data.length; i++) { let option = data[i]; if (this.translateFields) { option = Object.assign({}, option); for (let field of this.translateFields) option[field] = this.$translate.instant(option[field]); } let li = this.document.createElement('li'); fragment.appendChild(li); if (this.multiple) { let check = this.document.createElement('input'); check.type = 'checkbox'; li.appendChild(check); if (this.field && this.field.indexOf(option[this.valueField]) != -1) check.checked = true; } if (hasTemplate) { this.$transclude((clone, scope) => { Object.assign(scope, option); li.appendChild(clone[0]); this.scopes[i] = scope; }, null, 'tplItem'); } else { let text = this.document.createTextNode(option[this.showField]); li.appendChild(text); } } } this.ul.appendChild(fragment); this.activateOption(this._activeOption); this.$.$applyAsync(() => this.$.popover.relocate()); } destroyList() { this.ul.innerHTML = ''; if (this.scopes) for (let scope of this.scopes) scope.$destroy(); this.scopes = []; } getFields() { let fields = []; fields.push(this.valueField); fields.push(this.showField); if (this.fields) for (let field of this.fields) fields.push(field); return fields; } $onDestroy() { this.destroyList(); } // Model related code onDataChange() { if (this.model.orgData) this.emit('dataReady'); this.buildList(); } get modelData() { return this._model ? this._model.data : null; } get model() { return this._model; } set model(value) { this.linkEvents({_model: value}, {dataChange: this.onDataChange}); this.onDataChange(); } get url() { return this._url; } set url(value) { this._url = value; if (value) { this.model = new CrudModel(this.$q, this.$http); this.model.autoLoad = false; this.model.url = value; this.model.$onInit(); } } get data() { return this._data; } set data(value) { this._data = value; if (value) { this.model = new ArrayModel(this.$q, this.$filter); this.model.autoLoad = false; this.model.orgData = value; this.model.$onInit(); } } refreshModel() { let model = this.model; let order; if (this.order) order = this.order; else if (this.showField) order = `${this.showField} ASC`; let filter = { order, limit: this.limit || 8 }; if (model instanceof CrudModel) { let searchExpr = this._search == null ? null : this.searchFunction({$search: this._search}); Object.assign(filter, { fields: this.getFields(), include: this.include, where: mergeWhere(this.where, searchExpr) }); } else if (model instanceof ArrayModel) { if (this._search != null) filter.where = this.searchFunction({$search: this._search}); } return this.model.applyFilter(filter); } searchFunction(scope) { if (this.model instanceof CrudModel) return {[this.showField]: {like: `%${scope.$search}%`}}; if (this.model instanceof ArrayModel) return {[this.showField]: scope.$search}; } } DropDown.$inject = ['$element', '$scope', '$transclude', '$timeout', '$translate', '$http', '$q', '$filter']; /** * Gets the position of an event element relative to a parent. * * @param {HTMLElement} parent The parent element * @param {Event} event The event object * @return {Number} The element index */ function getPosition(parent, event) { let target = event.target; let children = parent.children; if (target === parent) return -1; while (target.parentNode !== parent) target = target.parentNode; for (let i = 0; i < children.length; i++) if (children[i] === target) return i; return -1; } ngModule.component('vnDropDown', { template: require('./drop-down.html'), controller: DropDown, bindings: { field: '=?', selection: '=?', search: '