diff --git a/CHANGELOG.md b/CHANGELOG.md index 44cdd55..6fa8b3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +## 0.2.21 + +#### Features + +- **oi-select-options:** + - `minlength` is minimum length of query for searching + +- **list-placeholder:** placeholder for dropdown list if no items found + +#### Bug Fixes + +- **oi-options:** + - `select as` works correct with object data source, and with zero id + - `disable when` items don't add on click `enter` + +- **oiSelectAscSort** works correct with different locales (f.e. Turkish) + +- **oi-select** fix memory leak + ## 0.2.20 #### Features diff --git a/README.md b/README.md index be068ca..329543d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ #oi.select — AngularJS directive of select element -**[Download 0.2.20](https://github.com/tamtakoe/oi.select/tree/master/dist)** +**[Download 0.2.21](https://github.com/tamtakoe/oi.select/tree/master/dist)** ## Features @@ -87,6 +87,9 @@ Use `oi-select` directive: * `ng-disabled` — specifies that a drop-down list should be disabled * `multiple` — specifies that multiple options can be selected at once * `multiple-limit` — maximum number of options that can be selected at once +* `placeholder` — native placeholder +* `multiple-placeholder` — placeholder which is shown in multiple mode near chosen options +* `list-placeholder` — placeholder which is shown in list if no elements found * `readonly` — specifies that an input field is read-only * `autofocus` — specifies that an input field should automatically get focus when the page loads * `oi-select-options` — object with options. You can override them in `oiSelectProvider.options` @@ -104,6 +107,7 @@ Use `oi-select` directive: * `newItemFn` — function which get query and return new item object or promise. F.e. `'addItem($query)'` * `removeItemFn` — function which get removed item model and return any value or promise. If promise was rejected, item wouldn't removed. F.e. `'removeItem($item)'` * `maxlength` — maximum number of characters allowed in the input + * `minlength` — minimum number of characters for searching ### oiSelect service * `options` — default options which we can override in `oiSelectProvider.options` diff --git a/bower.json b/bower.json index db59773..01abc12 100644 --- a/bower.json +++ b/bower.json @@ -3,7 +3,7 @@ "name": "https://github.com/tamtakoe" }, "name": "oi.select", - "version": "0.2.20", + "version": "0.2.21", "main": ["./dist/select-tpls.min.js", "./dist/select.min.css"], "dependencies": { "angular": ">=1.2", diff --git a/dist/select-tpls.js b/dist/select-tpls.js index 0b994be..83981b7 100644 --- a/dist/select-tpls.js +++ b/dist/select-tpls.js @@ -12,13 +12,14 @@ angular.module('oi.select') editItem: false, newItem: false, closeList: true, - saveTrigger: 'enter tab blur' + saveTrigger: 'enter tab blur', + minlength: 0 }, version: { - full: '0.2.20', + full: '0.2.21', major: 0, minor: 2, - dot: 20 + dot: 21 }, $get: function() { return { @@ -141,7 +142,7 @@ angular.module('oi.select') } return function () { - $document[0].removeEventListener('click', clickHandler); + $document[0].removeEventListener('click', clickHandler, true); element[0].removeEventListener('mousedown', mousedownHandler, true); element[0].removeEventListener('blur', blurHandler, true); inputElement.off('focus', focusHandler); @@ -292,18 +293,6 @@ angular.module('oi.select') return true; } - function objToArr(obj) { - var arr = []; - - angular.forEach(obj, function (value, key) { - if (key.toString().charAt(0) !== '$') { - arr.push(value); - } - }); - - return arr; - } - //lodash _.intersection + filter + invert function intersection(xArr, yArr, xFilter, yFilter, invert) { var i, j, n, filteredX, filteredY, out = invert ? [].concat(xArr) : []; @@ -326,7 +315,7 @@ angular.module('oi.select') function getValue(valueName, item, scope, getter) { var locals = {}; - //'name.subname' -> {name: {subname: list}}' + //'name.subname' -> {name: {subname: item}} -> locals' valueName.split('.').reduce(function (previousValue, currentItem, index, arr) { return previousValue[currentItem] = index < arr.length - 1 ? {} : item; }, locals); @@ -339,11 +328,11 @@ angular.module('oi.select') bindFocusBlur: bindFocusBlur, scrollActiveOption: scrollActiveOption, groupsIsEmpty: groupsIsEmpty, - objToArr: objToArr, getValue: getValue, intersection: intersection } }]); + angular.module('oi.select') .directive('oiSelect', ['$document', '$q', '$timeout', '$parse', '$interpolate', '$injector', '$filter', '$animate', 'oiUtils', 'oiSelect', function($document, $q, $timeout, $parse, $interpolate, $injector, $filter, $animate, oiUtils, oiSelect) { @@ -365,11 +354,24 @@ angular.module('oi.select') var selectAsName = / as /.test(match[0]) && match[1], //item.modelValue displayName = match[2] || match[1], //item.label - valueName = match[5] || match[7], //item + valueName = match[5] || match[7], //item (value) + keyName = match[6], //(key) groupByName = match[3] || '', //item.groupName disableWhenName = match[4] || '', //item.disableWhenName trackByName = match[9] || displayName, //item.id - valueMatches = match[8].match(VALUES_REGEXP); //collection + valueMatches = match[8].match(VALUES_REGEXP), //collection + valueTitle = valueName, + keyTitle = keyName; + + if (keyName) { //convert object data sources format to array data sources format + valueName = 'i'; + selectAsName = valueName + '.' + (selectAsName || valueTitle); + trackByName = valueName + '.' + keyName; + displayName = valueName + '.' + displayName; + keyName = valueName + '.' + keyName; + groupByName = groupByName ? valueName + '.' + groupByName: undefined; + disableWhenName = disableWhenName ? valueName + '.' + disableWhenName: undefined; + } var valuesName = valueMatches[1], //collection filteredValuesName = valuesName + (valueMatches[3] || ''), //collection | filter @@ -384,6 +386,7 @@ angular.module('oi.select') trackByFn = $parse(trackByName); var multiplePlaceholderFn = $interpolate(attrs.multiplePlaceholder || ''), + listPlaceholderFn = $interpolate(attrs.listPlaceholder || ''), placeholderFn = $interpolate(attrs.placeholder || ''), optionsFn = $parse(attrs.oiSelectOptions), isOldAngular = angular.version.major <= 1 && angular.version.minor <= 3; @@ -405,6 +408,7 @@ angular.module('oi.select') listElement = angular.element(element[0].querySelector('.select-dropdown')), placeholder = placeholderFn(scope), multiplePlaceholder = multiplePlaceholderFn(scope), + listPlaceholder = listPlaceholderFn(scope), elementOptions = optionsFn(scope.$parent) || {}, options = angular.extend({cleanModel: elementOptions.newItem === 'prompt'}, oiSelect.options, elementOptions), editItem = options.editItem, @@ -531,10 +535,12 @@ angular.module('oi.select') }); scope.$watch('query', function(inputValue, oldValue) { - if (saveOn(inputValue.slice(0, -1), inputValue.slice(-1))) { - return; - } + //terminated symbol + if (saveOn(inputValue.slice(0, -1), inputValue.slice(-1))) return; + //length less then minlength + if (String(inputValue).length < options.minlength) return; + //We don't get matches if nothing added into matches list if (inputValue !== oldValue && (!scope.oldQuery || inputValue) && !matchesWereReset) { listElement[0].scrollTop = 0; @@ -572,6 +578,12 @@ angular.module('oi.select') }); }); + scope.$watch('isEmptyList', function(isEmptyList) { + $animate[isEmptyList ? 'addClass' : 'removeClass'](element, 'emptyList', !isOldAngular && { + tempClasses: 'emptyList-animate' + }); + }); + scope.$watch('showLoader', function(isLoading) { $animate[isLoading ? 'addClass' : 'removeClass'](element, 'loading', !isOldAngular && { tempClasses: 'loading-animate' @@ -766,6 +778,9 @@ angular.module('oi.select') resetMatches(); element[0].addEventListener('click', click, true); //triggered before add or delete item event + scope.$on('$destroy', function() { + element[0].removeEventListener('click', click, true); + }); element.on('focus', focus); element.on('blur', blur); @@ -791,6 +806,9 @@ angular.module('oi.select') } function click(event) { + //query length less then minlength + if (scope.query.length < options.minlength) return; + //option is disabled if (oiUtils.contains(element[0], event.target, 'disabled')) return; @@ -844,7 +862,7 @@ angular.module('oi.select') selectedOrder = triggerName !== 'blur' ? scope.order[scope.selectorPosition] : null, //do not save selected element in dropdown list on blur itemPromise; - if (isTriggered && (isNewItem || selectedOrder)) { + if (isTriggered && (isNewItem || selectedOrder && !getDisableWhen(selectedOrder))) { scope.showLoader = true; itemPromise = $q.when(selectedOrder || newItemFn(scope.$parent, {$query: query})); @@ -891,7 +909,7 @@ angular.module('oi.select') } function getDisableWhen(item) { - return oiUtils.getValue(valueName, item, scope.$parent, disableWhenFn); + return scope.isEmptyList || oiUtils.getValue(valueName, item, scope.$parent, disableWhenFn); } function getGroupName(option) { @@ -906,7 +924,7 @@ angular.module('oi.select') value = value instanceof Array ? value : value ? [value]: []; return value.filter(function(item) { - return item && (item instanceof Array && item.length || selectAsFn || getLabel(item)); + return item !== undefined && (item instanceof Array && item.length || selectAsFn || getLabel(item)); }); } @@ -915,6 +933,8 @@ angular.module('oi.select') } function getMatches(query, selectedAs) { + scope.isEmptyList = false; + if (timeoutPromise && waitTime) { $timeout.cancel(timeoutPromise); //cancel previous timeout } @@ -936,14 +956,44 @@ angular.module('oi.select') return $q.when(values.$promise || values) .then(function(values) { + scope.groups = {}; + if (values && keyName) { + //convert object data sources format to array data sources format + var arr = []; + + angular.forEach(values, function (value, key) { + if (key.toString().charAt(0) !== '$') { + var item = {}; + + item[keyTitle] = key; + item[valueTitle] = value; + arr.push(item); + } + }); + + values = arr; + } + if (values && !selectedAs) { var outputValues = multiple ? scope.output : []; - var filteredList = listFilter(oiUtils.objToArr(values), query, getLabel, listFilterOptionsFn(scope.$parent), element); + var filteredList = listFilter(values, query, getLabel, listFilterOptionsFn(scope.$parent), element); var withoutIntersection = oiUtils.intersection(filteredList, outputValues, trackBy, trackBy, true); var filteredOutput = filter(withoutIntersection); + //add element with placeholder to empty list + if (!filteredOutput.length) { + scope.isEmptyList = true; + + if (listPlaceholder) { + var context = {}; + + displayFn.assign(context, listPlaceholder); + filteredOutput = [context[valueName]] + } + } + scope.groups = group(filteredOutput); } updateGroupPos(); @@ -1058,7 +1108,7 @@ angular.module('oi.select') if (query.length > 0 || angular.isNumber(query)) { label = label.toString(); - query = oiSelectEscape(query.toString()); + query = oiSelectEscape(query); html = label.replace(new RegExp(query, 'gi'), '$&'); } else { @@ -1074,16 +1124,16 @@ angular.module('oi.select') var i, j, isFound, output, output1 = [], output2 = [], output3 = [], output4 = []; if (query) { - query = oiSelectEscape(String(query)); + query = oiSelectEscape(query).toLocaleLowerCase(); for (i = 0, isFound = false; i < input.length; i++) { - isFound = getLabel(input[i]).match(new RegExp(query, "i")); + isFound = getLabel(input[i]).toLocaleLowerCase().match(new RegExp(query)); if (!isFound && options && (options.length || options.fields)) { for (j = 0; j < options.length; j++) { if (isFound) break; - isFound = String(input[i][options[j]]).match(new RegExp(query, "i")); + isFound = String(input[i][options[j]]).toLocaleLowerCase().match(new RegExp(query)); } } @@ -1092,7 +1142,7 @@ angular.module('oi.select') } } for (i = 0; i < output1.length; i++) { - if (getLabel(output1[i]).match(new RegExp('^' + query, "i"))) { + if (getLabel(output1[i]).toLocaleLowerCase().match(new RegExp('^' + query))) { output2.push(output1[i]); } else { output3.push(output1[i]); diff --git a/dist/select-tpls.min.js b/dist/select-tpls.min.js index a19cc65..0366da1 100644 --- a/dist/select-tpls.min.js +++ b/dist/select-tpls.min.js @@ -1 +1 @@ -angular.module("oi.select",[]),angular.module("oi.select").provider("oiSelect",function(){return{options:{debounce:500,searchFilter:"oiSelectCloseIcon",dropdownFilter:"oiSelectHighlight",listFilter:"oiSelectAscSort",groupFilter:"oiSelectGroup",editItem:!1,newItem:!1,closeList:!0,saveTrigger:"enter tab blur"},version:{full:"0.2.20",major:0,minor:2,dot:20},$get:function(){return{options:this.options,version:this.version}}}}).factory("oiSelectEscape",function(){var e=/[-\/\\^$*+?.()|[\]{}]/g,t="\\$&";return function(n){return String(n).replace(e,t)}}).factory("oiSelectEditItem",function(){return function(e,t,n,o){return o?"":n(e)}}).factory("oiUtils",["$document","$timeout",function(e,t){function n(e,t,n){for(var o=t;o&&o.ownerDocument&&11!==o.nodeType;){if(n){if(o===e)return!1;if(o.className.indexOf(n)>=0)return!0}else if(o===e)return!0;o=o.parentNode}return!1}function o(o,r){function i(e){return e&&"INPUT"!==e.target.nodeName?void 0:(d=!1,c?void(d=!0):void t(function(){o.triggerHandler("blur")}))}function s(){a||(a=!0,t(function(){o.triggerHandler("focus")}))}function l(){c=!0}function u(e){c=!1;var s=e.target,l=n(o[0],s);d&&!l&&i(),l&&"INPUT"!==s.nodeName&&t(function(){r[0].focus()}),!l&&a&&(a=!1)}var a,c,d;return e[0].addEventListener("click",u,!0),o[0].addEventListener("mousedown",l,!0),o[0].addEventListener("blur",i,!0),r.on("focus",s),function(){e[0].removeEventListener("click",u),o[0].removeEventListener("mousedown",l,!0),o[0].removeEventListener("blur",i,!0),r.off("focus",s)}}function r(e,t){var n,o,r,i,l,a;t&&(o=e.offsetHeight,r=u(t,"height","margin"),i=e.scrollTop||0,n=s(t).top-s(e).top+i,l=n,a=n-o+r,n+r>o+i?e.scrollTop=a:i>n&&(e.scrollTop=l))}function i(e,t,n,o,r){function i(e){return parseFloat(r[e])}for(var s=n===(o?"border":"content")?4:"width"===t?1:0,l=0,u=["Top","Right","Bottom","Left"];4>s;s+=2)"margin"===n&&(l+=i(n+u[s])),o?("content"===n&&(l-=i("padding"+u[s])),"margin"!==n&&(l-=i("border"+u[s]+"Width"))):(l+=i("padding"+u[s]),"padding"!==n&&(l+=i("border"+u[s]+"Width")));return l}function s(e){var t,n,o=e.getBoundingClientRect(),r=e&&e.ownerDocument;if(r)return t=r.documentElement,n=l(r),{top:o.top+n.pageYOffset-t.clientTop,left:o.left+n.pageXOffset-t.clientLeft}}function l(e){return null!=e&&e===e.window?e:9===e.nodeType&&e.defaultView}function u(e,t,n){var o=!0,r="width"===t?e.offsetWidth:e.offsetHeight,s=window.getComputedStyle(e,null),l=!1;if(0>=r||null==r){if(r=s[t],(0>r||null==r)&&(r=e.style[t]),g.test(r))return r;r=parseFloat(r)||0}return r+i(e,t,n||(l?"border":"content"),o,s)}function a(e){for(var t in e)if(e.hasOwnProperty(t)&&e[t].length)return!1;return!0}function c(e){var t=[];return angular.forEach(e,function(e,n){"$"!==n.toString().charAt(0)&&t.push(e)}),t}function d(e,t,n,o,r){var i,s,l,u,a,c=r?[].concat(e):[];for(i=0,l=e.length;i=y&&u.contains(r[0],t.target,"select-dropdown")||(e.inputHide&&e.removeItem(0),!e.isOpen||!lt.closeList||"INPUT"===t.target.nodeName&&e.query.length?J(e.query):(Z({query:lt.editItem&&!at}),e.$evalAsync()))}function V(){e.isFocused||(e.isFocused=!0,c.disabled||(e.backspaceFocus=!1))}function q(){e.isFocused=!1,w||I(),E("blur")||Z(),e.$evalAsync()}function E(o,r){r||(r=o,o=e.query);var i,s=lt.saveTrigger.split(" ").indexOf(r)+1,l=lt.newItem&&o,u="blur"!==r?e.order[e.selectorPosition]:null;return s&&(l||u)?(e.showLoader=!0,i=t.when(u||S(e.$parent,{$query:o})),i.then(function(o){if(void 0===o)return t.reject();e.addItem(o);var r=e.order.length-1;e.selectorPosition===r&&et(ot,0),lt.newItemFn&&!u||n(angular.noop),Z()}).catch(function(){f("invalid-item"),e.showLoader=!1}),!0):void 0}function x(){var e=w&&Y(d.$modelValue)?it:rt;nt.attr("placeholder",e)}function C(t){return u.getValue(F,t,e.$parent,N)}function j(t){return u.getValue(F,t,e.$parent,H)}function W(t){return u.getValue(F,t,e.$parent,O)}function B(t){return u.getValue(F,t,e.$parent,D)}function G(t){return u.getValue(F,t,e.$parent,A)||""}function z(t){return u.getValue(P,t,e.$parent,T)}function X(e){return e=e instanceof Array?e:e?[e]:[],e.filter(function(e){return e&&(e instanceof Array&&e.length||H||W(e))})}function Y(e){return!!X(e).length}function J(o,i){return v&&ct&&n.cancel(v),v=n(function(){var s=_(e.$parent,{$query:o,$selectedAs:i})||"";return e.selectorPosition="prompt"===lt.newItem?!1:0,o||i||(e.oldQuery=null),(s.$promise&&!s.$resolved||angular.isFunction(s.then))&&(ct=lt.debounce),e.showLoader=!0,t.when(s.$promise||s).then(function(t){if(e.groups={},t&&!i){var n=w?e.output:[],s=vt(u.objToArr(t),o,W,$t(e.$parent),r),l=u.intersection(s,n,C,C,!0),a=z(l);e.groups=tt(a)}return K(),t}).finally(function(){e.showLoader=!1,lt.closeList&&!lt.cleanModel&&n(function(){et(ot,0)})})},ct)}function K(){var t,n,o,r=[],i=0;e.order=[],e.groupPos={};for(n in e.groups)e.groups.hasOwnProperty(n)&&"$"!=n.charAt(0)&&r.push(n);for(R&&r.sort(),t=0;t=y)return void f("limited");var n=e.groups[G(t)]=e.groups[G(t)]||[],o=H?j(t):t;n.splice(n.indexOf(t),1),w?d.$setViewValue(angular.isArray(d.$modelValue)?d.$modelValue.concat(o):[o]):(d.$setViewValue(o),I()),u.groupsIsEmpty(e.groups)&&(e.groups={}),w||lt.closeList||Z({query:!0}),p(),e.oldQuery=e.oldQuery||e.query,e.query="",e.backspaceFocus=!1}},e.removeItem=function(n){c.disabled||w&&0>n||(b=w?d.$modelValue[n]:d.$modelValue,t.when(pt(e.$parent,{$item:b})).then(function(){(w||e.inputHide)&&(w?(d.$modelValue.splice(n,1),d.$setViewValue([].concat(d.$modelValue))):(k(),lt.cleanModel&&d.$setViewValue(void 0)),(w||!e.backspaceFocus)&&(e.query=dt(b,$,W,at,r)||""),w&<.closeList&&Z({query:!0}))}))},e.setSelection=function(t){m||e.selectorPosition===t?m=!1:et(ot,t)},e.keyUp=function(t){switch(t.keyCode){case 8:e.query.length||w&&e.output.length||Z()}},e.keyDown=function(t){var n=0,o=e.order.length-1;switch(t.keyCode){case 38:e.selectorPosition=angular.isNumber(e.selectorPosition)?e.selectorPosition:n,et(ot,e.selectorPosition===n?o:e.selectorPosition-1),m=!0;break;case 40:e.selectorPosition=angular.isNumber(e.selectorPosition)?e.selectorPosition:n-1,et(ot,e.selectorPosition===o?n:e.selectorPosition+1),m=!0,e.query.length||e.isOpen||J(),e.inputHide&&k();break;case 37:case 39:break;case 9:E("tab");break;case 13:E("enter"),t.preventDefault();break;case 32:E("space");break;case 27:w||(I(),lt.cleanModel&&d.$setViewValue(b)),Z();break;case 8:if(!e.query.length){if((!w||ut)&&(e.backspaceFocus=!0),e.backspaceFocus&&e.output&&(!w||e.output.length)){e.removeItem(e.output.length-1),ut&&t.preventDefault();break}e.backspaceFocus=!e.backspaceFocus;break}default:return e.inputHide&&k(),e.backspaceFocus=!1,!1}},e.getSearchLabel=function(t){var n=W(t);return ft(n,e.oldQuery||e.query,t,gt(e.$parent),r)},e.getDropdownLabel=function(t){var n=W(t);return mt(n,e.oldQuery||e.query,t,ht(e.$parent),r)},e.getGroupLabel=function(t,n){return bt(t,e.oldQuery||e.query,n,wt(e.$parent),r)},e.getDisableWhen=B,Z(),r[0].addEventListener("click",L,!0),r.on("focus",V),r.on("blur",q)}}}}]),angular.module("oi.select").filter("oiSelectGroup",["$sce",function(e){return function(t){return e.trustAsHtml(t)}}]).filter("oiSelectCloseIcon",["$sce",function(e){return function(t){var n='×';return e.trustAsHtml(t+n)}}]).filter("oiSelectHighlight",["$sce","oiSelectEscape",function(e,t){return function(n,o){var r;return o.length>0||angular.isNumber(o)?(n=n.toString(),o=t(o.toString()),r=n.replace(new RegExp(o,"gi"),"$&")):r=n,e.trustAsHtml(r)}}]).filter("oiSelectAscSort",["oiSelectEscape",function(e){function t(t,n,o,r){var i,s,l,u,a=[],c=[],d=[],p=[];if(n){for(n=e(String(n)),i=0,l=!1;i
')}]); \ No newline at end of file +angular.module("oi.select",[]),angular.module("oi.select").provider("oiSelect",function(){return{options:{debounce:500,searchFilter:"oiSelectCloseIcon",dropdownFilter:"oiSelectHighlight",listFilter:"oiSelectAscSort",groupFilter:"oiSelectGroup",editItem:!1,newItem:!1,closeList:!0,saveTrigger:"enter tab blur",minlength:0},version:{full:"0.2.21",major:0,minor:2,dot:21},$get:function(){return{options:this.options,version:this.version}}}}).factory("oiSelectEscape",function(){var e=/[-\/\\^$*+?.()|[\]{}]/g,t="\\$&";return function(n){return String(n).replace(e,t)}}).factory("oiSelectEditItem",function(){return function(e,t,n,o){return o?"":n(e)}}).factory("oiUtils",["$document","$timeout",function(e,t){function n(e,t,n){for(var o=t;o&&o.ownerDocument&&11!==o.nodeType;){if(n){if(o===e)return!1;if(o.className.indexOf(n)>=0)return!0}else if(o===e)return!0;o=o.parentNode}return!1}function o(o,r){function i(e){return e&&"INPUT"!==e.target.nodeName?void 0:(d=!1,c?void(d=!0):void t(function(){o.triggerHandler("blur")}))}function s(){a||(a=!0,t(function(){o.triggerHandler("focus")}))}function l(){c=!0}function u(e){c=!1;var s=e.target,l=n(o[0],s);d&&!l&&i(),l&&"INPUT"!==s.nodeName&&t(function(){r[0].focus()}),!l&&a&&(a=!1)}var a,c,d;return e[0].addEventListener("click",u,!0),o[0].addEventListener("mousedown",l,!0),o[0].addEventListener("blur",i,!0),r.on("focus",s),function(){e[0].removeEventListener("click",u,!0),o[0].removeEventListener("mousedown",l,!0),o[0].removeEventListener("blur",i,!0),r.off("focus",s)}}function r(e,t){var n,o,r,i,l,a;t&&(o=e.offsetHeight,r=u(t,"height","margin"),i=e.scrollTop||0,n=s(t).top-s(e).top+i,l=n,a=n-o+r,n+r>o+i?e.scrollTop=a:i>n&&(e.scrollTop=l))}function i(e,t,n,o,r){function i(e){return parseFloat(r[e])}for(var s=n===(o?"border":"content")?4:"width"===t?1:0,l=0,u=["Top","Right","Bottom","Left"];4>s;s+=2)"margin"===n&&(l+=i(n+u[s])),o?("content"===n&&(l-=i("padding"+u[s])),"margin"!==n&&(l-=i("border"+u[s]+"Width"))):(l+=i("padding"+u[s]),"padding"!==n&&(l+=i("border"+u[s]+"Width")));return l}function s(e){var t,n,o=e.getBoundingClientRect(),r=e&&e.ownerDocument;if(r)return t=r.documentElement,n=l(r),{top:o.top+n.pageYOffset-t.clientTop,left:o.left+n.pageXOffset-t.clientLeft}}function l(e){return null!=e&&e===e.window?e:9===e.nodeType&&e.defaultView}function u(e,t,n){var o=!0,r="width"===t?e.offsetWidth:e.offsetHeight,s=window.getComputedStyle(e,null),l=!1;if(0>=r||null==r){if(r=s[t],(0>r||null==r)&&(r=e.style[t]),f.test(r))return r;r=parseFloat(r)||0}return r+i(e,t,n||(l?"border":"content"),o,s)}function a(e){for(var t in e)if(e.hasOwnProperty(t)&&e[t].length)return!1;return!0}function c(e,t,n,o,r){var i,s,l,u,a,c=r?[].concat(e):[];for(i=0,l=e.length;i=P&&u.contains(r[0],t.target,"select-dropdown")||(e.inputHide&&e.removeItem(0),!e.isOpen||!pt.closeList||"INPUT"===t.target.nodeName&&e.query.length?tt(e.query):(ot({query:pt.editItem&&!gt}),e.$evalAsync()))}function b(){e.isFocused||(e.isFocused=!0,c.disabled||(e.backspaceFocus=!1))}function y(){e.isFocused=!1,V||h(),L("blur")||ot(),e.$evalAsync()}function L(o,r){r||(r=o,o=e.query);var i,s=pt.saveTrigger.split(" ").indexOf(r)+1,l=pt.newItem&&o,u="blur"!==r?e.order[e.selectorPosition]:null;return s&&(l||u&&!Y(u))?(e.showLoader=!0,i=t.when(u||x(e.$parent,{$query:o})),i.then(function(o){if(void 0===o)return t.reject();e.addItem(o);var r=e.order.length-1;e.selectorPosition===r&&rt(lt,0),pt.newItemFn&&!u||n(angular.noop),ot()}).catch(function(){f("invalid-item"),e.showLoader=!1}),!0):void 0}function O(){var e=V&&et(d.$modelValue)?at:ut;st.attr("placeholder",e)}function A(t){return u.getValue(v,t,e.$parent,U)}function z(t){return u.getValue(v,t,e.$parent,D)}function X(t){return u.getValue(v,t,e.$parent,_)}function Y(t){return e.isEmptyList||u.getValue(v,t,e.$parent,N)}function J(t){return u.getValue(v,t,e.$parent,T)||""}function K(t){return u.getValue(H,t,e.$parent,M)}function Z(e){return e=e instanceof Array?e:e?[e]:[],e.filter(function(e){return void 0!==e&&(e instanceof Array&&e.length||D||X(e))})}function et(e){return!!Z(e).length}function tt(o,i){return e.isEmptyList=!1,F&&mt&&n.cancel(F),F=n(function(){var s=Q(e.$parent,{$query:o,$selectedAs:i})||"";return e.selectorPosition="prompt"===pt.newItem?!1:0,o||i||(e.oldQuery=null),(s.$promise&&!s.$resolved||angular.isFunction(s.then))&&(mt=pt.debounce),e.showLoader=!0,t.when(s.$promise||s).then(function(t){if(e.groups={},t&&$){var n=[];angular.forEach(t,function(e,t){if("$"!==t.toString().charAt(0)){var o={};o[S]=t,o[k]=e,n.push(o)}}),t=n}if(t&&!i){var s=V?e.output:[],l=Lt(t,o,X,kt(e.$parent),r),a=u.intersection(l,s,A,A,!0),c=K(a);if(!c.length&&(e.isEmptyList=!0,ct)){var d={};_.assign(d,ct),c=[d[v]]}e.groups=it(c)}return nt(),t}).finally(function(){e.showLoader=!1,pt.closeList&&!pt.cleanModel&&n(function(){rt(lt,0)})})},mt)}function nt(){var t,n,o,r=[],i=0;e.order=[],e.groupPos={};for(n in e.groups)e.groups.hasOwnProperty(n)&&"$"!=n.charAt(0)&&r.push(n);for(G&&r.sort(),t=0;t=P)return void f("limited");var n=e.groups[J(t)]=e.groups[J(t)]||[],o=D?z(t):t;n.splice(n.indexOf(t),1),V?d.$setViewValue(angular.isArray(d.$modelValue)?d.$modelValue.concat(o):[o]):(d.$setViewValue(o),h()),u.groupsIsEmpty(e.groups)&&(e.groups={}),V||pt.closeList||ot({query:!0}),p(),e.oldQuery=e.oldQuery||e.query,e.query="",e.backspaceFocus=!1}},e.removeItem=function(n){c.disabled||V&&0>n||(q=V?d.$modelValue[n]:d.$modelValue,t.when(vt(e.$parent,{$item:q})).then(function(){(V||e.inputHide)&&(V?(d.$modelValue.splice(n,1),d.$setViewValue([].concat(d.$modelValue))):(m(),pt.cleanModel&&d.$setViewValue(void 0)),(V||!e.backspaceFocus)&&(e.query=ht(q,C,X,gt,r)||""),V&&pt.closeList&&ot({query:!0}))}))},e.setSelection=function(t){I||e.selectorPosition===t?I=!1:rt(lt,t)},e.keyUp=function(t){switch(t.keyCode){case 8:e.query.length||V&&e.output.length||ot()}},e.keyDown=function(t){var n=0,o=e.order.length-1;switch(t.keyCode){case 38:e.selectorPosition=angular.isNumber(e.selectorPosition)?e.selectorPosition:n,rt(lt,e.selectorPosition===n?o:e.selectorPosition-1),I=!0;break;case 40:e.selectorPosition=angular.isNumber(e.selectorPosition)?e.selectorPosition:n-1,rt(lt,e.selectorPosition===o?n:e.selectorPosition+1),I=!0,e.query.length||e.isOpen||tt(),e.inputHide&&m();break;case 37:case 39:break;case 9:L("tab");break;case 13:L("enter"),t.preventDefault();break;case 32:L("space");break;case 27:V||(h(),pt.cleanModel&&d.$setViewValue(q)),ot();break;case 8:if(!e.query.length){if((!V||ft)&&(e.backspaceFocus=!0),e.backspaceFocus&&e.output&&(!V||e.output.length)){e.removeItem(e.output.length-1),ft&&t.preventDefault();break}e.backspaceFocus=!e.backspaceFocus;break}default:return e.inputHide&&m(),e.backspaceFocus=!1,!1}},e.getSearchLabel=function(t){var n=X(t);return $t(n,e.oldQuery||e.query,t,wt(e.$parent),r)},e.getDropdownLabel=function(t){var n=X(t);return bt(n,e.oldQuery||e.query,t,yt(e.$parent),r)},e.getGroupLabel=function(t,n){return St(t,e.oldQuery||e.query,n,It(e.$parent),r)},e.getDisableWhen=Y,ot(),r[0].addEventListener("click",w,!0),e.$on("$destroy",function(){r[0].removeEventListener("click",w,!0)}),r.on("focus",b),r.on("blur",y)}}}}]),angular.module("oi.select").filter("oiSelectGroup",["$sce",function(e){return function(t){return e.trustAsHtml(t)}}]).filter("oiSelectCloseIcon",["$sce",function(e){return function(t){var n='×';return e.trustAsHtml(t+n)}}]).filter("oiSelectHighlight",["$sce","oiSelectEscape",function(e,t){return function(n,o){var r;return o.length>0||angular.isNumber(o)?(n=n.toString(),o=t(o),r=n.replace(new RegExp(o,"gi"),"$&")):r=n,e.trustAsHtml(r)}}]).filter("oiSelectAscSort",["oiSelectEscape",function(e){function t(t,n,o,r){var i,s,l,u,a=[],c=[],d=[],p=[];if(n){for(n=e(n).toLocaleLowerCase(),i=0,l=!1;i
')}]); \ No newline at end of file diff --git a/dist/select.css b/dist/select.css index a07ad32..9f502ad 100644 --- a/dist/select.css +++ b/dist/select.css @@ -185,3 +185,6 @@ oi-select.open:not(.multiple) .select-search:after { oi-select.loading:not(.multiple) .select-search:after { border-width: 0; } +oi-select.emptyList .select-dropdown-optgroup-option strong { + font-weight: normal; +} diff --git a/dist/select.js b/dist/select.js index b95dd5b..88b5537 100644 --- a/dist/select.js +++ b/dist/select.js @@ -12,13 +12,14 @@ angular.module('oi.select') editItem: false, newItem: false, closeList: true, - saveTrigger: 'enter tab blur' + saveTrigger: 'enter tab blur', + minlength: 0 }, version: { - full: '0.2.20', + full: '0.2.21', major: 0, minor: 2, - dot: 20 + dot: 21 }, $get: function() { return { @@ -141,7 +142,7 @@ angular.module('oi.select') } return function () { - $document[0].removeEventListener('click', clickHandler); + $document[0].removeEventListener('click', clickHandler, true); element[0].removeEventListener('mousedown', mousedownHandler, true); element[0].removeEventListener('blur', blurHandler, true); inputElement.off('focus', focusHandler); @@ -292,18 +293,6 @@ angular.module('oi.select') return true; } - function objToArr(obj) { - var arr = []; - - angular.forEach(obj, function (value, key) { - if (key.toString().charAt(0) !== '$') { - arr.push(value); - } - }); - - return arr; - } - //lodash _.intersection + filter + invert function intersection(xArr, yArr, xFilter, yFilter, invert) { var i, j, n, filteredX, filteredY, out = invert ? [].concat(xArr) : []; @@ -326,7 +315,7 @@ angular.module('oi.select') function getValue(valueName, item, scope, getter) { var locals = {}; - //'name.subname' -> {name: {subname: list}}' + //'name.subname' -> {name: {subname: item}} -> locals' valueName.split('.').reduce(function (previousValue, currentItem, index, arr) { return previousValue[currentItem] = index < arr.length - 1 ? {} : item; }, locals); @@ -339,11 +328,11 @@ angular.module('oi.select') bindFocusBlur: bindFocusBlur, scrollActiveOption: scrollActiveOption, groupsIsEmpty: groupsIsEmpty, - objToArr: objToArr, getValue: getValue, intersection: intersection } }]); + angular.module('oi.select') .directive('oiSelect', ['$document', '$q', '$timeout', '$parse', '$interpolate', '$injector', '$filter', '$animate', 'oiUtils', 'oiSelect', function($document, $q, $timeout, $parse, $interpolate, $injector, $filter, $animate, oiUtils, oiSelect) { @@ -365,11 +354,24 @@ angular.module('oi.select') var selectAsName = / as /.test(match[0]) && match[1], //item.modelValue displayName = match[2] || match[1], //item.label - valueName = match[5] || match[7], //item + valueName = match[5] || match[7], //item (value) + keyName = match[6], //(key) groupByName = match[3] || '', //item.groupName disableWhenName = match[4] || '', //item.disableWhenName trackByName = match[9] || displayName, //item.id - valueMatches = match[8].match(VALUES_REGEXP); //collection + valueMatches = match[8].match(VALUES_REGEXP), //collection + valueTitle = valueName, + keyTitle = keyName; + + if (keyName) { //convert object data sources format to array data sources format + valueName = 'i'; + selectAsName = valueName + '.' + (selectAsName || valueTitle); + trackByName = valueName + '.' + keyName; + displayName = valueName + '.' + displayName; + keyName = valueName + '.' + keyName; + groupByName = groupByName ? valueName + '.' + groupByName: undefined; + disableWhenName = disableWhenName ? valueName + '.' + disableWhenName: undefined; + } var valuesName = valueMatches[1], //collection filteredValuesName = valuesName + (valueMatches[3] || ''), //collection | filter @@ -384,6 +386,7 @@ angular.module('oi.select') trackByFn = $parse(trackByName); var multiplePlaceholderFn = $interpolate(attrs.multiplePlaceholder || ''), + listPlaceholderFn = $interpolate(attrs.listPlaceholder || ''), placeholderFn = $interpolate(attrs.placeholder || ''), optionsFn = $parse(attrs.oiSelectOptions), isOldAngular = angular.version.major <= 1 && angular.version.minor <= 3; @@ -405,6 +408,7 @@ angular.module('oi.select') listElement = angular.element(element[0].querySelector('.select-dropdown')), placeholder = placeholderFn(scope), multiplePlaceholder = multiplePlaceholderFn(scope), + listPlaceholder = listPlaceholderFn(scope), elementOptions = optionsFn(scope.$parent) || {}, options = angular.extend({cleanModel: elementOptions.newItem === 'prompt'}, oiSelect.options, elementOptions), editItem = options.editItem, @@ -531,10 +535,12 @@ angular.module('oi.select') }); scope.$watch('query', function(inputValue, oldValue) { - if (saveOn(inputValue.slice(0, -1), inputValue.slice(-1))) { - return; - } + //terminated symbol + if (saveOn(inputValue.slice(0, -1), inputValue.slice(-1))) return; + //length less then minlength + if (String(inputValue).length < options.minlength) return; + //We don't get matches if nothing added into matches list if (inputValue !== oldValue && (!scope.oldQuery || inputValue) && !matchesWereReset) { listElement[0].scrollTop = 0; @@ -572,6 +578,12 @@ angular.module('oi.select') }); }); + scope.$watch('isEmptyList', function(isEmptyList) { + $animate[isEmptyList ? 'addClass' : 'removeClass'](element, 'emptyList', !isOldAngular && { + tempClasses: 'emptyList-animate' + }); + }); + scope.$watch('showLoader', function(isLoading) { $animate[isLoading ? 'addClass' : 'removeClass'](element, 'loading', !isOldAngular && { tempClasses: 'loading-animate' @@ -766,6 +778,9 @@ angular.module('oi.select') resetMatches(); element[0].addEventListener('click', click, true); //triggered before add or delete item event + scope.$on('$destroy', function() { + element[0].removeEventListener('click', click, true); + }); element.on('focus', focus); element.on('blur', blur); @@ -791,6 +806,9 @@ angular.module('oi.select') } function click(event) { + //query length less then minlength + if (scope.query.length < options.minlength) return; + //option is disabled if (oiUtils.contains(element[0], event.target, 'disabled')) return; @@ -844,7 +862,7 @@ angular.module('oi.select') selectedOrder = triggerName !== 'blur' ? scope.order[scope.selectorPosition] : null, //do not save selected element in dropdown list on blur itemPromise; - if (isTriggered && (isNewItem || selectedOrder)) { + if (isTriggered && (isNewItem || selectedOrder && !getDisableWhen(selectedOrder))) { scope.showLoader = true; itemPromise = $q.when(selectedOrder || newItemFn(scope.$parent, {$query: query})); @@ -891,7 +909,7 @@ angular.module('oi.select') } function getDisableWhen(item) { - return oiUtils.getValue(valueName, item, scope.$parent, disableWhenFn); + return scope.isEmptyList || oiUtils.getValue(valueName, item, scope.$parent, disableWhenFn); } function getGroupName(option) { @@ -906,7 +924,7 @@ angular.module('oi.select') value = value instanceof Array ? value : value ? [value]: []; return value.filter(function(item) { - return item && (item instanceof Array && item.length || selectAsFn || getLabel(item)); + return item !== undefined && (item instanceof Array && item.length || selectAsFn || getLabel(item)); }); } @@ -915,6 +933,8 @@ angular.module('oi.select') } function getMatches(query, selectedAs) { + scope.isEmptyList = false; + if (timeoutPromise && waitTime) { $timeout.cancel(timeoutPromise); //cancel previous timeout } @@ -936,14 +956,44 @@ angular.module('oi.select') return $q.when(values.$promise || values) .then(function(values) { + scope.groups = {}; + if (values && keyName) { + //convert object data sources format to array data sources format + var arr = []; + + angular.forEach(values, function (value, key) { + if (key.toString().charAt(0) !== '$') { + var item = {}; + + item[keyTitle] = key; + item[valueTitle] = value; + arr.push(item); + } + }); + + values = arr; + } + if (values && !selectedAs) { var outputValues = multiple ? scope.output : []; - var filteredList = listFilter(oiUtils.objToArr(values), query, getLabel, listFilterOptionsFn(scope.$parent), element); + var filteredList = listFilter(values, query, getLabel, listFilterOptionsFn(scope.$parent), element); var withoutIntersection = oiUtils.intersection(filteredList, outputValues, trackBy, trackBy, true); var filteredOutput = filter(withoutIntersection); + //add element with placeholder to empty list + if (!filteredOutput.length) { + scope.isEmptyList = true; + + if (listPlaceholder) { + var context = {}; + + displayFn.assign(context, listPlaceholder); + filteredOutput = [context[valueName]] + } + } + scope.groups = group(filteredOutput); } updateGroupPos(); @@ -1058,7 +1108,7 @@ angular.module('oi.select') if (query.length > 0 || angular.isNumber(query)) { label = label.toString(); - query = oiSelectEscape(query.toString()); + query = oiSelectEscape(query); html = label.replace(new RegExp(query, 'gi'), '$&'); } else { @@ -1074,16 +1124,16 @@ angular.module('oi.select') var i, j, isFound, output, output1 = [], output2 = [], output3 = [], output4 = []; if (query) { - query = oiSelectEscape(String(query)); + query = oiSelectEscape(query).toLocaleLowerCase(); for (i = 0, isFound = false; i < input.length; i++) { - isFound = getLabel(input[i]).match(new RegExp(query, "i")); + isFound = getLabel(input[i]).toLocaleLowerCase().match(new RegExp(query)); if (!isFound && options && (options.length || options.fields)) { for (j = 0; j < options.length; j++) { if (isFound) break; - isFound = String(input[i][options[j]]).match(new RegExp(query, "i")); + isFound = String(input[i][options[j]]).toLocaleLowerCase().match(new RegExp(query)); } } @@ -1092,7 +1142,7 @@ angular.module('oi.select') } } for (i = 0; i < output1.length; i++) { - if (getLabel(output1[i]).match(new RegExp('^' + query, "i"))) { + if (getLabel(output1[i]).toLocaleLowerCase().match(new RegExp('^' + query))) { output2.push(output1[i]); } else { output3.push(output1[i]); diff --git a/dist/select.min.css b/dist/select.min.css index 1711cb7..076fbdb 100644 --- a/dist/select.min.css +++ b/dist/select.min.css @@ -1 +1 @@ -oi-select{display:block;position:relative;width:100%}oi-select .select-search{cursor:text;border:1px solid #d9d9d9;background-color:#fff;overflow:hidden!important;-moz-box-sizing:border-box;box-sizing:border-box;border-radius:4px}oi-select .select-search-list{display:-webkit-box;display:-webkit-flex;display:-moz-box;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-box-pack:start;-webkit-justify-content:flex-start;-moz-box-pack:start;-ms-flex-pack:start;justify-content:flex-start;margin:0;padding:2px 4px;list-style:none}oi-select .select-search-list-item{font-size:14px;margin:2px 4px 2px 0;vertical-align:middle;white-space:normal}oi-select .select-search-list-item_selection{cursor:pointer;background:#efefef;border-color:#ebebeb}oi-select .select-search-list-item_selection:hover{border-color:#e5e5e5}oi-select .select-search-list-item_selection.focused,oi-select .select-search-list-item_selection:active{border:1px solid #fff;border-radius:0;box-shadow:inset 0 0 10px 5px #fff}oi-select .select-search-list-item_selection.focused .close{display:none}oi-select .select-search-list-item_selection-remove{padding-left:2px}oi-select .select-search-list-item_loader{float:right;margin-top:6px;width:16px;height:16px;background:url(data:image/gif;base64,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) top}oi-select .select-search-list-item_input{-webkit-box-flex:1;-webkit-flex-grow:1;-moz-box-flex:1;-ms-flex-positive:1;flex-grow:1;margin:4px 0 5px 5px}oi-select .select-search-list-item_input input{padding:0;outline:0;border:0;width:100%}oi-select .select-search-list-item_hide{position:fixed;width:0;height:0;margin:0;opacity:0;pointer-events:none;text-indent:-9999em}oi-select .select-dropdown{position:absolute;width:inherit;overflow-y:scroll;max-height:100px;min-width:160px;font-size:14px;background-color:#fff;border-radius:0 0 4px 4px;border:1px solid #66afe9;border-top:0;outline:0;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);background-clip:padding-box;z-index:1000}oi-select .select-dropdown-optgroup{margin:0;padding:0}oi-select .select-dropdown-optgroup-header{font-weight:bolder;padding:3px 10px}oi-select .select-dropdown-optgroup-option{padding:3px 20px}oi-select .select-dropdown-optgroup-option.active:not(.disabled){background-color:#f1f1f1;cursor:pointer}oi-select .select-dropdown-optgroup-option.disabled{color:#aaa}oi-select:not(.multiple) .select-search-list-item_selection{color:#000;width:100%;border-color:#fff;text-align:left}oi-select:not(.multiple) .select-search-list-item_selection:not(:active){background:0 0}oi-select:not(.multiple):not(.cleanMode) .select-search-list-item_selection-remove{display:none}oi-select:not(.multiple):not(.cleanMode) .select-search:after{content:"";position:absolute;display:block;right:10px;width:0;height:0;margin-top:-19px;border-color:#000 transparent transparent;border-style:solid;border-width:5px 5px 0}oi-select[disabled=disabled] .select-search{cursor:not-allowed;background:#eee;border:1px solid #bdbdbd;opacity:.5}oi-select[disabled=disabled] .select-search-list-item_selection{cursor:not-allowed;box-shadow:none;border-color:transparent}oi-select[disabled=disabled] .select-search-list-item_selection-remove{visibility:hidden}oi-select[disabled=disabled] .select-search-list-item_input input:disabled{cursor:not-allowed;background:0 0}oi-select.focused .select-search{border-color:#66afe9;outline:0;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}oi-select.invalid-item .select-dropdown,oi-select.invalid-item .select-search,oi-select.limited .select-dropdown,oi-select.limited .select-search{border-color:#f1bc28;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(230,189,46,.6)}oi-select.open .select-search{border-radius:4px 4px 0 0;border-bottom:1px solid rgba(0,0,0,.075)}oi-select.open:not(.multiple) .select-search:after{border-color:transparent transparent #000;border-width:0 5px 5px}oi-select.loading:not(.multiple) .select-search:after{border-width:0} \ No newline at end of file +oi-select{display:block;position:relative;width:100%}oi-select .select-search{cursor:text;border:1px solid #d9d9d9;background-color:#fff;overflow:hidden!important;-moz-box-sizing:border-box;box-sizing:border-box;border-radius:4px}oi-select .select-search-list{display:-webkit-box;display:-webkit-flex;display:-moz-box;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-box-pack:start;-webkit-justify-content:flex-start;-moz-box-pack:start;-ms-flex-pack:start;justify-content:flex-start;margin:0;padding:2px 4px;list-style:none}oi-select .select-search-list-item{font-size:14px;margin:2px 4px 2px 0;vertical-align:middle;white-space:normal}oi-select .select-search-list-item_selection{cursor:pointer;background:#efefef;border-color:#ebebeb}oi-select .select-search-list-item_selection:hover{border-color:#e5e5e5}oi-select .select-search-list-item_selection.focused,oi-select .select-search-list-item_selection:active{border:1px solid #fff;border-radius:0;box-shadow:inset 0 0 10px 5px #fff}oi-select .select-search-list-item_selection.focused .close{display:none}oi-select .select-search-list-item_selection-remove{padding-left:2px}oi-select .select-search-list-item_loader{float:right;margin-top:6px;width:16px;height:16px;background:url(data:image/gif;base64,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) top}oi-select .select-search-list-item_input{-webkit-box-flex:1;-webkit-flex-grow:1;-moz-box-flex:1;-ms-flex-positive:1;flex-grow:1;margin:4px 0 5px 5px}oi-select .select-search-list-item_input input{padding:0;outline:0;border:0;width:100%}oi-select .select-search-list-item_hide{position:fixed;width:0;height:0;margin:0;opacity:0;pointer-events:none;text-indent:-9999em}oi-select .select-dropdown{position:absolute;width:inherit;overflow-y:scroll;max-height:100px;min-width:160px;font-size:14px;background-color:#fff;border-radius:0 0 4px 4px;border:1px solid #66afe9;border-top:0;outline:0;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);background-clip:padding-box;z-index:1000}oi-select .select-dropdown-optgroup{margin:0;padding:0}oi-select .select-dropdown-optgroup-header{font-weight:bolder;padding:3px 10px}oi-select .select-dropdown-optgroup-option{padding:3px 20px}oi-select .select-dropdown-optgroup-option.active:not(.disabled){background-color:#f1f1f1;cursor:pointer}oi-select .select-dropdown-optgroup-option.disabled{color:#aaa}oi-select:not(.multiple) .select-search-list-item_selection{color:#000;width:100%;border-color:#fff;text-align:left}oi-select:not(.multiple) .select-search-list-item_selection:not(:active){background:0 0}oi-select:not(.multiple):not(.cleanMode) .select-search-list-item_selection-remove{display:none}oi-select:not(.multiple):not(.cleanMode) .select-search:after{content:"";position:absolute;display:block;right:10px;width:0;height:0;margin-top:-19px;border-color:#000 transparent transparent;border-style:solid;border-width:5px 5px 0}oi-select[disabled=disabled] .select-search{cursor:not-allowed;background:#eee;border:1px solid #bdbdbd;opacity:.5}oi-select[disabled=disabled] .select-search-list-item_selection{cursor:not-allowed;box-shadow:none;border-color:transparent}oi-select[disabled=disabled] .select-search-list-item_selection-remove{visibility:hidden}oi-select[disabled=disabled] .select-search-list-item_input input:disabled{cursor:not-allowed;background:0 0}oi-select.focused .select-search{border-color:#66afe9;outline:0;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}oi-select.invalid-item .select-dropdown,oi-select.invalid-item .select-search,oi-select.limited .select-dropdown,oi-select.limited .select-search{border-color:#f1bc28;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(230,189,46,.6)}oi-select.open .select-search{border-radius:4px 4px 0 0;border-bottom:1px solid rgba(0,0,0,.075)}oi-select.open:not(.multiple) .select-search:after{border-color:transparent transparent #000;border-width:0 5px 5px}oi-select.loading:not(.multiple) .select-search:after{border-width:0}oi-select.emptyList .select-dropdown-optgroup-option strong{font-weight:400} \ No newline at end of file diff --git a/dist/select.min.js b/dist/select.min.js index 3b92423..43dd7c2 100644 --- a/dist/select.min.js +++ b/dist/select.min.js @@ -1 +1 @@ -angular.module("oi.select",[]),angular.module("oi.select").provider("oiSelect",function(){return{options:{debounce:500,searchFilter:"oiSelectCloseIcon",dropdownFilter:"oiSelectHighlight",listFilter:"oiSelectAscSort",groupFilter:"oiSelectGroup",editItem:!1,newItem:!1,closeList:!0,saveTrigger:"enter tab blur"},version:{full:"0.2.20",major:0,minor:2,dot:20},$get:function(){return{options:this.options,version:this.version}}}}).factory("oiSelectEscape",function(){var e=/[-\/\\^$*+?.()|[\]{}]/g,t="\\$&";return function(n){return String(n).replace(e,t)}}).factory("oiSelectEditItem",function(){return function(e,t,n,r){return r?"":n(e)}}).factory("oiUtils",["$document","$timeout",function(e,t){function n(e,t,n){for(var r=t;r&&r.ownerDocument&&11!==r.nodeType;){if(n){if(r===e)return!1;if(r.className.indexOf(n)>=0)return!0}else if(r===e)return!0;r=r.parentNode}return!1}function r(r,o){function i(e){return e&&"INPUT"!==e.target.nodeName?void 0:(d=!1,c?void(d=!0):void t(function(){r.triggerHandler("blur")}))}function u(){a||(a=!0,t(function(){r.triggerHandler("focus")}))}function l(){c=!0}function s(e){c=!1;var u=e.target,l=n(r[0],u);d&&!l&&i(),l&&"INPUT"!==u.nodeName&&t(function(){o[0].focus()}),!l&&a&&(a=!1)}var a,c,d;return e[0].addEventListener("click",s,!0),r[0].addEventListener("mousedown",l,!0),r[0].addEventListener("blur",i,!0),o.on("focus",u),function(){e[0].removeEventListener("click",s),r[0].removeEventListener("mousedown",l,!0),r[0].removeEventListener("blur",i,!0),o.off("focus",u)}}function o(e,t){var n,r,o,i,l,a;t&&(r=e.offsetHeight,o=s(t,"height","margin"),i=e.scrollTop||0,n=u(t).top-u(e).top+i,l=n,a=n-r+o,n+o>r+i?e.scrollTop=a:i>n&&(e.scrollTop=l))}function i(e,t,n,r,o){function i(e){return parseFloat(o[e])}for(var u=n===(r?"border":"content")?4:"width"===t?1:0,l=0,s=["Top","Right","Bottom","Left"];4>u;u+=2)"margin"===n&&(l+=i(n+s[u])),r?("content"===n&&(l-=i("padding"+s[u])),"margin"!==n&&(l-=i("border"+s[u]+"Width"))):(l+=i("padding"+s[u]),"padding"!==n&&(l+=i("border"+s[u]+"Width")));return l}function u(e){var t,n,r=e.getBoundingClientRect(),o=e&&e.ownerDocument;if(o)return t=o.documentElement,n=l(o),{top:r.top+n.pageYOffset-t.clientTop,left:r.left+n.pageXOffset-t.clientLeft}}function l(e){return null!=e&&e===e.window?e:9===e.nodeType&&e.defaultView}function s(e,t,n){var r=!0,o="width"===t?e.offsetWidth:e.offsetHeight,u=window.getComputedStyle(e,null),l=!1;if(0>=o||null==o){if(o=u[t],(0>o||null==o)&&(o=e.style[t]),g.test(o))return o;o=parseFloat(o)||0}return o+i(e,t,n||(l?"border":"content"),r,u)}function a(e){for(var t in e)if(e.hasOwnProperty(t)&&e[t].length)return!1;return!0}function c(e){var t=[];return angular.forEach(e,function(e,n){"$"!==n.toString().charAt(0)&&t.push(e)}),t}function d(e,t,n,r,o){var i,u,l,s,a,c=o?[].concat(e):[];for(i=0,l=e.length;i=y&&s.contains(o[0],t.target,"select-dropdown")||(e.inputHide&&e.removeItem(0),!e.isOpen||!lt.closeList||"INPUT"===t.target.nodeName&&e.query.length?J(e.query):(Z({query:lt.editItem&&!at}),e.$evalAsync()))}function E(){e.isFocused||(e.isFocused=!0,c.disabled||(e.backspaceFocus=!1))}function q(){e.isFocused=!1,b||I(),L("blur")||Z(),e.$evalAsync()}function L(r,o){o||(o=r,r=e.query);var i,u=lt.saveTrigger.split(" ").indexOf(o)+1,l=lt.newItem&&r,s="blur"!==o?e.order[e.selectorPosition]:null;return u&&(l||s)?(e.showLoader=!0,i=t.when(s||S(e.$parent,{$query:r})),i.then(function(r){if(void 0===r)return t.reject();e.addItem(r);var o=e.order.length-1;e.selectorPosition===o&&et(rt,0),lt.newItemFn&&!s||n(angular.noop),Z()}).catch(function(){p("invalid-item"),e.showLoader=!1}),!0):void 0}function C(){var e=b&&Y(d.$modelValue)?it:ot;nt.attr("placeholder",e)}function x(t){return s.getValue(k,t,e.$parent,_)}function j(t){return s.getValue(k,t,e.$parent,A)}function B(t){return s.getValue(k,t,e.$parent,H)}function W(t){return s.getValue(k,t,e.$parent,T)}function G(t){return s.getValue(k,t,e.$parent,O)||""}function z(t){return s.getValue(P,t,e.$parent,N)}function X(e){return e=e instanceof Array?e:e?[e]:[],e.filter(function(e){return e&&(e instanceof Array&&e.length||A||B(e))})}function Y(e){return!!X(e).length}function J(r,i){return v&&ct&&n.cancel(v),v=n(function(){var u=D(e.$parent,{$query:r,$selectedAs:i})||"";return e.selectorPosition="prompt"===lt.newItem?!1:0,r||i||(e.oldQuery=null),(u.$promise&&!u.$resolved||angular.isFunction(u.then))&&(ct=lt.debounce),e.showLoader=!0,t.when(u.$promise||u).then(function(t){if(e.groups={},t&&!i){var n=b?e.output:[],u=vt(s.objToArr(t),r,B,$t(e.$parent),o),l=s.intersection(u,n,x,x,!0),a=z(l);e.groups=tt(a)}return K(),t}).finally(function(){e.showLoader=!1,lt.closeList&&!lt.cleanModel&&n(function(){et(rt,0)})})},ct)}function K(){var t,n,r,o=[],i=0;e.order=[],e.groupPos={};for(n in e.groups)e.groups.hasOwnProperty(n)&&"$"!=n.charAt(0)&&o.push(n);for(U&&o.sort(),t=0;t=y)return void p("limited");var n=e.groups[G(t)]=e.groups[G(t)]||[],r=A?j(t):t;n.splice(n.indexOf(t),1),b?d.$setViewValue(angular.isArray(d.$modelValue)?d.$modelValue.concat(r):[r]):(d.$setViewValue(r),I()),s.groupsIsEmpty(e.groups)&&(e.groups={}),b||lt.closeList||Z({query:!0}),f(),e.oldQuery=e.oldQuery||e.query,e.query="",e.backspaceFocus=!1}},e.removeItem=function(n){c.disabled||b&&0>n||(w=b?d.$modelValue[n]:d.$modelValue,t.when(ft(e.$parent,{$item:w})).then(function(){(b||e.inputHide)&&(b?(d.$modelValue.splice(n,1),d.$setViewValue([].concat(d.$modelValue))):(F(),lt.cleanModel&&d.$setViewValue(void 0)),(b||!e.backspaceFocus)&&(e.query=dt(w,$,B,at,o)||""),b&<.closeList&&Z({query:!0}))}))},e.setSelection=function(t){m||e.selectorPosition===t?m=!1:et(rt,t)},e.keyUp=function(t){switch(t.keyCode){case 8:e.query.length||b&&e.output.length||Z()}},e.keyDown=function(t){var n=0,r=e.order.length-1;switch(t.keyCode){case 38:e.selectorPosition=angular.isNumber(e.selectorPosition)?e.selectorPosition:n,et(rt,e.selectorPosition===n?r:e.selectorPosition-1),m=!0;break;case 40:e.selectorPosition=angular.isNumber(e.selectorPosition)?e.selectorPosition:n-1,et(rt,e.selectorPosition===r?n:e.selectorPosition+1),m=!0,e.query.length||e.isOpen||J(),e.inputHide&&F();break;case 37:case 39:break;case 9:L("tab");break;case 13:L("enter"),t.preventDefault();break;case 32:L("space");break;case 27:b||(I(),lt.cleanModel&&d.$setViewValue(w)),Z();break;case 8:if(!e.query.length){if((!b||st)&&(e.backspaceFocus=!0),e.backspaceFocus&&e.output&&(!b||e.output.length)){e.removeItem(e.output.length-1),st&&t.preventDefault();break}e.backspaceFocus=!e.backspaceFocus;break}default:return e.inputHide&&F(),e.backspaceFocus=!1,!1}},e.getSearchLabel=function(t){var n=B(t);return pt(n,e.oldQuery||e.query,t,gt(e.$parent),o)},e.getDropdownLabel=function(t){var n=B(t);return mt(n,e.oldQuery||e.query,t,ht(e.$parent),o)},e.getGroupLabel=function(t,n){return wt(t,e.oldQuery||e.query,n,bt(e.$parent),o)},e.getDisableWhen=W,Z(),o[0].addEventListener("click",V,!0),o.on("focus",E),o.on("blur",q)}}}}]),angular.module("oi.select").filter("oiSelectGroup",["$sce",function(e){return function(t){return e.trustAsHtml(t)}}]).filter("oiSelectCloseIcon",["$sce",function(e){return function(t){var n='×';return e.trustAsHtml(t+n)}}]).filter("oiSelectHighlight",["$sce","oiSelectEscape",function(e,t){return function(n,r){var o;return r.length>0||angular.isNumber(r)?(n=n.toString(),r=t(r.toString()),o=n.replace(new RegExp(r,"gi"),"$&")):o=n,e.trustAsHtml(o)}}]).filter("oiSelectAscSort",["oiSelectEscape",function(e){function t(t,n,r,o){var i,u,l,s,a=[],c=[],d=[],f=[];if(n){for(n=e(String(n)),i=0,l=!1;i=0)return!0}else if(r===e)return!0;r=r.parentNode}return!1}function r(r,o){function i(e){return e&&"INPUT"!==e.target.nodeName?void 0:(d=!1,c?void(d=!0):void t(function(){r.triggerHandler("blur")}))}function s(){a||(a=!0,t(function(){r.triggerHandler("focus")}))}function u(){c=!0}function l(e){c=!1;var s=e.target,u=n(r[0],s);d&&!u&&i(),u&&"INPUT"!==s.nodeName&&t(function(){o[0].focus()}),!u&&a&&(a=!1)}var a,c,d;return e[0].addEventListener("click",l,!0),r[0].addEventListener("mousedown",u,!0),r[0].addEventListener("blur",i,!0),o.on("focus",s),function(){e[0].removeEventListener("click",l,!0),r[0].removeEventListener("mousedown",u,!0),r[0].removeEventListener("blur",i,!0),o.off("focus",s)}}function o(e,t){var n,r,o,i,u,a;t&&(r=e.offsetHeight,o=l(t,"height","margin"),i=e.scrollTop||0,n=s(t).top-s(e).top+i,u=n,a=n-r+o,n+o>r+i?e.scrollTop=a:i>n&&(e.scrollTop=u))}function i(e,t,n,r,o){function i(e){return parseFloat(o[e])}for(var s=n===(r?"border":"content")?4:"width"===t?1:0,u=0,l=["Top","Right","Bottom","Left"];4>s;s+=2)"margin"===n&&(u+=i(n+l[s])),r?("content"===n&&(u-=i("padding"+l[s])),"margin"!==n&&(u-=i("border"+l[s]+"Width"))):(u+=i("padding"+l[s]),"padding"!==n&&(u+=i("border"+l[s]+"Width")));return u}function s(e){var t,n,r=e.getBoundingClientRect(),o=e&&e.ownerDocument;if(o)return t=o.documentElement,n=u(o),{top:r.top+n.pageYOffset-t.clientTop,left:r.left+n.pageXOffset-t.clientLeft}}function u(e){return null!=e&&e===e.window?e:9===e.nodeType&&e.defaultView}function l(e,t,n){var r=!0,o="width"===t?e.offsetWidth:e.offsetHeight,s=window.getComputedStyle(e,null),u=!1;if(0>=o||null==o){if(o=s[t],(0>o||null==o)&&(o=e.style[t]),p.test(o))return o;o=parseFloat(o)||0}return o+i(e,t,n||(u?"border":"content"),r,s)}function a(e){for(var t in e)if(e.hasOwnProperty(t)&&e[t].length)return!1;return!0}function c(e,t,n,r,o){var i,s,u,l,a,c=o?[].concat(e):[];for(i=0,u=e.length;i=P&&l.contains(o[0],t.target,"select-dropdown")||(e.inputHide&&e.removeItem(0),!e.isOpen||!ft.closeList||"INPUT"===t.target.nodeName&&e.query.length?tt(e.query):(rt({query:ft.editItem&&!gt}),e.$evalAsync()))}function y(){e.isFocused||(e.isFocused=!0,c.disabled||(e.backspaceFocus=!1))}function b(){e.isFocused=!1,q||h(),L("blur")||rt(),e.$evalAsync()}function L(r,o){o||(o=r,r=e.query);var i,s=ft.saveTrigger.split(" ").indexOf(o)+1,u=ft.newItem&&r,l="blur"!==o?e.order[e.selectorPosition]:null;return s&&(u||l&&!Y(l))?(e.showLoader=!0,i=t.when(l||x(e.$parent,{$query:r})),i.then(function(r){if(void 0===r)return t.reject();e.addItem(r);var o=e.order.length-1;e.selectorPosition===o&&ot(ut,0),ft.newItemFn&&!l||n(angular.noop),rt()}).catch(function(){p("invalid-item"),e.showLoader=!1}),!0):void 0}function O(){var e=q&&et(d.$modelValue)?at:lt;st.attr("placeholder",e)}function A(t){return l.getValue(v,t,e.$parent,R)}function z(t){return l.getValue(v,t,e.$parent,T)}function X(t){return l.getValue(v,t,e.$parent,N)}function Y(t){return e.isEmptyList||l.getValue(v,t,e.$parent,_)}function J(t){return l.getValue(v,t,e.$parent,D)||""}function K(t){return l.getValue(H,t,e.$parent,M)}function Z(e){return e=e instanceof Array?e:e?[e]:[],e.filter(function(e){return void 0!==e&&(e instanceof Array&&e.length||T||X(e))})}function et(e){return!!Z(e).length}function tt(r,i){return e.isEmptyList=!1,k&&mt&&n.cancel(k),k=n(function(){var s=Q(e.$parent,{$query:r,$selectedAs:i})||"";return e.selectorPosition="prompt"===ft.newItem?!1:0,r||i||(e.oldQuery=null),(s.$promise&&!s.$resolved||angular.isFunction(s.then))&&(mt=ft.debounce),e.showLoader=!0,t.when(s.$promise||s).then(function(t){if(e.groups={},t&&$){var n=[];angular.forEach(t,function(e,t){if("$"!==t.toString().charAt(0)){var r={};r[E]=t,r[S]=e,n.push(r)}}),t=n}if(t&&!i){var s=q?e.output:[],u=Lt(t,r,X,St(e.$parent),o),a=l.intersection(u,s,A,A,!0),c=K(a);if(!c.length&&(e.isEmptyList=!0,ct)){var d={};N.assign(d,ct),c=[d[v]]}e.groups=it(c)}return nt(),t}).finally(function(){e.showLoader=!1,ft.closeList&&!ft.cleanModel&&n(function(){ot(ut,0)})})},mt)}function nt(){var t,n,r,o=[],i=0;e.order=[],e.groupPos={};for(n in e.groups)e.groups.hasOwnProperty(n)&&"$"!=n.charAt(0)&&o.push(n);for(G&&o.sort(),t=0;t=P)return void p("limited");var n=e.groups[J(t)]=e.groups[J(t)]||[],r=T?z(t):t;n.splice(n.indexOf(t),1),q?d.$setViewValue(angular.isArray(d.$modelValue)?d.$modelValue.concat(r):[r]):(d.$setViewValue(r),h()),l.groupsIsEmpty(e.groups)&&(e.groups={}),q||ft.closeList||rt({query:!0}),f(),e.oldQuery=e.oldQuery||e.query,e.query="",e.backspaceFocus=!1}},e.removeItem=function(n){c.disabled||q&&0>n||(V=q?d.$modelValue[n]:d.$modelValue,t.when(vt(e.$parent,{$item:V})).then(function(){(q||e.inputHide)&&(q?(d.$modelValue.splice(n,1),d.$setViewValue([].concat(d.$modelValue))):(m(),ft.cleanModel&&d.$setViewValue(void 0)),(q||!e.backspaceFocus)&&(e.query=ht(V,C,X,gt,o)||""),q&&ft.closeList&&rt({query:!0}))}))},e.setSelection=function(t){F||e.selectorPosition===t?F=!1:ot(ut,t)},e.keyUp=function(t){switch(t.keyCode){case 8:e.query.length||q&&e.output.length||rt()}},e.keyDown=function(t){var n=0,r=e.order.length-1;switch(t.keyCode){case 38:e.selectorPosition=angular.isNumber(e.selectorPosition)?e.selectorPosition:n,ot(ut,e.selectorPosition===n?r:e.selectorPosition-1),F=!0;break;case 40:e.selectorPosition=angular.isNumber(e.selectorPosition)?e.selectorPosition:n-1,ot(ut,e.selectorPosition===r?n:e.selectorPosition+1),F=!0,e.query.length||e.isOpen||tt(),e.inputHide&&m();break;case 37:case 39:break;case 9:L("tab");break;case 13:L("enter"),t.preventDefault();break;case 32:L("space");break;case 27:q||(h(),ft.cleanModel&&d.$setViewValue(V)),rt();break;case 8:if(!e.query.length){if((!q||pt)&&(e.backspaceFocus=!0),e.backspaceFocus&&e.output&&(!q||e.output.length)){e.removeItem(e.output.length-1),pt&&t.preventDefault();break}e.backspaceFocus=!e.backspaceFocus;break}default:return e.inputHide&&m(),e.backspaceFocus=!1,!1}},e.getSearchLabel=function(t){var n=X(t);return $t(n,e.oldQuery||e.query,t,wt(e.$parent),o)},e.getDropdownLabel=function(t){var n=X(t);return yt(n,e.oldQuery||e.query,t,bt(e.$parent),o)},e.getGroupLabel=function(t,n){return Et(t,e.oldQuery||e.query,n,Ft(e.$parent),o)},e.getDisableWhen=Y,rt(),o[0].addEventListener("click",w,!0),e.$on("$destroy",function(){o[0].removeEventListener("click",w,!0)}),o.on("focus",y),o.on("blur",b)}}}}]),angular.module("oi.select").filter("oiSelectGroup",["$sce",function(e){return function(t){return e.trustAsHtml(t)}}]).filter("oiSelectCloseIcon",["$sce",function(e){return function(t){var n='×';return e.trustAsHtml(t+n)}}]).filter("oiSelectHighlight",["$sce","oiSelectEscape",function(e,t){return function(n,r){var o;return r.length>0||angular.isNumber(r)?(n=n.toString(),r=t(r),o=n.replace(new RegExp(r,"gi"),"$&")):o=n,e.trustAsHtml(o)}}]).filter("oiSelectAscSort",["oiSelectEscape",function(e){function t(t,n,r,o){var i,s,u,l,a=[],c=[],d=[],f=[];if(n){for(n=e(n).toLocaleLowerCase(),i=0,u=!1;iMultiple
Multiple
-oi-options="item.name for item in shopArr track by item.id" +oi-options="key as value for (key, value) in shopObj" ng-model="bundle" multiple
@@ -57,6 +57,7 @@

Multiple


Use multiple-placeholder if you need to set placeholder if model not empty

+

Use list-placeholder if you need to set label for empty list

Multiple ng-model="bundle3" multiple multiple-placeholder="add" + list-placeholder="not found" placeholder="Select" >
@@ -75,6 +77,7 @@

Multiple

ng-model="bundle3" multiple multiple-placeholder="add" +list-placeholder="not found"
 {{bundle3}}
diff --git a/docs/examples/single/controller.js b/docs/examples/single/controller.js
index 4a3e314..d60f079 100644
--- a/docs/examples/single/controller.js
+++ b/docs/examples/single/controller.js
@@ -1,9 +1,9 @@
 angular.module('selectDemo')
     .controller('selectSingleController', function ($scope, ShopObj) {
 
-        $scope.shopObjShort = ShopObj.get();
+        $scope.shopObj = ShopObj.get();
 
-        $scope.shopObjShort.$promise.then(function(data) {
+        $scope.shopObj.$promise.then(function(data) {
             $scope.bundle = {a:1}; //data[3];
         })
     });
\ No newline at end of file
diff --git a/docs/examples/single/template.html b/docs/examples/single/template.html
index f291bd3..5ecf1c4 100644
--- a/docs/examples/single/template.html
+++ b/docs/examples/single/template.html
@@ -4,7 +4,7 @@ 

Single

@@ -13,7 +13,7 @@

Single

-oi-options="item for (key, item) in shopObjShort" +oi-options="item for (key, item) in shopObj" ng-model="bundle"
diff --git a/docs/examples/validation/template.html b/docs/examples/validation/template.html
index d49c1da..5146044 100644
--- a/docs/examples/validation/template.html
+++ b/docs/examples/validation/template.html
@@ -77,14 +77,15 @@ 

Validation


-

Also you can set maximum number of characters allowed in the input

+

Also you can set maximum number of characters allowed in the input and minimum number of characters to prevent searching if query is too small

@@ -96,7 +97,8 @@

Validation

oi-options="item.name for item in shopArr" ng-model="bundle3" oi-select-options="{ - maxlength: 3 + maxlength: 4, + minlength: 2 }"
diff --git a/index.html b/index.html
index efad9a3..13cdb74 100644
--- a/index.html
+++ b/index.html
@@ -10,9 +10,9 @@
     
 
     
-    
-    
-    
+    
+    
+    
     
     
 
@@ -52,4 +52,4 @@
 
     
 
-
\ No newline at end of file
diff --git a/package.json b/package.json
index d95a361..35ac54b 100644
--- a/package.json
+++ b/package.json
@@ -1,13 +1,13 @@
 {
     "name":    "oi.select",
     "license": "MIT",
-    "version": "0.2.20",
+    "version": "0.2.21",
     "repository": {
         "type": "git",
         "url": "git://github.com/tamtakoe/oi.select.git"
     },
     "devDependencies": {
-        "gulp": "3.9.0",
+        "gulp": "3.9.1",
         "gulp-watch": "4.2.4",
         "gulp-webserver": "0.8.7",
         "gulp-stylus": "2.0.1",
diff --git a/src/directives.js b/src/directives.js
index 9b59c3f..4a3ea6e 100644
--- a/src/directives.js
+++ b/src/directives.js
@@ -19,11 +19,24 @@ angular.module('oi.select')
 
             var selectAsName          = / as /.test(match[0]) && match[1],    //item.modelValue
                 displayName           = match[2] || match[1],                 //item.label
-                valueName             = match[5] || match[7],                 //item
+                valueName             = match[5] || match[7],                 //item (value)
+                keyName               = match[6],                             //(key)
                 groupByName           = match[3] || '',                       //item.groupName
                 disableWhenName       = match[4] || '',                       //item.disableWhenName
                 trackByName           = match[9] || displayName,              //item.id
-                valueMatches          = match[8].match(VALUES_REGEXP);        //collection
+                valueMatches          = match[8].match(VALUES_REGEXP),        //collection
+                valueTitle            = valueName,
+                keyTitle              = keyName;
+
+            if (keyName) { //convert object data sources format to array data sources format
+                valueName             = 'i';
+                selectAsName          = valueName + '.' + (selectAsName || valueTitle);
+                trackByName           = valueName + '.' + keyName;
+                displayName           = valueName + '.' + displayName;
+                keyName               = valueName + '.' + keyName;
+                groupByName           = groupByName     ? valueName + '.' + groupByName: undefined;
+                disableWhenName       = disableWhenName ? valueName + '.' + disableWhenName: undefined;
+            }
 
             var valuesName            = valueMatches[1],                      //collection
                 filteredValuesName    = valuesName + (valueMatches[3] || ''), //collection | filter
@@ -38,6 +51,7 @@ angular.module('oi.select')
                 trackByFn             = $parse(trackByName);
 
             var multiplePlaceholderFn = $interpolate(attrs.multiplePlaceholder || ''),
+                listPlaceholderFn     = $interpolate(attrs.listPlaceholder || ''),
                 placeholderFn         = $interpolate(attrs.placeholder || ''),
                 optionsFn             = $parse(attrs.oiSelectOptions),
                 isOldAngular          = angular.version.major <= 1 && angular.version.minor <= 3;
@@ -59,6 +73,7 @@ angular.module('oi.select')
                     listElement         = angular.element(element[0].querySelector('.select-dropdown')),
                     placeholder         = placeholderFn(scope),
                     multiplePlaceholder = multiplePlaceholderFn(scope),
+                    listPlaceholder     = listPlaceholderFn(scope),
                     elementOptions      = optionsFn(scope.$parent) || {},
                     options             = angular.extend({cleanModel: elementOptions.newItem === 'prompt'}, oiSelect.options, elementOptions),
                     editItem            = options.editItem,
@@ -185,10 +200,12 @@ angular.module('oi.select')
                 });
 
                 scope.$watch('query', function(inputValue, oldValue) {
-                    if (saveOn(inputValue.slice(0, -1), inputValue.slice(-1))) {
-                        return;
-                    }
+                    //terminated symbol
+                    if (saveOn(inputValue.slice(0, -1), inputValue.slice(-1))) return;
 
+                    //length less then minlength
+                    if (String(inputValue).length < options.minlength) return;
+                    
                     //We don't get matches if nothing added into matches list
                     if (inputValue !== oldValue && (!scope.oldQuery || inputValue) && !matchesWereReset) {
                         listElement[0].scrollTop = 0;
@@ -226,6 +243,12 @@ angular.module('oi.select')
                     });
                 });
 
+                scope.$watch('isEmptyList', function(isEmptyList) {
+                    $animate[isEmptyList ? 'addClass' : 'removeClass'](element, 'emptyList', !isOldAngular && {
+                        tempClasses: 'emptyList-animate'
+                    });
+                });
+
                 scope.$watch('showLoader', function(isLoading) {
                     $animate[isLoading ? 'addClass' : 'removeClass'](element, 'loading', !isOldAngular && {
                         tempClasses: 'loading-animate'
@@ -448,6 +471,9 @@ angular.module('oi.select')
                 }
 
                 function click(event) {
+                    //query length less then minlength
+                    if (scope.query.length < options.minlength) return;
+                    
                     //option is disabled
                     if (oiUtils.contains(element[0], event.target, 'disabled')) return;
 
@@ -501,7 +527,7 @@ angular.module('oi.select')
                         selectedOrder  = triggerName !== 'blur' ? scope.order[scope.selectorPosition] : null, //do not save selected element in dropdown list on blur
                         itemPromise;
 
-                    if (isTriggered && (isNewItem || selectedOrder)) {
+                    if (isTriggered && (isNewItem || selectedOrder && !getDisableWhen(selectedOrder))) {
                         scope.showLoader = true;
                         itemPromise = $q.when(selectedOrder || newItemFn(scope.$parent, {$query: query}));
 
@@ -548,7 +574,7 @@ angular.module('oi.select')
                 }
 
                 function getDisableWhen(item) {
-                    return oiUtils.getValue(valueName, item, scope.$parent, disableWhenFn);
+                    return scope.isEmptyList || oiUtils.getValue(valueName, item, scope.$parent, disableWhenFn);
                 }
 
                 function getGroupName(option) {
@@ -563,7 +589,7 @@ angular.module('oi.select')
                     value = value instanceof Array ? value : value ? [value]: [];
 
                     return value.filter(function(item) {
-                        return item && (item instanceof Array && item.length || selectAsFn || getLabel(item));
+                        return item !== undefined && (item instanceof Array && item.length || selectAsFn || getLabel(item));
                     });
                 }
 
@@ -572,6 +598,8 @@ angular.module('oi.select')
                 }
 
                 function getMatches(query, selectedAs) {
+                    scope.isEmptyList = false;
+
                     if (timeoutPromise && waitTime) {
                         $timeout.cancel(timeoutPromise); //cancel previous timeout
                     }
@@ -593,14 +621,44 @@ angular.module('oi.select')
 
                         return $q.when(values.$promise || values)
                             .then(function(values) {
+
                                 scope.groups = {};
 
+                                if (values && keyName) {
+                                    //convert object data sources format to array data sources format
+                                    var arr = [];
+
+                                    angular.forEach(values, function (value, key) {
+                                        if (key.toString().charAt(0) !== '$') {
+                                            var item = {};
+
+                                            item[keyTitle] = key;
+                                            item[valueTitle] = value;
+                                            arr.push(item);
+                                        }
+                                    });
+
+                                    values = arr;
+                                }
+
                                 if (values && !selectedAs) {
                                     var outputValues = multiple ? scope.output : [];
-                                    var filteredList = listFilter(oiUtils.objToArr(values), query, getLabel, listFilterOptionsFn(scope.$parent), element);
+                                    var filteredList = listFilter(values, query, getLabel, listFilterOptionsFn(scope.$parent), element);
                                     var withoutIntersection = oiUtils.intersection(filteredList, outputValues, trackBy, trackBy, true);
                                     var filteredOutput = filter(withoutIntersection);
 
+                                    //add element with placeholder to empty list
+                                    if (!filteredOutput.length) {
+                                        scope.isEmptyList = true;
+
+                                        if (listPlaceholder) {
+                                            var context = {};
+
+                                            displayFn.assign(context, listPlaceholder);
+                                            filteredOutput = [context[valueName]]
+                                        }
+                                    }
+
                                     scope.groups = group(filteredOutput);
                                 }
                                 updateGroupPos();
diff --git a/src/filters.js b/src/filters.js
index adae0e4..b75f613 100644
--- a/src/filters.js
+++ b/src/filters.js
@@ -20,7 +20,7 @@ angular.module('oi.select')
 
         if (query.length > 0 || angular.isNumber(query)) {
             label = label.toString();
-            query = oiSelectEscape(query.toString());
+            query = oiSelectEscape(query);
 
             html = label.replace(new RegExp(query, 'gi'), '$&');
         } else {
@@ -36,16 +36,16 @@ angular.module('oi.select')
         var i, j, isFound, output, output1 = [], output2 = [], output3 = [], output4 = [];
 
         if (query) {
-            query = oiSelectEscape(String(query));
+            query = oiSelectEscape(query).toLocaleLowerCase();
 
             for (i = 0, isFound = false; i < input.length; i++) {
-                isFound = getLabel(input[i]).match(new RegExp(query, "i"));
+                isFound = getLabel(input[i]).toLocaleLowerCase().match(new RegExp(query));
 
                 if (!isFound && options && (options.length || options.fields)) {
                     for (j = 0; j < options.length; j++) {
                         if (isFound) break;
 
-                        isFound = String(input[i][options[j]]).match(new RegExp(query, "i"));
+                        isFound = String(input[i][options[j]]).toLocaleLowerCase().match(new RegExp(query));
                     }
                 }
 
@@ -54,7 +54,7 @@ angular.module('oi.select')
                 }
             }
             for (i = 0; i < output1.length; i++) {
-                if (getLabel(output1[i]).match(new RegExp('^' + query, "i"))) {
+                if (getLabel(output1[i]).toLocaleLowerCase().match(new RegExp('^' + query))) {
                     output2.push(output1[i]);
                 } else {
                     output3.push(output1[i]);
diff --git a/src/services.js b/src/services.js
index 2f16ba6..9067241 100644
--- a/src/services.js
+++ b/src/services.js
@@ -11,13 +11,14 @@ angular.module('oi.select')
             editItem:       false,
             newItem:        false,
             closeList:      true,
-            saveTrigger:    'enter tab blur'
+            saveTrigger:    'enter tab blur',
+            minlength:      0
         },
         version: {
-            full: '0.2.20',
+            full: '0.2.21',
             major: 0,
             minor: 2,
-            dot: 20
+            dot: 21
         },
         $get: function() {
             return {
@@ -291,18 +292,6 @@ angular.module('oi.select')
         return true;
     }
 
-    function objToArr(obj) {
-        var arr = [];
-
-        angular.forEach(obj, function (value, key) {
-            if (key.toString().charAt(0) !== '$') {
-                arr.push(value);
-            }
-        });
-
-        return arr;
-    }
-
     //lodash _.intersection + filter + invert
     function intersection(xArr, yArr, xFilter, yFilter, invert) {
         var i, j, n, filteredX, filteredY, out = invert ? [].concat(xArr) : [];
@@ -325,7 +314,7 @@ angular.module('oi.select')
     function getValue(valueName, item, scope, getter) {
         var locals = {};
 
-        //'name.subname' -> {name: {subname: list}}'
+        //'name.subname' -> {name: {subname: item}} -> locals'
         valueName.split('.').reduce(function (previousValue, currentItem, index, arr) {
             return previousValue[currentItem] = index < arr.length - 1 ? {} : item;
         }, locals);
@@ -338,7 +327,6 @@ angular.module('oi.select')
         bindFocusBlur: bindFocusBlur,
         scrollActiveOption: scrollActiveOption,
         groupsIsEmpty: groupsIsEmpty,
-        objToArr: objToArr,
         getValue: getValue,
         intersection: intersection
     }
diff --git a/src/style.styl b/src/style.styl
index 7e554f8..2b2f43f 100644
--- a/src/style.styl
+++ b/src/style.styl
@@ -189,4 +189,9 @@ oi-select
 
   &.loading:not(.multiple)
     .select-search:after
-      border-width 0
\ No newline at end of file
+      border-width 0
+
+  &.emptyList
+    .select-dropdown-optgroup-option
+      strong
+        font-weight normal
\ No newline at end of file