import ngModule from '../../module'; import Component from '../../lib/component'; import './style.scss'; //import './model-proxy/model-proxy'; export default class DropDown extends Component { constructor($element, $scope, $transclude, $timeout, $http, $translate) { super($element, $scope); this.$transclude = $transclude; this.$timeout = $timeout; this.$translate = $translate; 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() { this.input = this.element.querySelector('.search'); 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.shown; } set shown(value) { this.$.popover.shown = value; } get search() { return this._search; } set search(value) { value = value == '' || value == null ? null : value; if (value === this._search && this.$.model.data != null) return; this._search = value; this.$.model.clear(); if (value != null) this._activeOption = 0; this.$timeout.cancel(this.searchTimeout); this.searchTimeout = this.$timeout(() => { this.refreshModel(); this.searchTimeout = null; }, 350); this.buildList(); } get statusText() { let model = this.$.model; let data = model.data; let statusText = null; if (model.isLoading || this.searchTimeout) statusText = 'Loading...'; else if (data == null) statusText = 'No data'; else if (model.moreRows) statusText = 'Load More'; else if (data.length === 0) statusText = 'No results found'; return statusText; } get model() { return this.$.model; } 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 {String} search The initial search term or %null */ show(search) { this._activeOption = -1; this.search = search; this.buildList(); this.$.popover.parent = this.parent; this.$.popover.show(); } /** * 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.$.model.data; 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.$.model.data; 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; } if (this.onSelect) this.onSelect({value: value}); } if (!this.multiple) this.$.popover.hide(); } refreshModel() { this.$.model.filter = { fields: this.selectFields, where: this.getWhere(this._search), order: this.getOrder(), limit: this.limit || 8 }; this.$.model.refresh(this._search); } getWhere(search) { if (search == '' || search == null) return undefined; if (this.where) { let jsonFilter = this.where.replace(/search/g, search); return this.$.$eval(jsonFilter); } let where = {}; where[this.showField] = {like: `%${search}%`}; return where; } getOrder() { if (this.order) return this.order; else if (this.showField) return `${this.showField} ASC`; return undefined; } 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); } onModelDataChange() { this.buildList(); } onDocKeyDown(event) { if (event.defaultPrevented) return; let data = this.$.model.data; 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() { this.destroyList(); let hasTemplate = this.$transclude && this.$transclude.isSlotFilled('tplItem'); let fragment = this.document.createDocumentFragment(); let data = this.$.model.data; 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 = []; } $onDestroy() { this.destroyList(); } } DropDown.$inject = ['$element', '$scope', '$transclude', '$timeout', '$http', '$translate']; /** * 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: '=?', data: '