+
+
+
+
+
+
+
+
+
-
-
\ No newline at end of file
+
+ {{$ctrl.statusText}}
+
+
+
\ No newline at end of file
diff --git a/client/core/src/components/drop-down/drop-down.js b/client/core/src/components/drop-down/drop-down.js
old mode 100644
new mode 100755
index 93de8cf15..c1b34a319
--- a/client/core/src/components/drop-down/drop-down.js
+++ b/client/core/src/components/drop-down/drop-down.js
@@ -1,44 +1,48 @@
import ngModule from '../../module';
-import validKey from '../../lib/key-codes';
+import Component from '../../lib/component';
import './style.scss';
+import './model';
-export default class DropDown {
- constructor($element, $filter, $timeout) {
- this.$element = $element;
- this.$filter = $filter;
+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._search = null;
- this.itemsFiltered = [];
+ this.valueField = 'id';
+ this.showField = 'name';
+ this._search = undefined;
+ this._shown = false;
this._activeOption = -1;
- this._focusingFilter = false;
- this._tryToShow = 0;
+ 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);
+
+ this.element.addEventListener('mousedown',
+ e => this.onBackgroundMouseDown(e));
+ this.element.addEventListener('focusin',
+ e => this.onFocusIn(e));
+ this.list.addEventListener('scroll',
+ e => this.onScroll(e));
}
- get container() {
- return this.$element[0].querySelector('ul.dropdown');
+ get shown() {
+ return this._shown;
}
- get show() {
- return this._show;
- }
-
- set show(value) {
- let oldValue = this.show;
- // It wait up to 1 second if the dropdown opens but there is no data to show
- if (value && !oldValue && !this.itemsFiltered.length && this._tryToShow < 4) {
- this.$timeout(() => {
- this._tryToShow++;
- this.show = true;
- if (this.activeOption === -1) {
- this.activeOption = 0;
- }
- }, 250);
- } else {
- this._tryToShow = 0;
- this._show = value;
- this._toggleDropDown(value, oldValue);
- }
+ set shown(value) {
+ if (value)
+ this.show();
+ else
+ this.hide();
}
get search() {
@@ -46,242 +50,390 @@ export default class DropDown {
}
set search(value) {
- let val = (value === undefined || value === '') ? null : value;
- this._search = val;
+ value = value == '' || value == null ? null : value;
+ if (value === this._search) return;
- if (this.filterAction)
- this.onFilterRest();
- else
- this.filterItems();
+ 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;
}
get activeOption() {
return this._activeOption;
}
- set activeOption(value) {
- if (value < 0) {
- value = 0;
- } else if (value >= this.itemsFiltered.length) {
- value = this.showLoadMore ? this.itemsFiltered.length : this.itemsFiltered.length - 1;
- }
- this.$timeout(() => {
- this._activeOption = value;
- if (value && value >= this.itemsFiltered.length - 3 && !this.removeLoadMore) {
- this.loadItems();
- }
- });
+ /**
+ * 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();
}
- _toggleDropDown(value, oldValue) {
- this.$timeout(() => {
- this._eventScroll(value);
- this._calculatePosition(value, oldValue);
- });
+ /**
+ * 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();
}
- _eventScroll(add, num) {
- let count = num || 0;
- if (add) {
- if (this.container) {
- this.container.addEventListener('scroll', e => this.loadFromScroll(e));
- // this.$timeout(() => { // falla al entrar por primera vez xq pierde el foco y cierra el dropdown
- // this._setFocusInFilterInput();
- // });
- } else if (count < 4) {
- count++;
- this.$timeout(() => { // wait angular ngIf
- this._eventScroll(add, count);
- }, 250);
- }
- } else if (this.container) {
- this.container.removeEventListener('scroll', e => this.loadFromScroll(e));
+ /**
+ * 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`;
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Activates a 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.container.children[option];
+ this.activeLi.className = 'active';
}
}
- _setFocusInFilterInput() {
- let inputFilterSearch = this.$element[0].querySelector('input');
- this._focusingFilter = true;
- if (inputFilterSearch)
- this.$timeout(() => {
- inputFilterSearch.focus();
- this._focusingFilter = false;
- }, 250);
- }
+ /**
+ * 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;
- _background(create) {
- let el = document.getElementById('ddBack');
- if (el) {
- el.parentNode.removeChild(el);
- }
+ if (this.multiple) {
+ if (!Array.isArray(this.selection)) {
+ this.selection = [];
+ this.field = [];
+ }
- if (create) {
- el = document.createElement('div');
- el.id = 'ddBack';
- document.body.appendChild(el);
- }
- }
- _calculatePosition(value, oldValue) {
- if (value && !oldValue) {
- if (this.parent === undefined) {
- this.parent = this.$element.parent().parent();
+ this.selection.push(item);
+ this.field.push(value);
+ } else {
+ this.selection = item;
+ this.field = value;
}
- let parentRect = this.parent.getBoundingClientRect();
- let elemetRect = this.$element[0].getBoundingClientRect();
- let instOffset = parentRect.bottom + elemetRect.height;
-
- if (instOffset >= window.innerHeight) {
- this._background(true);
- this.$element.addClass('fixed-dropDown');
- this.$element.css('top', `${(parentRect.top - elemetRect.height)}px`);
- this.$element.css('left', `${(parentRect.x)}px`);
- this.$element.css('height', `${elemetRect.height}px`);
- }
- } else if (!value && oldValue) {
- this.$element.removeAttr('style');
- if (this.itemWidth) {
- this.$element.css('width', this.itemWidth + 'px');
- }
- this.$element.removeClass('fixed-dropDown');
- this._background();
+ if (this.onSelect)
+ this.onSelect({value: value});
}
+
+ if (!this.multiple)
+ this.hide();
}
- filterItems() {
- this.itemsFiltered = this.search ? this.$filter('filter')(this.items, this.search) : this.items;
+ refreshModel() {
+ this.$.model.filter = {
+ fields: this.selectFields,
+ where: this.getWhere(this._search),
+ order: this.getOrder(),
+ limit: this.limit || 8
+ };
+ this.$.model.refresh(this._search);
}
- onFilterRest() {
- if (this.filterAction) {
- this.filterAction({search: 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] = {regexp: search};
+ return where;
}
- clearSearch() {
+ getOrder() {
+ if (this.order)
+ return this.order;
+ else if (this.showField)
+ return `${this.showField} ASC`;
+
+ return undefined;
+ }
+
+ onMouseDown(event) {
+ this.lastMouseEvent = event;
+ }
+
+ onBackgroundMouseDown(event) {
+ if (event != this.lastMouseEvent)
+ this.hide();
+ }
+
+ onFocusIn(event) {
+ this.lastFocusEvent = event;
+ }
+
+ onDocFocusIn(event) {
+ if (event !== this.lastFocusEvent && event.originalTarget !== this.parent)
+ this.hide();
+ }
+
+ onClearClick() {
this.search = null;
}
- selectOption() {
- if (this.activeOption >= 0 && this.activeOption < this.items.length && this.items[this.activeOption]) {
- this.selected = this.items[this.activeOption];
- this.show = false;
- this.clearSearch();
- } else if (this.showLoadMore && this.activeOption === this.items.length) {
- this.loadMore();
- }
+ onScroll() {
+ let list = this.list;
+ let shouldLoad =
+ list.scrollTop + list.clientHeight >= (list.scrollHeight - 40)
+ && !this.$.model.isLoading;
+
+ if (shouldLoad)
+ this.$.model.loadMore();
}
- onKeydown(event) {
- if (this.show) {
- if (event.keyCode === 13) { // Enter
- this.$timeout(() => {
- this.selectOption();
- });
- event.preventDefault();
- } else if (event.keyCode === 27) { // Escape
- this.clearSearch();
- } else if (event.keyCode === 38) { // Arrow up
- this.activeOption--;
- this.$timeout(() => {
- this.setScrollPosition();
- }, 100);
- } else if (event.keyCode === 40) { // Arrow down
- this.activeOption++;
- this.$timeout(() => {
- this.setScrollPosition();
- }, 100);
- } else if (event.keyCode === 35) { // End
- this.activeOption = this.itemsFiltered.length - 1;
- this.$timeout(() => {
- this.setScrollPosition();
- }, 100);
- } else if (event.keyCode === 36) { // Start
- this.activeOption = 0;
- this.$timeout(() => {
- this.setScrollPosition();
- }, 100);
- } else if (this.filter) {
- let oldValue = this.search || '';
- if (validKey(event)) {
- this.search = oldValue + String.fromCharCode(event.keyCode);
- } else if (event.keyCode === 8) { // backSpace
- this.search = oldValue.slice(0, -1);
- }
- } /* else {
- console.error(`Error: keyCode ${event.keyCode} not supported`);
- } */
- }
+ onLoadMoreClick(event) {
+ event.preventDefault();
+ this.$.model.loadMore();
}
- setScrollPosition() {
- let child = this.$element[0].querySelector('ul.dropdown li.active');
- if (child) {
- let childRect = child.getBoundingClientRect();
- let containerRect = this.container.getBoundingClientRect();
- if (typeof child.scrollIntoView === 'function' && (childRect.top > containerRect.top + containerRect.height || childRect.top < containerRect.top)) {
- child.scrollIntoView();
+
+ 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;
+ }
+
+ 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);
+ }
}
}
+
+ this.container.innerHTML = '';
+ this.container.appendChild(fragment);
+ this.activateOption(this._activeOption);
+ this.relocate();
}
- selectItem(item) {
- this.selected = item;
- if (this.multiple) {
- item.checked = !item.checked;
- this.show = true;
- } else {
- this.show = false;
- }
- }
- loadItems() {
- if (this.showLoadMore && this.loadMore) {
- this.loadMore();
- }
- this.show = true;
- }
- loadFromScroll(e) {
- let containerRect = e.target.getBoundingClientRect();
- if (e.target.scrollHeight - e.target.scrollTop - containerRect.height <= 50) {
- this.loadItems();
- }
- }
- $onChanges(changesObj) {
- if (changesObj.show && changesObj.itemWidth && changesObj.itemWidth.currentValue) {
- this.$element.css('width', changesObj.itemWidth.currentValue + 'px');
- }
- if (changesObj.items) {
- this.filterItems();
- }
- }
- $onInit() {
- if (this.parent)
- this.parent.addEventListener('keydown', e => this.onKeydown(e));
+
+ destroyScopes() {
+ if (this.scopes)
+ for (let scope of this.scopes)
+ scope.$destroy();
}
+
$onDestroy() {
- if (this.parent)
- this.parent.removeEventListener('keydown', e => this.onKeydown(e));
+ this.destroyScopes();
}
}
+DropDown.$inject = ['$element', '$scope', '$transclude', '$timeout', '$http', '$translate'];
-DropDown.$inject = ['$element', '$filter', '$timeout'];
+/**
+ * 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: {
- items: '<',
- show: '<',
- filter: '@?',
- selected: '=',
- search: '=?',
- loadMore: '&?',
- removeLoadMore: '',
- filterAction: '&?',
- showLoadMore: '',
- itemWidth: '',
+ field: '=?',
+ data: '',
+ selection: '=?',
+ search: '',
+ limit: '',
+ showFilter: '',
parent: '',
- multiple: ''
+ multiple: '',
+ onSelect: '&?'
},
transclude: {
- vnItem: '?vnItem'
+ tplItem: '?tplItem'
}
});
diff --git a/client/core/src/components/drop-down/drop-down.spec.js b/client/core/src/components/drop-down/drop-down.spec.js
index 13facff6b..fc4028fc8 100644
--- a/client/core/src/components/drop-down/drop-down.spec.js
+++ b/client/core/src/components/drop-down/drop-down.spec.js
@@ -1,9 +1,13 @@
import './drop-down.js';
+import template from './drop-down.html';
describe('Component vnDropDown', () => {
let $componentController;
let $timeout;
let $element;
+ let $scope;
+ let $httpBackend;
+ let $q;
let $filter;
let controller;
@@ -11,337 +15,34 @@ describe('Component vnDropDown', () => {
angular.mock.module('client');
});
- beforeEach(angular.mock.inject((_$componentController_, _$timeout_, _$filter_, _$httpBackend_) => {
- _$httpBackend_.when('GET', /\/locale\/\w+\/[a-z]{2}\.json/).respond({});
+ beforeEach(angular.mock.inject((_$componentController_, $rootScope, _$timeout_, _$httpBackend_, _$q_, _$filter_) => {
$componentController = _$componentController_;
- $element = angular.element('