2018-02-10 15:18:01 +00:00
|
|
|
import ngModule from '../../module';
|
2018-03-09 13:15:30 +00:00
|
|
|
import Component from '../../lib/component';
|
2017-06-13 11:08:06 +00:00
|
|
|
import './style.scss';
|
2018-03-09 13:15:30 +00:00
|
|
|
import './model';
|
2017-06-13 11:08:06 +00:00
|
|
|
|
2018-03-09 13:15:30 +00:00
|
|
|
export default class DropDown extends Component {
|
|
|
|
constructor($element, $scope, $transclude, $timeout, $http, $translate) {
|
|
|
|
super($element, $scope);
|
|
|
|
this.$transclude = $transclude;
|
2017-09-14 11:40:55 +00:00
|
|
|
this.$timeout = $timeout;
|
2018-03-09 13:15:30 +00:00
|
|
|
this.$translate = $translate;
|
2017-09-14 11:40:55 +00:00
|
|
|
|
2018-03-09 13:15:30 +00:00
|
|
|
this.valueField = 'id';
|
|
|
|
this.showField = 'name';
|
|
|
|
this._search = undefined;
|
|
|
|
this._shown = false;
|
2017-09-14 11:40:55 +00:00
|
|
|
this._activeOption = -1;
|
2018-03-09 13:15:30 +00:00
|
|
|
this.showLoadMore = true;
|
|
|
|
this.showFilter = true;
|
|
|
|
|
|
|
|
this.input = this.element.querySelector('.search');
|
|
|
|
this.body = this.element.querySelector('.body');
|
|
|
|
this.container = this.element.querySelector('ul');
|
|
|
|
this.list = this.element.querySelector('.list');
|
|
|
|
|
|
|
|
this.docKeyDownHandler = e => this.onDocKeyDown(e);
|
|
|
|
this.docFocusInHandler = e => this.onDocFocusIn(e);
|
2017-12-12 12:38:23 +00:00
|
|
|
|
2018-03-09 13:15:30 +00:00
|
|
|
this.element.addEventListener('mousedown',
|
|
|
|
e => this.onBackgroundMouseDown(e));
|
|
|
|
this.element.addEventListener('focusin',
|
|
|
|
e => this.onFocusIn(e));
|
|
|
|
this.list.addEventListener('scroll',
|
|
|
|
e => this.onScroll(e));
|
2017-12-12 12:38:23 +00:00
|
|
|
}
|
2017-12-12 12:59:31 +00:00
|
|
|
|
2018-03-09 13:15:30 +00:00
|
|
|
get shown() {
|
|
|
|
return this._shown;
|
2017-09-21 11:10:30 +00:00
|
|
|
}
|
2017-10-04 06:47:16 +00:00
|
|
|
|
2018-03-09 13:15:30 +00:00
|
|
|
set shown(value) {
|
|
|
|
if (value)
|
|
|
|
this.show();
|
|
|
|
else
|
|
|
|
this.hide();
|
2017-10-04 06:47:16 +00:00
|
|
|
}
|
|
|
|
|
2017-09-14 11:40:55 +00:00
|
|
|
get search() {
|
|
|
|
return this._search;
|
|
|
|
}
|
2017-10-04 06:47:16 +00:00
|
|
|
|
2017-09-14 11:40:55 +00:00
|
|
|
set search(value) {
|
2018-03-09 13:15:30 +00:00
|
|
|
value = value == '' || value == null ? null : value;
|
|
|
|
if (value === this._search) return;
|
2017-09-14 11:40:55 +00:00
|
|
|
|
2018-03-09 13:15:30 +00:00
|
|
|
this._search = value;
|
|
|
|
this.$.model.clear();
|
|
|
|
|
|
|
|
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;
|
2017-09-14 11:40:55 +00:00
|
|
|
}
|
2017-10-04 06:47:16 +00:00
|
|
|
|
2017-09-14 11:40:55 +00:00
|
|
|
get activeOption() {
|
|
|
|
return this._activeOption;
|
|
|
|
}
|
2017-10-04 06:47:16 +00:00
|
|
|
|
2018-03-09 13:15:30 +00:00
|
|
|
/**
|
|
|
|
* 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) {
|
|
|
|
if (this._shown) return;
|
|
|
|
this._shown = true;
|
|
|
|
this._activeOption = -1;
|
|
|
|
this.search = search;
|
|
|
|
this.buildList();
|
|
|
|
this.element.style.display = 'block';
|
|
|
|
this.list.scrollTop = 0;
|
|
|
|
this.$timeout(() => this.$element.addClass('shown'), 40);
|
|
|
|
this.document.addEventListener('keydown', this.docKeyDownHandler);
|
|
|
|
this.document.addEventListener('focusin', this.docFocusInHandler);
|
|
|
|
this.relocate();
|
|
|
|
this.input.focus();
|
2017-06-15 05:45:01 +00:00
|
|
|
}
|
2017-06-29 11:56:52 +00:00
|
|
|
|
2018-03-09 13:15:30 +00:00
|
|
|
/**
|
|
|
|
* Hides the drop-down.
|
|
|
|
*/
|
|
|
|
hide() {
|
|
|
|
if (!this._shown) return;
|
|
|
|
this._shown = false;
|
|
|
|
this.element.style.display = '';
|
|
|
|
this.$element.removeClass('shown');
|
|
|
|
this.document.removeEventListener('keydown', this.docKeyDownHandler);
|
|
|
|
this.document.removeEventListener('focusin', this.docFocusInHandler);
|
|
|
|
if (this.parent)
|
|
|
|
this.parent.focus();
|
2017-12-12 12:59:31 +00:00
|
|
|
}
|
|
|
|
|
2018-03-09 13:15:30 +00:00
|
|
|
/**
|
|
|
|
* Repositions the drop-down to a correct location relative to the parent.
|
|
|
|
*/
|
|
|
|
relocate() {
|
|
|
|
if (!this.parent) return;
|
|
|
|
|
|
|
|
let style = this.body.style;
|
|
|
|
style.width = '';
|
|
|
|
style.height = '';
|
|
|
|
|
|
|
|
let parentRect = this.parent.getBoundingClientRect();
|
|
|
|
let bodyRect = this.body.getBoundingClientRect();
|
|
|
|
|
|
|
|
let top = parentRect.top + parentRect.height;
|
|
|
|
let height = bodyRect.height;
|
|
|
|
let width = Math.max(bodyRect.width, parentRect.width);
|
|
|
|
|
|
|
|
let margin = 10;
|
|
|
|
|
|
|
|
if (top + height + margin > window.innerHeight)
|
|
|
|
top = Math.max(parentRect.top - height, margin);
|
|
|
|
|
|
|
|
style.top = `${top}px`;
|
|
|
|
style.left = `${parentRect.left}px`;
|
|
|
|
style.width = `${width}px`;
|
|
|
|
|
|
|
|
if (height + margin * 2 > window.innerHeight)
|
|
|
|
style.height = `${window.innerHeight - margin * 2}px`;
|
2017-11-22 12:10:33 +00:00
|
|
|
}
|
2017-12-12 12:59:31 +00:00
|
|
|
|
2018-03-09 13:15:30 +00:00
|
|
|
/**
|
|
|
|
* Activates a 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;
|
2017-12-12 12:59:31 +00:00
|
|
|
}
|
|
|
|
|
2018-03-09 13:15:30 +00:00
|
|
|
/**
|
|
|
|
* Activates a option.
|
|
|
|
*
|
|
|
|
* @param {Number} option The option index
|
|
|
|
*/
|
|
|
|
activateOption(option) {
|
|
|
|
this._activeOption = option;
|
|
|
|
|
|
|
|
if (this.activeLi)
|
|
|
|
this.activeLi.className = '';
|
2017-11-23 13:07:55 +00:00
|
|
|
|
2018-03-09 13:15:30 +00:00
|
|
|
let data = this.$.model.data;
|
|
|
|
|
|
|
|
if (option >= 0 && data && option < data.length) {
|
|
|
|
this.activeLi = this.container.children[option];
|
|
|
|
this.activeLi.className = 'active';
|
2017-11-23 13:07:55 +00:00
|
|
|
}
|
|
|
|
}
|
2017-11-22 12:10:33 +00:00
|
|
|
|
2018-03-09 13:15:30 +00:00
|
|
|
/**
|
|
|
|
* Selects an option.
|
|
|
|
*
|
|
|
|
* @param {Number} option The option index
|
|
|
|
*/
|
|
|
|
selectOption(option) {
|
|
|
|
if (option != -1) {
|
|
|
|
let data = this.$.model.data;
|
|
|
|
let item = data ? data[option] : null;
|
|
|
|
let value = item ? item[this.valueField] : null;
|
2017-11-22 12:10:33 +00:00
|
|
|
|
2018-03-09 13:15:30 +00:00
|
|
|
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;
|
2017-11-22 12:10:33 +00:00
|
|
|
}
|
2018-03-09 13:15:30 +00:00
|
|
|
|
|
|
|
if (this.onSelect)
|
|
|
|
this.onSelect({value: value});
|
2017-11-22 12:10:33 +00:00
|
|
|
}
|
2018-03-09 13:15:30 +00:00
|
|
|
|
|
|
|
if (!this.multiple)
|
|
|
|
this.hide();
|
2017-11-22 12:10:33 +00:00
|
|
|
}
|
|
|
|
|
2018-03-09 13:15:30 +00:00
|
|
|
refreshModel() {
|
|
|
|
this.$.model.filter = {
|
|
|
|
fields: this.selectFields,
|
|
|
|
where: this.getWhere(this._search),
|
|
|
|
order: this.getOrder(),
|
|
|
|
limit: this.limit || 8
|
|
|
|
};
|
|
|
|
this.$.model.refresh(this._search);
|
2017-06-29 06:13:30 +00:00
|
|
|
}
|
2017-06-29 11:56:52 +00:00
|
|
|
|
2018-03-09 13:15:30 +00:00
|
|
|
getWhere(search) {
|
|
|
|
if (search == '' || search == null)
|
|
|
|
return undefined;
|
|
|
|
|
|
|
|
if (this.where) {
|
|
|
|
let jsonFilter = this.where.replace(/search/g, search);
|
|
|
|
return this.$.$eval(jsonFilter);
|
2017-10-11 10:57:13 +00:00
|
|
|
}
|
2018-03-09 13:15:30 +00:00
|
|
|
|
|
|
|
let where = {};
|
|
|
|
where[this.showField] = {regexp: search};
|
|
|
|
return where;
|
2017-06-29 06:13:30 +00:00
|
|
|
}
|
|
|
|
|
2018-03-09 13:15:30 +00:00
|
|
|
getOrder() {
|
|
|
|
if (this.order)
|
|
|
|
return this.order;
|
|
|
|
else if (this.showField)
|
|
|
|
return `${this.showField} ASC`;
|
|
|
|
|
|
|
|
return undefined;
|
2017-09-14 11:40:55 +00:00
|
|
|
}
|
|
|
|
|
2018-03-09 13:15:30 +00:00
|
|
|
onMouseDown(event) {
|
|
|
|
this.lastMouseEvent = event;
|
2017-06-22 10:03:01 +00:00
|
|
|
}
|
2017-09-14 11:40:55 +00:00
|
|
|
|
2018-03-09 13:15:30 +00:00
|
|
|
onBackgroundMouseDown(event) {
|
|
|
|
if (event != this.lastMouseEvent)
|
|
|
|
this.hide();
|
2017-09-14 11:40:55 +00:00
|
|
|
}
|
2018-03-09 13:15:30 +00:00
|
|
|
|
|
|
|
onFocusIn(event) {
|
|
|
|
this.lastFocusEvent = event;
|
2017-09-20 09:50:53 +00:00
|
|
|
}
|
2018-03-09 13:15:30 +00:00
|
|
|
|
|
|
|
onDocFocusIn(event) {
|
2018-03-09 14:09:54 +00:00
|
|
|
if (event !== this.lastFocusEvent)
|
2018-03-09 13:15:30 +00:00
|
|
|
this.hide();
|
2017-09-20 11:52:53 +00:00
|
|
|
}
|
2018-03-09 13:15:30 +00:00
|
|
|
|
|
|
|
onClearClick() {
|
|
|
|
this.search = null;
|
2017-10-03 07:16:02 +00:00
|
|
|
}
|
2018-03-09 13:15:30 +00:00
|
|
|
|
|
|
|
onScroll() {
|
|
|
|
let list = this.list;
|
|
|
|
let shouldLoad =
|
|
|
|
list.scrollTop + list.clientHeight >= (list.scrollHeight - 40)
|
|
|
|
&& !this.$.model.isLoading;
|
|
|
|
|
|
|
|
if (shouldLoad)
|
|
|
|
this.$.model.loadMore();
|
2017-11-22 12:10:33 +00:00
|
|
|
}
|
2018-03-09 13:15:30 +00:00
|
|
|
|
|
|
|
onLoadMoreClick(event) {
|
|
|
|
event.preventDefault();
|
|
|
|
this.$.model.loadMore();
|
|
|
|
}
|
|
|
|
|
|
|
|
onContainerClick(event) {
|
|
|
|
if (event.defaultPrevented) return;
|
|
|
|
let index = getPosition(this.container, 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 13: // Enter
|
|
|
|
this.selectOption(option);
|
|
|
|
break;
|
|
|
|
case 27: // Escape
|
|
|
|
this.hide();
|
|
|
|
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;
|
2017-11-22 12:10:33 +00:00
|
|
|
}
|
2018-03-09 13:15:30 +00:00
|
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
this.$.$applyAsync();
|
|
|
|
}
|
|
|
|
|
|
|
|
buildList() {
|
|
|
|
this.destroyScopes();
|
|
|
|
this.scopes = [];
|
|
|
|
|
|
|
|
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 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(data[i][this.valueField]) != -1)
|
|
|
|
check.checked = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (hasTemplate) {
|
|
|
|
this.$transclude((clone, scope) => {
|
|
|
|
Object.assign(scope, data[i]);
|
|
|
|
li.appendChild(clone[0]);
|
|
|
|
this.scopes[i] = scope;
|
|
|
|
}, null, 'tplItem');
|
|
|
|
} else {
|
|
|
|
let text = this.document.createTextNode(data[i][this.showField]);
|
|
|
|
li.appendChild(text);
|
|
|
|
}
|
|
|
|
}
|
2017-11-22 12:10:33 +00:00
|
|
|
}
|
2018-03-09 13:15:30 +00:00
|
|
|
|
|
|
|
this.container.innerHTML = '';
|
|
|
|
this.container.appendChild(fragment);
|
|
|
|
this.activateOption(this._activeOption);
|
|
|
|
this.relocate();
|
2017-11-22 12:10:33 +00:00
|
|
|
}
|
2018-03-09 13:15:30 +00:00
|
|
|
|
|
|
|
destroyScopes() {
|
|
|
|
if (this.scopes)
|
|
|
|
for (let scope of this.scopes)
|
|
|
|
scope.$destroy();
|
2017-09-14 11:40:55 +00:00
|
|
|
}
|
2018-03-09 13:15:30 +00:00
|
|
|
|
2017-09-14 11:40:55 +00:00
|
|
|
$onDestroy() {
|
2018-03-09 13:15:30 +00:00
|
|
|
this.destroyScopes();
|
2017-09-14 11:40:55 +00:00
|
|
|
}
|
2017-06-15 05:45:01 +00:00
|
|
|
}
|
2018-03-09 13:15:30 +00:00
|
|
|
DropDown.$inject = ['$element', '$scope', '$transclude', '$timeout', '$http', '$translate'];
|
2017-10-04 06:47:16 +00:00
|
|
|
|
2018-03-09 13:15:30 +00:00
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
}
|
2017-06-13 11:08:06 +00:00
|
|
|
|
2018-02-10 15:18:01 +00:00
|
|
|
ngModule.component('vnDropDown', {
|
2017-06-13 11:08:06 +00:00
|
|
|
template: require('./drop-down.html'),
|
2017-06-15 05:45:01 +00:00
|
|
|
controller: DropDown,
|
2017-06-13 11:08:06 +00:00
|
|
|
bindings: {
|
2018-03-09 13:15:30 +00:00
|
|
|
field: '=?',
|
|
|
|
data: '<?',
|
|
|
|
selection: '=?',
|
|
|
|
search: '<?',
|
|
|
|
limit: '<?',
|
|
|
|
showFilter: '<?',
|
2017-09-20 11:52:53 +00:00
|
|
|
parent: '<?',
|
2018-03-09 13:15:30 +00:00
|
|
|
multiple: '<?',
|
|
|
|
onSelect: '&?'
|
2017-09-20 09:50:53 +00:00
|
|
|
},
|
|
|
|
transclude: {
|
2018-03-09 13:15:30 +00:00
|
|
|
tplItem: '?tplItem'
|
2017-06-21 11:16:37 +00:00
|
|
|
}
|
2017-06-13 11:08:06 +00:00
|
|
|
});
|