diff --git a/client/client/src/basic-data/basic-data.html b/client/client/src/basic-data/basic-data.html index 15b2c981da..2ba2c294ae 100644 --- a/client/client/src/basic-data/basic-data.html +++ b/client/client/src/basic-data/basic-data.html @@ -22,7 +22,7 @@ - {{::i.name}} {{::i.surname}} + {{$parent.$parent.item.name}} {{$parent.$parent.item.surname}} diff --git a/client/client/src/fiscal-data/fiscal-data.html b/client/client/src/fiscal-data/fiscal-data.html index 3fbf66d902..a9aa7f7b06 100644 --- a/client/client/src/fiscal-data/fiscal-data.html +++ b/client/client/src/fiscal-data/fiscal-data.html @@ -20,7 +20,7 @@ - - - - + + + {{$parent.item.name}} + \ No newline at end of file diff --git a/client/core/src/autocomplete/autocomplete.js b/client/core/src/autocomplete/autocomplete.js index d4d17a54c0..d5e2d7e713 100644 --- a/client/core/src/autocomplete/autocomplete.js +++ b/client/core/src/autocomplete/autocomplete.js @@ -2,320 +2,136 @@ import {module} from '../module'; import Component from '../lib/component'; import './style.scss'; -/** - * Combobox like component with search and partial data loading features. - */ -export default class Autocomplete extends Component { - constructor($element, $scope, $http, vnPopover, $transclude, $timeout) { +class Autocomplete extends Component { + constructor($element, $scope, $http, $timeout) { super($element); - this.input = $element[0].querySelector('input'); - this.item = null; - this.$timeout = $timeout; - // this.data = null; - this.popover = null; - this.popoverId = null; - this.displayData = null; - this.timeoutId = null; - this.lastSearch = null; - this.lastRequest = null; - this.currentRequest = null; - this.moreData = false; - this.activeOption = -1; - this.locked = false; - this.$http = $http; + this.$element = $element; this.$scope = $scope; - this.vnPopover = vnPopover; - this.$transclude = $transclude; - this.scopes = null; + this.$http = $http; + this.$timeout = $timeout; - Object.assign(this, { - maxRows: 10, - requestDelay: 350, - showField: 'name', - valueField: 'id', - itemAs: 'i' - }); + this._showDropDown = false; + this.finding = false; + this.findMore = false; + this._value = null; + this._field = null; + this._preLoad = false; + this.maxRow = 10; + this.showField = this.showField || 'name'; + this.valueField = this.valueField || 'id'; + this.items = this.data || []; + this.displayValueMultiCheck = []; + this._multiField = []; + } - componentHandler.upgradeElement($element[0].firstChild); + get showDropDown() { + return this._showDropDown; + } + set showDropDown(value) { + if (value && this.url && !this._preLoad) { + this._preLoad = true; + this.getItems(); + } + this._showDropDown = value; + } + + get displayValue() { + return this._value; + } + + set displayValue(value) { + let val = (value === undefined || value === '') ? null : value; + if (this.multiple && val) { + let index = this.displayValueMultiCheck.indexOf(val); + if (index === -1) + this.displayValueMultiCheck.push(val); + else + this.displayValueMultiCheck.splice(index, 1); + + this._value = this.displayValueMultiCheck.join(', '); + } else { + this._value = val; + } + + if (value === null) { + this.field = null; + if (this.multiple && this.items.length) { + this.displayValueMultiCheck = []; + this.items.map(item => { + item.checked = false; + return item; + }); + } + } + } + + get field() { + return this.multiple ? this._multiField : this._field; } set field(value) { - this.locked = true; - this.setValue(value); - this.locked = false; - } - get field() { - return this.value; + this.finding = true; + if (value && value.hasOwnProperty(this.valueField)) { + this._field = value[this.valueField]; + this.setMultiField(value[this.valueField]); + } else { + this.setValue(value); + } + + if (value && value.hasOwnProperty(this.showField)) + this.displayValue = value[this.showField]; + + this.finding = false; + + if (this.onChange) + this.onChange({item: this._field}); } + set initialData(value) { if (value) { - if (!this.data) - this.data = []; - this.data.push(value); + this.field = value; } } - set selectFields(value) { - this._selectFields = []; - if (!value) - return; - - let res = value.split(','); - for (let i of res) - this._selectFields.push(i.trim()); - } - mdlUpdate() { - let mdlField = this.element.firstChild.MaterialTextfield; - if (mdlField) - mdlField.updateClasses_(); - } - loadData(textFilter) { - textFilter = textFilter ? textFilter : ''; - - if (this.lastSearch === textFilter) { - this.showPopoverIfFocus(); - return; + setMultiField(val) { + if (val && typeof val === 'object' && val[this.valueField]) { + val = val[this.valueField]; } - - this.lastSearch = textFilter; - - let lastRequest = this.lastRequest; - let requestWillSame = lastRequest !== null - && !this.moreData - && textFilter.substr(0, lastRequest.length) === lastRequest; - - if (requestWillSame || !this.url) - this.localFilter(textFilter); - else if (this.url) - this.requestData(textFilter, false); - else - this.setDisplayData(this.data); - } - getRequestFields() { - let fields = {}; - fields[this.valueField] = true; - fields[this.showField] = true; - - if (this._selectFields) - for (let field of this._selectFields) - fields[field] = true; - - return fields; - } - requestData(textFilter, append) { - let where = {}; - let skip = 0; - - if (textFilter) - where[this.showField] = {regexp: textFilter}; - if (append && this.data) - skip = this.data.length; - - let filter = { - fields: this.getRequestFields(), - where: where, - order: `${this.showField} ASC`, - skip: skip, - limit: this.maxRows - }; - - this.lastRequest = textFilter ? textFilter : ''; - let json = JSON.stringify(filter); - - if (this.currentRequest) - this.currentRequest.resolve(); - - this.currentRequest = this.$http.get(`${this.url}?filter=${json}`); - this.currentRequest.then( - json => this.onRequest(json.data, append), - json => this.onRequest([]) - ); - } - onRequest(data, append) { - this.currentRequest = null; - this.moreData = data.length >= this.maxRows; - - if (!append || !this.data) - this.data = data; - else - this.data = this.data.concat(data); - - this.setDisplayData(this.data); - } - localFilter(textFilter) { - let regex = new RegExp(textFilter, 'i'); - let data = this.data.filter(item => { - return regex.test(item[this.showField]); - }); - this.setDisplayData(data); - } - setDisplayData(data) { - this.displayData = data; - this.showPopoverIfFocus(); - } - showPopoverIfFocus() { - if (this.hasFocus) - this.showPopover(); - } - destroyScopes() { - if (this.scopes) - for (let scope of this.scopes) - scope.$destroy(); - } - showPopover() { - let data = this.displayData; - - if (!data) - return; - - let fragment = this.document.createDocumentFragment(); - this.destroyScopes(); - this.scopes = []; - - let hasTemplate = this.$transclude.isSlotFilled('tplItem'); - - for (let i = 0; i < data.length; i++) { - let li = this.document.createElement('li'); - fragment.appendChild(li); - - if (hasTemplate) { - this.$transclude((clone, scope) => { - scope[this.itemAs] = 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); - } - } - - if (this.moreData) { - let li = this.document.createElement('li'); - li.appendChild(this.document.createTextNode('Load more')); - li.className = 'load-more'; - fragment.appendChild(li); - } - - if (this.popover) { - this.popover.innerHTML = ''; - this.popover.appendChild(fragment); + if (val === null) { + this._multiField = []; } else { - let popover = this.document.createElement('ul'); - popover.addEventListener('click', - e => this.onPopoverClick(e)); - popover.addEventListener('mousedown', - e => this.onPopoverMousedown(e)); - popover.className = 'vn-autocomplete'; - popover.appendChild(fragment); - this.popoverId = this.vnPopover.show(popover, this.input); - this.popover = popover; + let index = this._multiField.indexOf(val); + if (index === -1) { + this._multiField.push(val); + } else { + this._multiField.splice(index, 1); + } } } - hidePopover() { - if (!this.popover) return; - this.activeOption = -1; - this.vnPopover.hide(this.popoverId); - this.destroyScopes(); - this.popover = null; - } - selectPopoverOption(index) { - if (!this.popover || index === -1) return; - if (index < this.displayData.length) { - this.selectOptionByDataIndex(this.displayData, index); - this.hidePopover(); - } else - this.requestData(this.lastRequest, true); - } - onPopoverClick(event) { - let target = event.target; - let childs = this.popover.childNodes; - if (target === this.popover) - return; + setValue(value) { + if (value) { + let data = this.items; - while (target.parentNode !== this.popover) - target = target.parentNode; + if (data && data.length) + for (let i = 0; i < data.length; i++) + if (data[i][this.valueField] === value) { + this.showItem(data[i]); + return; + } - for (let i = 0; i < childs.length; i++) - if (childs[i] === target) { - this.selectPopoverOption(i); - break; - } - } - onPopoverMousedown(event) { - // Prevents input from loosing focus - event.preventDefault(); - } - onClear() { - this.setValue(null); - this.$timeout( - () => { - this.mdlUpdate(); - } - ); - } - onClick(event) { - if (!this.popover) - this.showPopover(); - } - onFocus() { - this.hasFocus = true; - this.input.select(); - - this.loadData(); - } - onBlur() { - this.hasFocus = false; - this.restoreShowValue(); - this.hidePopover(); - } - onKeydown(event) { - switch (event.keyCode) { - case 13: // Enter - this.selectPopoverOption(this.activeOption); - break; - case 27: // Escape - this.restoreShowValue(); - this.input.select(); - break; - case 38: // Arrow up - this.activateOption(this.activeOption - 1); - break; - case 40: // Arrow down - this.activateOption(this.activeOption + 1); - break; - default: - return; + this.requestItem(value); + } else { + this._field = null; + this.setMultiField(null); + this.displayValue = ''; } + } - event.preventDefault(); - } - onKeyup(event) { - if (!this.isKeycodePrintable(event.keyCode)) return; - if (this.timeoutId) clearTimeout(this.timeoutId); - this.timeoutId = setTimeout(() => this.onTimeout(), this.requestDelay); - } - onTimeout() { - this.loadData(this.input.value); - this.timeoutId = null; - } - isKeycodePrintable(keyCode) { - return keyCode === 32 // Spacebar - || keyCode === 8 // Backspace - || (keyCode > 47 && keyCode < 58) // Numbers - || (keyCode > 64 && keyCode < 91) // Letters - || (keyCode > 95 && keyCode < 112) // Numpad - || (keyCode > 185 && keyCode < 193) // ;=,-./` - || (keyCode > 218 && keyCode < 223); // [\]' - } - restoreShowValue() { - this.putItem(this.item); - } - requestItem() { - if (!this.value) return; + requestItem(value) { + if (!value) return; let where = {}; - where[this.valueField] = this.value; + where[this.valueField] = value; let filter = { fields: this.getRequestFields(), @@ -335,87 +151,135 @@ export default class Autocomplete extends Component { else this.showItem(null); } - activateOption(index) { - if (!this.popover) - this.showPopover(); - let popover = this.popover; - let childs = popover.childNodes; - let len = this.displayData.length; + showItem(item) { + this.displayValue = item ? item[this.showField] : ''; + this.field = item; + this.setMultiField(item); + } - if (this.activeOption >= 0) - childs[this.activeOption].className = ''; + getRequestFields() { + let fields = {}; + fields[this.valueField] = true; + fields[this.showField] = true; - if (index >= len) - index = 0; - else if (index < 0) - index = len - 1; + if (this._selectFields) + for (let field of this._selectFields) + fields[field] = true; - if (index >= 0) { - let opt = childs[index]; - let top = popover.scrollTop; - let height = popover.clientHeight; + return fields; + } - if (opt.offsetTop + opt.offsetHeight > top + height) - top = opt.offsetTop + opt.offsetHeight - height; - else if (opt.offsetTop < top) - top = opt.offsetTop; + findItems(search) { + if (!this.url) + return this.items ? this.items : []; - opt.className = 'active'; - popover.scrollTop = top; + if (search && !this.finding) { + this.maxRow = false; + let filter = {where: {name: {regexp: search}}}; + let json = JSON.stringify(filter); + this.finding = true; + this.$http.get(`${this.url}?filter=${json}`).then( + json => { + this.items = []; + json.data.forEach( + el => { + if (this.multiple) { + el.checked = this.field.indexOf(el[this.valueField]) !== -1; + } + this.items.push(el); + } + ); + this.finding = false; + }, + () => { + this.finding = false; + } + ); + } else if (!search && !this.finding) { + this.maxRow = 10; + this.items = []; + this.getItems(); + } + } + getItems() { + let filter = {}; + + if (this.maxRow) { + if (this.items) { + filter.skip = this.items.length; + } + filter.limit = this.maxRow; + filter.order = 'name ASC'; } - this.activeOption = index; - } - setValue(value) { - this.value = value; + let json = JSON.stringify(filter); - if (value) { - let data = this.data; + this.$http.get(`${this.url}?filter=${json}`).then( + json => { + if (json.data.length) + json.data.forEach( + el => { + if (this.multiple) { + el.checked = this.field.indexOf(el[this.valueField]) !== -1; + } + this.items.push(el); + } + ); + else + this.maxRow = false; + } + ); + } + $onInit() { + this.findMore = this.url && this.maxRow; + this.mouseFocus = false; + this.focused = false; - if (data) - for (let i = 0; i < data.length; i++) - if (data[i][this.valueField] == value) { - this.putItem(data[i]); - return; - } + this.$element.bind('mouseover', e => { + this.$timeout(() => { + this.mouseFocus = true; + this.showDropDown = this.focused; + }); + }); - this.requestItem(); - } else - this.putItem(null); - } - selectOptionByIndex(index) { - this.selectOptionByDataIndex(this.data, index); - } - selectOptionByDataIndex(data, index) { - if (data && index >= 0 && index < data.length) - this.putItem(data[index]); - else - this.putItem(null); - } - putItem(item) { - this.showItem(item); - let value = item ? item[this.valueField] : undefined; + this.$element.bind('mouseout', () => { + this.$timeout(() => { + this.mouseFocus = false; + this.showDropDown = this.focused; + }); + }); + this.$element.bind('focusin', e => { + this.$timeout(() => { + this.focused = true; + this.showDropDown = true; + }); + }); + this.$element.bind('focusout', e => { + this.$timeout(() => { + this.focused = false; + this.showDropDown = this.mouseFocus; + }); + }); - if (!this.locked) - this.value = value; + let rectangle = this.$element[0].getBoundingClientRect(); + this.width = Math.round(rectangle.width) - 10; + } - if (this.onChange) - this.onChange({item: item}); - } - showItem(item) { - this.input.value = item ? item[this.showField] : ''; - this.item = item; - this.mdlUpdate(); - } $onDestroy() { - this.destroyScopes(); + this.$element.unbind('mouseover'); + this.$element.unbind('mouseout'); + this.$element.unbind('focusin'); + this.$element.unbind('focusout'); } + } -Autocomplete.$inject = ['$element', '$scope', '$http', 'vnPopover', '$transclude', '$timeout']; + +Autocomplete.$inject = ['$element', '$scope', '$http', '$timeout']; module.component('vnAutocomplete', { template: require('./autocomplete.html'), + controller: Autocomplete, bindings: { url: '@?', showField: '@?', @@ -426,10 +290,11 @@ module.component('vnAutocomplete', { data: ' { - describe('Component mdlUpdate', () => { - let $componentController; - let $state; - - beforeEach(() => { - angular.mock.module('client'); - }); - - beforeEach(angular.mock.inject((_$componentController_, _$state_) => { - $componentController = _$componentController_; - $state = _$state_; - $state.params.id = '1234'; - })); - - it('should define and set address property', () => { - let controller = $componentController('vnAddressCreate', {$state: $state}); - - expect(controller.address.clientFk).toBe(1234); - expect(controller.address.enabled).toBe(true); - }); - }); -}); diff --git a/client/core/src/autocomplete/style.scss b/client/core/src/autocomplete/style.scss index 5c4dd1a4b7..1472dbcf36 100644 --- a/client/core/src/autocomplete/style.scss +++ b/client/core/src/autocomplete/style.scss @@ -34,4 +34,10 @@ vn-autocomplete { .material-icons { font-size: 18px; } + vn-drop-down{ + margin-top: 47px; + } + vn-drop-down .dropdown-body .filter vn-icon { + margin-left: -26px; + } } \ No newline at end of file diff --git a/client/core/src/drop-down/drop-down.html b/client/core/src/drop-down/drop-down.html index dad08a84fe..c65feac282 100644 --- a/client/core/src/drop-down/drop-down.html +++ b/client/core/src/drop-down/drop-down.html @@ -1,19 +1,22 @@ -
- -
-
- -
+
\ No newline at end of file diff --git a/client/core/src/drop-down/drop-down.js b/client/core/src/drop-down/drop-down.js index ece781507d..d8de9c0144 100644 --- a/client/core/src/drop-down/drop-down.js +++ b/client/core/src/drop-down/drop-down.js @@ -2,12 +2,41 @@ import {module} from '../module'; import './style.scss'; export default class DropDown { - constructor($element, $filter) { + constructor($element, $filter, $timeout) { this.$element = $element; this.$filter = $filter; - this.search = ''; - this.itemsFiltered = []; + this.$timeout = $timeout; + this.parent = this.parent || $element[0].parentNode; + this._search = null; + this.itemsFiltered = []; + this._activeOption = -1; + } + + get search() { + return this._search; + } + set search(value) { + let val = (value === undefined && value === '') ? null : value; + this._search = val; + + if (this.filterAction) + this.onFilterRest(); + else + this.filterItems(); + } + get activeOption() { + return this._activeOption; + } + set activeOption(value) { + if (value < 0) { + value = 0; + } else if (value >= this.items.length) { + value = this.showLoadMore ? this.items.length : this.items.length - 1; + } + this.$timeout(() => { + this._activeOption = value; + }); } filterItems() { @@ -15,32 +44,92 @@ export default class DropDown { } onFilterRest() { - this.showLoadMore = false; - if (this.filterAction) { - this.filterAction({search: this.search}); - } + this.filterAction({search: this.search}); } $onChanges(changesObj) { if (changesObj.show && changesObj.top && changesObj.top.currentValue) { this.$element.css('top', changesObj.top.currentValue + 'px'); } + if (changesObj.show && changesObj.itemWidth && changesObj.itemWidth.currentValue) { + this.$element.css('width', changesObj.itemWidth.currentValue + 'px'); + } if (changesObj.items) { this.filterItems(); } } clearSearch() { - this.search = ''; - this.showLoadMore = this.loadMore != null; - if (this.filterAction) { - this.filterAction({search: this.search}); - } else { - this.filterItems(); + 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(); } } + + onKeydown(event) { + if (this.show) { + switch (event.keyCode) { + case 13: // Enter + this.$timeout(() => { + this.selectOption(); + }); + event.preventDefault(); + break; + case 27: // Escape + this.clearSearch(); + break; + case 38: // Arrow up + this.activeOption--; + this.$timeout(() => { + this.setScrollPosition(); + }, 100); + break; + case 40: // Arrow down + this.activeOption++; + this.$timeout(() => { + this.setScrollPosition(); + }, 100); + break; + default: + return; + } + } + } + setScrollPosition() { + let dropdown = this.$element[0].querySelector('ul.dropdown'); + let child = dropdown ? dropdown.childNodes[this.activeOption] : null; + if (child && typeof child.scrollIntoView === 'function') { + child.scrollIntoView(); + } + } + + selectItem(item) { + this.selected = item; + if (this.multiple) { + item.checked = !item.checked; + this.show = true; + } else { + this.show = false; + } + } + + $onInit() { + if (this.parent) + this.parent.addEventListener('keydown', e => this.onKeydown(e)); + } + $onDestroy() { + if (this.parent) + this.parent.removeEventListener('keydown', e => this.onKeydown(e)); + } } -DropDown.$inject = ['$element', '$filter']; +DropDown.$inject = ['$element', '$filter', '$timeout']; module.component('vnDropDown', { template: require('./drop-down.html'), @@ -50,9 +139,16 @@ module.component('vnDropDown', { show: '<', filter: '@?', selected: '=', + search: '=?', loadMore: '&?', filterAction: '&?', - showLoadMore: '
@@ -17,6 +18,7 @@ load-more="$ctrl.getItems()" show-load-more="$ctrl.maxRow" filter-action="$ctrl.findItems(search)" + parent="$ctrl.element" >
\ No newline at end of file diff --git a/client/core/src/icon-menu/icon-menu.js b/client/core/src/icon-menu/icon-menu.js index a81d3f512a..4ac9d2302e 100644 --- a/client/core/src/icon-menu/icon-menu.js +++ b/client/core/src/icon-menu/icon-menu.js @@ -9,6 +9,7 @@ export default class IconMenu { this._showDropDown = false; this.finding = false; this.findMore = false; + this.element = $element[0]; } get showDropDown() { return this._showDropDown; @@ -34,6 +35,7 @@ export default class IconMenu { } ); } else if (!search && !this.finding) { + this.maxRow = 10; this.items = []; this.getItems(); } @@ -83,10 +85,23 @@ export default class IconMenu { this.showDropDown = false; }); }); + this.$element.bind('focusin', e => { + this.$timeout(() => { + this.showDropDown = true; + }); + }); + this.$element.bind('focusout', e => { + this.$timeout(() => { + this.showDropDown = false; + }); + }); } + $onDestroy() { this.$element.unbind('mouseover'); this.$element.unbind('mouseout'); + this.$element.unbind('focusin'); + this.$element.unbind('focusout'); } } IconMenu.$inject = ['$element', '$http', '$timeout'];