diff --git a/dist/select.css b/dist/select.css deleted file mode 100644 index ae12d8ecc..000000000 --- a/dist/select.css +++ /dev/null @@ -1,167 +0,0 @@ -/*! - * ui-select - * http://github.com/angular-ui/ui-select - * Version: 0.8.3 - 2014-10-14T18:22:05.435Z - * License: MIT - */ - - -/* Style when highlighting a search. */ -.ui-select-highlight { - font-weight: bold; -} - -.ui-select-offscreen { - clip: rect(0 0 0 0) !important; - width: 1px !important; - height: 1px !important; - border: 0 !important; - margin: 0 !important; - padding: 0 !important; - overflow: hidden !important; - position: absolute !important; - outline: 0 !important; - left: 0px !important; - top: 0px !important; -} - -/* Select2 theme */ - -/* Mark invalid Select2 */ -.ng-dirty.ng-invalid > a.select2-choice { - border-color: #D44950; -} - -.select2-result-single { - padding-left: 0; -} - -/* Selectize theme */ - -/* Helper class to show styles when focus */ -.selectize-input.selectize-focus{ - border-color: #007FBB !important; -} - -/* Fix input width for Selectize theme */ -.selectize-control > .selectize-input > input { - width: 100%; -} - -/* Fix dropdown width for Selectize theme */ -.selectize-control > .selectize-dropdown { - width: 100%; -} - -/* Mark invalid Selectize */ -.ng-dirty.ng-invalid > div.selectize-input { - border-color: #D44950; -} - - -/* Bootstrap theme */ - -/* Helper class to show styles when focus */ -.btn-default-focus { - color: #333; - background-color: #EBEBEB; - border-color: #ADADAD; - text-decoration: none; - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); -} - - -/* Fix Bootstrap dropdown position when inside a input-group */ -.input-group > .ui-select-bootstrap.dropdown { - /* Instead of relative */ - position: static; -} - -.input-group > .ui-select-bootstrap > input.ui-select-search.form-control { - border-radius: 4px; /* FIXME hardcoded value :-/ */ - border-top-right-radius: 0; - border-bottom-right-radius: 0; -} - -.ui-select-bootstrap > .ui-select-match { - /* Instead of center because of .btn */ - text-align: left; -} - -.ui-select-bootstrap > .ui-select-match > .caret { - position: absolute; - top: 45%; - right: 15px; -} - -/* See Scrollable Menu with Bootstrap 3 http://stackoverflow.com/questions/19227496 */ -.ui-select-bootstrap > .ui-select-choices { - width: 100%; - height: auto; - max-height: 200px; - overflow-x: hidden; -} - -.ui-select-multiple.ui-select-bootstrap { - height: auto; - padding: .3em; -} - -.ui-select-multiple.ui-select-bootstrap input.ui-select-search { - background-color: transparent !important; /* To prevent double background when disabled */ - border: none; - outline: none; - height: 1.666666em; -} - -.ui-select-multiple.ui-select-bootstrap .ui-select-match .close { - font-size: 1.6em; - line-height: 0.75; -} - -.ui-select-multiple.ui-select-bootstrap .ui-select-match-item{ - outline: 0; -} - -.ui-select-bootstrap .ui-select-choices-row>a { - display: block; - padding: 3px 20px; - clear: both; - font-weight: 400; - line-height: 1.42857143; - color: #333; - white-space: nowrap; -} - -.ui-select-bootstrap .ui-select-choices-row>a:hover, .ui-select-bootstrap .ui-select-choices-row>a:focus { - text-decoration: none; - color: #262626; - background-color: #f5f5f5; -} - -.ui-select-bootstrap .ui-select-choices-row.active>a { - color: #fff; - text-decoration: none; - outline: 0; - background-color: #428bca; -} - -.ui-select-bootstrap .ui-select-choices-row.disabled>a, -.ui-select-bootstrap .ui-select-choices-row.active.disabled>a { - color: #777; - cursor: not-allowed; - background-color: #fff; -} - -/* fix hide/show angular animation */ -.ui-select-match.ng-hide-add, -.ui-select-search.ng-hide-add { - display: none !important; -} - -/* Mark invalid Bootstrap */ -.ui-select-bootstrap.ng-dirty.ng-invalid > button.btn.ui-select-match { - border-color: #D44950; -} diff --git a/dist/select.js b/dist/select.js deleted file mode 100755 index 3afe26f55..000000000 --- a/dist/select.js +++ /dev/null @@ -1,984 +0,0 @@ -/*! - * ui-select - * http://github.com/angular-ui/ui-select - * Version: 0.8.3 - 2014-10-14T18:22:05.432Z - * License: MIT - */ - - -(function () { - "use strict"; - - var KEY = { - TAB: 9, - ENTER: 13, - ESC: 27, - SPACE: 32, - LEFT: 37, - UP: 38, - RIGHT: 39, - DOWN: 40, - SHIFT: 16, - CTRL: 17, - ALT: 18, - PAGE_UP: 33, - PAGE_DOWN: 34, - HOME: 36, - END: 35, - BACKSPACE: 8, - DELETE: 46, - COMMAND: 91, - isControl: function (e) { - var k = e.which; - switch (k) { - case KEY.COMMAND: - case KEY.SHIFT: - case KEY.CTRL: - case KEY.ALT: - return true; - } - - if (e.metaKey) return true; - - return false; - }, - isFunctionKey: function (k) { - k = k.which ? k.which : k; - return k >= 112 && k <= 123; - }, - isVerticalMovement: function (k){ - return ~[KEY.UP, KEY.DOWN].indexOf(k); - }, - isHorizontalMovement: function (k){ - return ~[KEY.LEFT,KEY.RIGHT,KEY.BACKSPACE,KEY.DELETE].indexOf(k); - } - }; - - /** - * Add querySelectorAll() to jqLite. - * - * jqLite find() is limited to lookups by tag name. - * TODO This will change with future versions of AngularJS, to be removed when this happens - * - * See jqLite.find - why not use querySelectorAll? https://github.com/angular/angular.js/issues/3586 - * See feat(jqLite): use querySelectorAll instead of getElementsByTagName in jqLite.find https://github.com/angular/angular.js/pull/3598 - */ - if (angular.element.prototype.querySelectorAll === undefined) { - angular.element.prototype.querySelectorAll = function(selector) { - return angular.element(this[0].querySelectorAll(selector)); - }; - } - - angular.module('ui.select', []) - - .constant('uiSelectConfig', { - theme: 'bootstrap', - searchEnabled: true, - placeholder: '', // Empty by default, like HTML tag "); - - if(attrs.tabindex){ - //tabindex might be an expression, wait until it contains the actual value before we set the focusser tabindex - attrs.$observe('tabindex', function(value) { - //If we are using multiple, add tabindex to the search input - if($select.multiple){ - searchInput.attr("tabindex", value); - } else { - focusser.attr("tabindex", value); - } - //Remove the tabindex on the parent so that it is not focusable - element.removeAttr("tabindex"); - }); - } - - $compile(focusser)(scope); - $select.focusser = focusser; - - if (!$select.multiple){ - - element.append(focusser); - focusser.bind("focus", function(){ - scope.$evalAsync(function(){ - $select.focus = true; - }); - }); - focusser.bind("blur", function(){ - scope.$evalAsync(function(){ - $select.focus = false; - }); - }); - focusser.bind("keydown", function(e){ - - if (e.which === KEY.BACKSPACE) { - e.preventDefault(); - e.stopPropagation(); - $select.select(undefined); - scope.$apply(); - return; - } - - if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC) { - return; - } - - if (e.which == KEY.DOWN || e.which == KEY.UP || e.which == KEY.ENTER || e.which == KEY.SPACE){ - e.preventDefault(); - e.stopPropagation(); - $select.activate(); - } - - scope.$digest(); - }); - - focusser.bind("keyup input", function(e){ - - if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC || e.which == KEY.ENTER || e.which === KEY.BACKSPACE) { - return; - } - - $select.activate(focusser.val()); //User pressed some regular key, so we pass it to the search input - focusser.val(''); - scope.$digest(); - - }); - - } - - - scope.$watch('searchEnabled', function() { - var searchEnabled = scope.$eval(attrs.searchEnabled); - $select.searchEnabled = searchEnabled !== undefined ? searchEnabled : true; - }); - - attrs.$observe('disabled', function() { - // No need to use $eval() (thanks to ng-disabled) since we already get a boolean instead of a string - $select.disabled = attrs.disabled !== undefined ? attrs.disabled : false; - }); - - attrs.$observe('resetSearchInput', function() { - // $eval() is needed otherwise we get a string instead of a boolean - var resetSearchInput = scope.$eval(attrs.resetSearchInput); - $select.resetSearchInput = resetSearchInput !== undefined ? resetSearchInput : true; - }); - - if ($select.multiple){ - scope.$watchCollection(function(){ return ngModel.$modelValue; }, function(newValue, oldValue) { - if (oldValue != newValue) - ngModel.$modelValue = null; //Force scope model value and ngModel value to be out of sync to re-run formatters - }); - scope.$watchCollection('$select.selected', function() { - ngModel.$setViewValue(Date.now()); //Set timestamp as a unique string to force changes - }); - focusser.prop('disabled', true); //Focusser isn't needed if multiple - }else{ - scope.$watch('$select.selected', function(newValue) { - if (ngModel.$viewValue !== newValue) { - ngModel.$setViewValue(newValue); - } - }); - } - - ngModel.$render = function() { - if($select.multiple){ - // Make sure that model value is array - if(!angular.isArray(ngModel.$viewValue)){ - // Have tolerance for null or undefined values - if(angular.isUndefined(ngModel.$viewValue) || ngModel.$viewValue === null){ - $select.selected = []; - } else { - throw uiSelectMinErr('multiarr', "Expected model value to be array but got '{0}'", ngModel.$viewValue); - } - } - } - $select.selected = ngModel.$viewValue; - }; - - function onDocumentClick(e) { - var contains = false; - - if (window.jQuery) { - // Firefox 3.6 does not support element.contains() - // See Node.contains https://developer.mozilla.org/en-US/docs/Web/API/Node.contains - contains = window.jQuery.contains(element[0], e.target); - } else { - contains = element[0].contains(e.target); - } - - if (!contains) { - $select.close(); - scope.$digest(); - } - } - - // See Click everywhere but here event http://stackoverflow.com/questions/12931369 - $document.on('click', onDocumentClick); - - scope.$on('$destroy', function() { - $document.off('click', onDocumentClick); - }); - - // Move transcluded elements to their correct position in main template - transcludeFn(scope, function(clone) { - // See Transclude in AngularJS http://blog.omkarpatil.com/2012/11/transclude-in-angularjs.html - - // One day jqLite will be replaced by jQuery and we will be able to write: - // var transcludedElement = clone.filter('.my-class') - // instead of creating a hackish DOM element: - var transcluded = angular.element('
').append(clone); - - var transcludedMatch = transcluded.querySelectorAll('.ui-select-match'); - transcludedMatch.removeAttr('ui-select-match'); //To avoid loop in case directive as attr - if (transcludedMatch.length !== 1) { - throw uiSelectMinErr('transcluded', "Expected 1 .ui-select-match but got '{0}'.", transcludedMatch.length); - } - element.querySelectorAll('.ui-select-match').replaceWith(transcludedMatch); - - var transcludedChoices = transcluded.querySelectorAll('.ui-select-choices'); - transcludedChoices.removeAttr('ui-select-choices'); //To avoid loop in case directive as attr - if (transcludedChoices.length !== 1) { - throw uiSelectMinErr('transcluded', "Expected 1 .ui-select-choices but got '{0}'.", transcludedChoices.length); - } - element.querySelectorAll('.ui-select-choices').replaceWith(transcludedChoices); - }); - } - }; - }]) - - .directive('uiSelectChoices', - ['uiSelectConfig', 'RepeatParser', 'uiSelectMinErr', '$compile', - function(uiSelectConfig, RepeatParser, uiSelectMinErr, $compile) { - - return { - restrict: 'EA', - require: '^uiSelect', - replace: true, - transclude: true, - templateUrl: function(tElement) { - // Gets theme attribute from parent (ui-select) - var theme = tElement.parent().attr('theme') || uiSelectConfig.theme; - return theme + '/choices.tpl.html'; - }, - - compile: function(tElement, tAttrs) { - - if (!tAttrs.repeat) throw uiSelectMinErr('repeat', "Expected 'repeat' expression."); - - return function link(scope, element, attrs, $select, transcludeFn) { - - // var repeat = RepeatParser.parse(attrs.repeat); - var groupByExp = attrs.groupBy; - - $select.parseRepeatAttr(attrs.repeat, groupByExp); //Result ready at $select.parserResult - - $select.disableChoiceExpression = attrs.uiDisableChoice; - - if(groupByExp) { - var groups = element.querySelectorAll('.ui-select-choices-group'); - if (groups.length !== 1) throw uiSelectMinErr('rows', "Expected 1 .ui-select-choices-group but got '{0}'.", groups.length); - groups.attr('ng-repeat', RepeatParser.getGroupNgRepeatExpression()); - } - - var choices = element.querySelectorAll('.ui-select-choices-row'); - if (choices.length !== 1) { - throw uiSelectMinErr('rows', "Expected 1 .ui-select-choices-row but got '{0}'.", choices.length); - } - - choices.attr('ng-repeat', RepeatParser.getNgRepeatExpression($select.parserResult.itemName, '$select.items', $select.parserResult.trackByExp, groupByExp)) - .attr('ng-mouseenter', '$select.setActiveItem('+$select.parserResult.itemName +')') - .attr('ng-click', '$select.select(' + $select.parserResult.itemName + ')'); - - var rowsInner = element.querySelectorAll('.ui-select-choices-row-inner'); - if (rowsInner.length !== 1) throw uiSelectMinErr('rows', "Expected 1 .ui-select-choices-row-inner but got '{0}'.", rowsInner.length); - rowsInner.attr('uis-transclude-append', ''); //Adding uisTranscludeAppend directive to row element after choices element has ngRepeat - - $compile(element, transcludeFn)(scope); //Passing current transcludeFn to be able to append elements correctly from uisTranscludeAppend - - scope.$watch('$select.search', function(newValue) { - if(newValue && !$select.open && $select.multiple) $select.activate(false, true); - $select.activeIndex = 0; - $select.refresh(attrs.refresh); - }); - - attrs.$observe('refreshDelay', function() { - // $eval() is needed otherwise we get a string instead of a number - var refreshDelay = scope.$eval(attrs.refreshDelay); - $select.refreshDelay = refreshDelay !== undefined ? refreshDelay : uiSelectConfig.refreshDelay; - }); - }; - } - }; - }]) - // Recreates old behavior of ng-transclude. Used internally. - .directive('uisTranscludeAppend', function () { - return { - link: function (scope, element, attrs, ctrl, transclude) { - transclude(scope, function (clone) { - element.append(clone); - }); - } - }; - }) - .directive('uiSelectMatch', ['uiSelectConfig', function(uiSelectConfig) { - return { - restrict: 'EA', - require: '^uiSelect', - replace: true, - transclude: true, - templateUrl: function(tElement) { - // Gets theme attribute from parent (ui-select) - var theme = tElement.parent().attr('theme') || uiSelectConfig.theme; - var multi = tElement.parent().attr('multiple'); - return theme + (multi ? '/match-multiple.tpl.html' : '/match.tpl.html'); - }, - link: function(scope, element, attrs, $select) { - attrs.$observe('placeholder', function(placeholder) { - $select.placeholder = placeholder !== undefined ? placeholder : uiSelectConfig.placeholder; - }); - - if($select.multiple){ - $select.sizeSearchInput(); - } - - } - }; - }]) - - /** - * Highlights text that matches $select.search. - * - * Taken from AngularUI Bootstrap Typeahead - * See https://github.com/angular-ui/bootstrap/blob/0.10.0/src/typeahead/typeahead.js#L340 - */ - .filter('highlight', function() { - function escapeRegexp(queryToEscape) { - return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); - } - - return function(matchItem, query) { - return query && matchItem ? matchItem.replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : matchItem; - }; - }); -}()); - -angular.module("ui.select").run(["$templateCache", function($templateCache) {$templateCache.put("bootstrap/choices.tpl.html",""); -$templateCache.put("bootstrap/match-multiple.tpl.html"," × "); -$templateCache.put("bootstrap/match.tpl.html",""); -$templateCache.put("bootstrap/select-multiple.tpl.html","
"); -$templateCache.put("bootstrap/select.tpl.html","
"); -$templateCache.put("select2/choices.tpl.html",""); -$templateCache.put("select2/match-multiple.tpl.html","
  • "); -$templateCache.put("select2/match.tpl.html","{{$select.placeholder}} "); -$templateCache.put("select2/select-multiple.tpl.html","
    "); -$templateCache.put("select2/select.tpl.html","
    "); -$templateCache.put("selectize/choices.tpl.html","
    {{$group.name}}
    "); -$templateCache.put("selectize/match.tpl.html","
    "); -$templateCache.put("selectize/select.tpl.html","
    ");}]); \ No newline at end of file diff --git a/dist/select.min.css b/dist/select.min.css deleted file mode 100644 index cdb6a4f6f..000000000 --- a/dist/select.min.css +++ /dev/null @@ -1,6 +0,0 @@ -/*! - * ui-select - * http://github.com/angular-ui/ui-select - * Version: 0.8.3 - 2014-10-14T18:22:05.435Z - * License: MIT - */.ui-select-highlight{font-weight:700}.ui-select-offscreen{clip:rect(0 0 0 0)!important;width:1px!important;height:1px!important;border:0!important;margin:0!important;padding:0!important;overflow:hidden!important;position:absolute!important;outline:0!important;left:0!important;top:0!important}.ng-dirty.ng-invalid>a.select2-choice{border-color:#D44950}.select2-result-single{padding-left:0}.selectize-input.selectize-focus{border-color:#007FBB!important}.selectize-control>.selectize-dropdown,.selectize-control>.selectize-input>input{width:100%}.ng-dirty.ng-invalid>div.selectize-input{border-color:#D44950}.btn-default-focus{color:#333;background-color:#EBEBEB;border-color:#ADADAD;text-decoration:none;outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.input-group>.ui-select-bootstrap.dropdown{position:static}.input-group>.ui-select-bootstrap>input.ui-select-search.form-control{border-radius:4px 0 0 4px}.ui-select-bootstrap>.ui-select-match{text-align:left}.ui-select-bootstrap>.ui-select-match>.caret{position:absolute;top:45%;right:15px}.ui-select-bootstrap>.ui-select-choices{width:100%;height:auto;max-height:200px;overflow-x:hidden}.ui-select-multiple.ui-select-bootstrap{height:auto;padding:.3em}.ui-select-multiple.ui-select-bootstrap input.ui-select-search{background-color:transparent!important;border:none;outline:0;height:1.666666em}.ui-select-multiple.ui-select-bootstrap .ui-select-match .close{font-size:1.6em;line-height:.75}.ui-select-multiple.ui-select-bootstrap .ui-select-match-item{outline:0}.ui-select-bootstrap .ui-select-choices-row>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.ui-select-bootstrap .ui-select-choices-row>a:focus,.ui-select-bootstrap .ui-select-choices-row>a:hover{text-decoration:none;color:#262626;background-color:#f5f5f5}.ui-select-bootstrap .ui-select-choices-row.active>a{color:#fff;text-decoration:none;outline:0;background-color:#428bca}.ui-select-bootstrap .ui-select-choices-row.active.disabled>a,.ui-select-bootstrap .ui-select-choices-row.disabled>a{color:#777;cursor:not-allowed;background-color:#fff}.ui-select-match.ng-hide-add,.ui-select-search.ng-hide-add{display:none!important}.ui-select-bootstrap.ng-dirty.ng-invalid>button.btn.ui-select-match{border-color:#D44950} \ No newline at end of file diff --git a/dist/select.min.js b/dist/select.min.js deleted file mode 100755 index 0402e2a11..000000000 --- a/dist/select.min.js +++ /dev/null @@ -1,7 +0,0 @@ -/*! - * ui-select - * http://github.com/angular-ui/ui-select - * Version: 0.8.3 - 2014-10-14T18:22:05.432Z - * License: MIT - */ -!function(){"use strict";var e={TAB:9,ENTER:13,ESC:27,SPACE:32,LEFT:37,UP:38,RIGHT:39,DOWN:40,SHIFT:16,CTRL:17,ALT:18,PAGE_UP:33,PAGE_DOWN:34,HOME:36,END:35,BACKSPACE:8,DELETE:46,COMMAND:91,isControl:function(t){var c=t.which;switch(c){case e.COMMAND:case e.SHIFT:case e.CTRL:case e.ALT:return!0}return t.metaKey?!0:!1},isFunctionKey:function(e){return e=e.which?e.which:e,e>=112&&123>=e},isVerticalMovement:function(t){return~[e.UP,e.DOWN].indexOf(t)},isHorizontalMovement:function(t){return~[e.LEFT,e.RIGHT,e.BACKSPACE,e.DELETE].indexOf(t)}};void 0===angular.element.prototype.querySelectorAll&&(angular.element.prototype.querySelectorAll=function(e){return angular.element(this[0].querySelectorAll(e))}),angular.module("ui.select",[]).constant("uiSelectConfig",{theme:"bootstrap",searchEnabled:!0,placeholder:"",refreshDelay:1e3}).service("uiSelectMinErr",function(){var e=angular.$$minErr("ui.select");return function(){var t=e.apply(this,arguments),c=t.message.replace(new RegExp("\nhttp://errors.angularjs.org/.*"),"");return new Error(c)}}).service("RepeatParser",["uiSelectMinErr","$parse",function(e,t){var c=this;c.parse=function(c){var l=c.match(/^\s*(?:([\s\S]+?)\s+as\s+)?([\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);if(!l)throw e("iexp","Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.",c);return{itemName:l[2],source:t(l[3]),trackByExp:l[4],modelMapper:t(l[1]||l[2])}},c.getGroupNgRepeatExpression=function(){return"$group in $select.groups"},c.getNgRepeatExpression=function(e,t,c,l){var s=e+" in "+(l?"$group.items":t);return c&&(s+=" track by "+c),s}}]).controller("uiSelectCtrl",["$scope","$element","$timeout","RepeatParser","uiSelectMinErr",function(t,c,l,s,i){function n(){p.resetSearchInput&&(p.search=d,p.selected&&p.items.length&&!p.multiple&&(p.activeIndex=p.items.indexOf(p.selected)))}function a(t){var c=!0;switch(t){case e.DOWN:!p.open&&p.multiple?p.activate(!1,!0):p.activeIndex0&&p.activeIndex--;break;case e.TAB:(!p.multiple||p.open)&&p.select(p.items[p.activeIndex],!0);break;case e.ENTER:p.open?p.select(p.items[p.activeIndex]):p.activate(!1,!0);break;case e.ESC:p.close();break;default:c=!1}return c}function r(t){function c(){switch(t){case e.LEFT:return~p.activeMatchIndex?u:n;case e.RIGHT:return~p.activeMatchIndex&&a!==n?r:(p.activate(),!1);case e.BACKSPACE:return~p.activeMatchIndex?(p.removeChoice(a),u):n;case e.DELETE:return~p.activeMatchIndex?(p.removeChoice(p.activeMatchIndex),a):!1}}var l=o(h[0]),s=p.selected.length,i=0,n=s-1,a=p.activeMatchIndex,r=p.activeMatchIndex+1,u=p.activeMatchIndex-1,d=a;return l>0||p.search.length&&t==e.RIGHT?!1:(p.close(),d=c(),p.activeMatchIndex=p.selected.length&&d!==!1?Math.min(n,Math.max(i,d)):-1,!0)}function o(e){return angular.isNumber(e.selectionStart)?e.selectionStart:e.value.length}function u(){var e=c.querySelectorAll(".ui-select-choices-content"),t=e.querySelectorAll(".ui-select-choices-row");if(t.length<1)throw i("choices","Expected multiple .ui-select-choices-row but got '{0}'.",t.length);var l=t[p.activeIndex],s=l.offsetTop+l.clientHeight-e[0].scrollTop,n=e[0].offsetHeight;s>n?e[0].scrollTop+=s-n:s=p.items.length?0:p.activeIndex,l(function(){p.search=e||p.search,h[0].focus()}))},p.findGroupByName=function(e){return p.groups&&p.groups.filter(function(t){return t.name===e})[0]},p.parseRepeatAttr=function(e,c){function l(e){p.groups=[],angular.forEach(e,function(e){var l=t.$eval(c),s=angular.isFunction(l)?l(e):e[l],i=p.findGroupByName(s);i?i.items.push(e):p.groups.push({name:s,items:[e]})}),p.items=[],p.groups.forEach(function(e){p.items=p.items.concat(e.items)})}function n(e){p.items=e}var a=c?l:n;p.parserResult=s.parse(e),p.isGrouped=!!c,p.itemProperty=p.parserResult.itemName,t.$watchCollection(p.parserResult.source,function(e){if(void 0===e||null===e)p.items=[];else{if(!angular.isArray(e))throw i("items","Expected an array but got '{0}'.",e);if(p.multiple){var t=e.filter(function(e){return p.selected.indexOf(e)<0});a(t)}else a(e);p.ngModel.$modelValue=null}}),p.multiple&&t.$watchCollection("$select.selected",function(e){var c=p.parserResult.source(t);if(e.length){var l=c.filter(function(t){return e.indexOf(t)<0});a(l)}else a(c);p.sizeSearchInput()})};var v;p.refresh=function(e){void 0!==e&&(v&&l.cancel(v),v=l(function(){t.$eval(e)},p.refreshDelay))},p.setActiveItem=function(e){p.activeIndex=p.items.indexOf(e)},p.isActive=function(e){return p.open&&p.items.indexOf(e[p.itemProperty])===p.activeIndex},p.isDisabled=function(e){if(p.open){var t,c=p.items.indexOf(e[p.itemProperty]),l=!1;return c>=0&&!angular.isUndefined(p.disableChoiceExpression)&&(t=p.items[c],l=!!e.$eval(p.disableChoiceExpression),t._uiSelectChoiceDisabled=l),l}},p.select=function(e,c){if(void 0===e||!e._uiSelectChoiceDisabled){var l={};l[p.parserResult.itemName]=e,p.onSelectCallback(t,{$item:e,$model:p.parserResult.modelMapper(t,l)}),p.multiple?(p.selected.push(e),p.sizeSearchInput()):p.selected=e,p.close(c)}},p.close=function(e){p.open&&(n(),p.open=!1,p.multiple||l(function(){p.focusser.prop("disabled",!1),e||p.focusser[0].focus()},0,!1))},p.toggle=function(e){p.open?p.close():p.activate(),e.preventDefault(),e.stopPropagation()},p.removeChoice=function(e){var c=p.selected[e],l={};l[p.parserResult.itemName]=c,p.selected.splice(e,1),p.activeMatchIndex=-1,p.sizeSearchInput(),p.onRemoveCallback(t,{$item:c,$model:p.parserResult.modelMapper(t,l)})},p.getPlaceholder=function(){return p.multiple&&p.selected.length?void 0:p.placeholder};var f;p.sizeSearchInput=function(){var e=h[0],c=h.parent().parent()[0];h.css("width","10px");var s=function(){var t=c.clientWidth-e.offsetLeft-10;50>t&&(t=c.clientWidth),h.css("width",t+"px")};l(function(){0!==c.clientWidth||f?f||s():f=t.$watch(function(){return c.clientWidth},function(e){0!==e&&(s(),f(),f=null)})},0,!1)},h.on("keydown",function(c){var l=c.which;t.$apply(function(){var t=!1;p.multiple&&e.isHorizontalMovement(l)&&(t=r(l)),!t&&p.items.length>0&&(t=a(l)),t&&l!=e.TAB&&(c.preventDefault(),c.stopPropagation())}),e.isVerticalMovement(l)&&p.items.length>0&&u()}),h.on("blur",function(){l(function(){p.activeMatchIndex=-1})}),t.$on("$destroy",function(){h.off("keydown blur")})}]).directive("uiSelect",["$document","uiSelectConfig","uiSelectMinErr","$compile","$parse",function(t,c,l,s,i){return{restrict:"EA",templateUrl:function(e,t){var l=t.theme||c.theme;return l+(angular.isDefined(t.multiple)?"/select-multiple.tpl.html":"/select.tpl.html")},replace:!0,transclude:!0,require:["uiSelect","ngModel"],scope:!0,controller:"uiSelectCtrl",controllerAs:"$select",link:function(c,n,a,r,o){function u(e){var t=!1;t=window.jQuery?window.jQuery.contains(n[0],e.target):n[0].contains(e.target),t||(p.close(),c.$digest())}var p=r[0],d=r[1],h=n.querySelectorAll("input.ui-select-search");p.multiple=angular.isDefined(a.multiple)?""===a.multiple?!0:"true"===a.multiple.toLowerCase():!1,p.onSelectCallback=i(a.onSelect),p.onRemoveCallback=i(a.onRemove),d.$parsers.unshift(function(e){var t,l={};if(p.multiple){for(var s=[],i=p.selected.length-1;i>=0;i--)l={},l[p.parserResult.itemName]=p.selected[i],t=p.parserResult.modelMapper(c,l),s.unshift(t);return s}return l={},l[p.parserResult.itemName]=e,t=p.parserResult.modelMapper(c,l)}),d.$formatters.unshift(function(e){var t,l=p.parserResult.source(c,{$select:{search:""}}),s={};if(l){if(p.multiple){var i=[],n=function(e,l){if(e&&e.length){for(var n=e.length-1;n>=0;n--)if(s[p.parserResult.itemName]=e[n],t=p.parserResult.modelMapper(c,s),t==l)return i.unshift(e[n]),!0;return!1}};if(!e)return i;for(var a=e.length-1;a>=0;a--)n(p.selected,e[a])||n(l,e[a]);return i}var r=function(l){return s[p.parserResult.itemName]=l,t=p.parserResult.modelMapper(c,s),t==e};if(p.selected&&r(p.selected))return p.selected;for(var o=l.length-1;o>=0;o--)if(r(l[o]))return l[o]}return e}),p.ngModel=d;var v=angular.element("");a.tabindex&&a.$observe("tabindex",function(e){p.multiple?h.attr("tabindex",e):v.attr("tabindex",e),n.removeAttr("tabindex")}),s(v)(c),p.focusser=v,p.multiple||(n.append(v),v.bind("focus",function(){c.$evalAsync(function(){p.focus=!0})}),v.bind("blur",function(){c.$evalAsync(function(){p.focus=!1})}),v.bind("keydown",function(t){return t.which===e.BACKSPACE?(t.preventDefault(),t.stopPropagation(),p.select(void 0),void c.$apply()):void(t.which===e.TAB||e.isControl(t)||e.isFunctionKey(t)||t.which===e.ESC||((t.which==e.DOWN||t.which==e.UP||t.which==e.ENTER||t.which==e.SPACE)&&(t.preventDefault(),t.stopPropagation(),p.activate()),c.$digest()))}),v.bind("keyup input",function(t){t.which===e.TAB||e.isControl(t)||e.isFunctionKey(t)||t.which===e.ESC||t.which==e.ENTER||t.which===e.BACKSPACE||(p.activate(v.val()),v.val(""),c.$digest())})),c.$watch("searchEnabled",function(){var e=c.$eval(a.searchEnabled);p.searchEnabled=void 0!==e?e:!0}),a.$observe("disabled",function(){p.disabled=void 0!==a.disabled?a.disabled:!1}),a.$observe("resetSearchInput",function(){var e=c.$eval(a.resetSearchInput);p.resetSearchInput=void 0!==e?e:!0}),p.multiple?(c.$watchCollection(function(){return d.$modelValue},function(e,t){t!=e&&(d.$modelValue=null)}),c.$watchCollection("$select.selected",function(){d.$setViewValue(Date.now())}),v.prop("disabled",!0)):c.$watch("$select.selected",function(e){d.$viewValue!==e&&d.$setViewValue(e)}),d.$render=function(){if(p.multiple&&!angular.isArray(d.$viewValue)){if(!angular.isUndefined(d.$viewValue)&&null!==d.$viewValue)throw l("multiarr","Expected model value to be array but got '{0}'",d.$viewValue);p.selected=[]}p.selected=d.$viewValue},t.on("click",u),c.$on("$destroy",function(){t.off("click",u)}),o(c,function(e){var t=angular.element("
    ").append(e),c=t.querySelectorAll(".ui-select-match");if(c.removeAttr("ui-select-match"),1!==c.length)throw l("transcluded","Expected 1 .ui-select-match but got '{0}'.",c.length);n.querySelectorAll(".ui-select-match").replaceWith(c);var s=t.querySelectorAll(".ui-select-choices");if(s.removeAttr("ui-select-choices"),1!==s.length)throw l("transcluded","Expected 1 .ui-select-choices but got '{0}'.",s.length);n.querySelectorAll(".ui-select-choices").replaceWith(s)})}}}]).directive("uiSelectChoices",["uiSelectConfig","RepeatParser","uiSelectMinErr","$compile",function(e,t,c,l){return{restrict:"EA",require:"^uiSelect",replace:!0,transclude:!0,templateUrl:function(t){var c=t.parent().attr("theme")||e.theme;return c+"/choices.tpl.html"},compile:function(s,i){if(!i.repeat)throw c("repeat","Expected 'repeat' expression.");return function(s,i,n,a,r){var o=n.groupBy;if(a.parseRepeatAttr(n.repeat,o),a.disableChoiceExpression=n.uiDisableChoice,o){var u=i.querySelectorAll(".ui-select-choices-group");if(1!==u.length)throw c("rows","Expected 1 .ui-select-choices-group but got '{0}'.",u.length);u.attr("ng-repeat",t.getGroupNgRepeatExpression())}var p=i.querySelectorAll(".ui-select-choices-row");if(1!==p.length)throw c("rows","Expected 1 .ui-select-choices-row but got '{0}'.",p.length);p.attr("ng-repeat",t.getNgRepeatExpression(a.parserResult.itemName,"$select.items",a.parserResult.trackByExp,o)).attr("ng-mouseenter","$select.setActiveItem("+a.parserResult.itemName+")").attr("ng-click","$select.select("+a.parserResult.itemName+")");var d=i.querySelectorAll(".ui-select-choices-row-inner");if(1!==d.length)throw c("rows","Expected 1 .ui-select-choices-row-inner but got '{0}'.",d.length);d.attr("uis-transclude-append",""),l(i,r)(s),s.$watch("$select.search",function(e){e&&!a.open&&a.multiple&&a.activate(!1,!0),a.activeIndex=0,a.refresh(n.refresh)}),n.$observe("refreshDelay",function(){var t=s.$eval(n.refreshDelay);a.refreshDelay=void 0!==t?t:e.refreshDelay})}}}}]).directive("uisTranscludeAppend",function(){return{link:function(e,t,c,l,s){s(e,function(e){t.append(e)})}}}).directive("uiSelectMatch",["uiSelectConfig",function(e){return{restrict:"EA",require:"^uiSelect",replace:!0,transclude:!0,templateUrl:function(t){var c=t.parent().attr("theme")||e.theme,l=t.parent().attr("multiple");return c+(l?"/match-multiple.tpl.html":"/match.tpl.html")},link:function(t,c,l,s){l.$observe("placeholder",function(t){s.placeholder=void 0!==t?t:e.placeholder}),s.multiple&&s.sizeSearchInput()}}}]).filter("highlight",function(){function e(e){return e.replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1")}return function(t,c){return c&&t?t.replace(new RegExp(e(c),"gi"),'$&'):t}})}(),angular.module("ui.select").run(["$templateCache",function(e){e.put("bootstrap/choices.tpl.html",''),e.put("bootstrap/match-multiple.tpl.html",' × '),e.put("bootstrap/match.tpl.html",''),e.put("bootstrap/select-multiple.tpl.html",''),e.put("bootstrap/select.tpl.html",''),e.put("select2/choices.tpl.html",'
    • {{$group.name}}
    '),e.put("select2/match-multiple.tpl.html",'
  • '),e.put("select2/match.tpl.html",'{{$select.placeholder}} '),e.put("select2/select-multiple.tpl.html",'
    '),e.put("select2/select.tpl.html",'
    '),e.put("selectize/choices.tpl.html",'
    {{$group.name}}
    '),e.put("selectize/match.tpl.html",'
    '),e.put("selectize/select.tpl.html",'
    ')}]); \ No newline at end of file diff --git a/examples/demo-tagging.html b/examples/demo-tagging.html new file mode 100644 index 000000000..909c05ec6 --- /dev/null +++ b/examples/demo-tagging.html @@ -0,0 +1,122 @@ + + + + + AngularJS ui-select + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

    Tagging Demos

    + +

    Simple String Tags

    +

    (With Custom Tag Label)

    + + {{$item}} + + {{color}} + + +

    Selected: {{multipleDemo.colors}}

    +
    + +

    Simple String Tags

    +

    (Predictive Search Model / No Labels)

    + + {{$item}} + + {{color}} + + +

    Selected: {{multipleDemo.colors2}}

    +
    + +

    Object Tags

    + + {{$item.name}} <{{$item.email}}> + +
    +
    + + email: {{person.email}} + age: + +
    +
    +

    Selected: {{multipleDemo.selectedPeople}}

    +
    + +

    Object Tags with Tokenization (Space, Forward Slash, Comma)

    + Note that the SPACE character can't be used literally, use the keyword SPACE + + {{$item.name}} <{{$item.email}}> + +
    +
    + + email: {{person.email}} + age: + +
    +
    +

    Selected: {{multipleDemo.selectedPeople2}}

    + +
    + + + diff --git a/examples/demo.js b/examples/demo.js index e0cd943a8..402227995 100644 --- a/examples/demo.js +++ b/examples/demo.js @@ -106,6 +106,17 @@ app.controller('DemoCtrl', function($scope, $http, $timeout) { }; }; + $scope.tagTransform = function (newTag) { + var item = { + name: newTag, + email: newTag+'@email.com', + age: 'unknown', + country: 'unknown' + }; + + return item; + }; + $scope.person = {}; $scope.people = [ { name: 'Adam', email: 'adam@email.com', age: 12, country: 'United States' }, @@ -124,7 +135,9 @@ app.controller('DemoCtrl', function($scope, $http, $timeout) { $scope.multipleDemo = {}; $scope.multipleDemo.colors = ['Blue','Red']; + $scope.multipleDemo.colors2 = ['Blue','Red']; $scope.multipleDemo.selectedPeople = [$scope.people[5], $scope.people[4]]; + $scope.multipleDemo.selectedPeople2 = $scope.multipleDemo.selectedPeople; $scope.multipleDemo.selectedPeopleWithGroupBy = [$scope.people[8], $scope.people[6]]; $scope.multipleDemo.selectedPeopleSimple = ['samantha@email.com','wladimir@email.com']; diff --git a/src/select.js b/src/select.js index 3b81924b1..263d007b6 100644 --- a/src/select.js +++ b/src/select.js @@ -20,6 +20,10 @@ BACKSPACE: 8, DELETE: 46, COMMAND: 91, + + MAP: { 91 : "COMMAND", 8 : "BACKSPACE" , 9 : "TAB" , 13 : "ENTER" , 16 : "SHIFT" , 17 : "CTRL" , 18 : "ALT" , 19 : "PAUSEBREAK" , 20 : "CAPSLOCK" , 27 : "ESC" , 32 : "SPACE" , 33 : "PAGE_UP", 34 : "PAGE_DOWN" , 35 : "END" , 36 : "HOME" , 37 : "LEFT" , 38 : "UP" , 39 : "RIGHT" , 40 : "DOWN" , 43 : "+" , 44 : "PRINTSCREEN" , 45 : "INSERT" , 46 : "DELETE", 48 : "0" , 49 : "1" , 50 : "2" , 51 : "3" , 52 : "4" , 53 : "5" , 54 : "6" , 55 : "7" , 56 : "8" , 57 : "9" , 59 : ";", 61 : "=" , 65 : "A" , 66 : "B" , 67 : "C" , 68 : "D" , 69 : "E" , 70 : "F" , 71 : "G" , 72 : "H" , 73 : "I" , 74 : "J" , 75 : "K" , 76 : "L", 77 : "M" , 78 : "N" , 79 : "O" , 80 : "P" , 81 : "Q" , 82 : "R" , 83 : "S" , 84 : "T" , 85 : "U" , 86 : "V" , 87 : "W" , 88 : "X" , 89 : "Y" , 90 : "Z", 96 : "0" , 97 : "1" , 98 : "2" , 99 : "3" , 100 : "4" , 101 : "5" , 102 : "6" , 103 : "7" , 104 : "8" , 105 : "9", 106 : "*" , 107 : "+" , 109 : "-" , 110 : "." , 111 : "/", 112 : "F1" , 113 : "F2" , 114 : "F3" , 115 : "F4" , 116 : "F5" , 117 : "F6" , 118 : "F7" , 119 : "F8" , 120 : "F9" , 121 : "F10" , 122 : "F11" , 123 : "F12", 144 : "NUMLOCK" , 145 : "SCROLLLOCK" , 186 : ";" , 187 : "=" , 188 : "SPACE" , 189 : "-" , 190 : "." , 191 : "/" , 192 : "`" , 219 : "[" , 220 : "\\" , 221 : "]" , 222 : "'" + }, + isControl: function (e) { var k = e.which; switch (k) { @@ -138,8 +142,8 @@ * put as much logic in the controller (instead of the link functions) as possible so it can be easily tested. */ .controller('uiSelectCtrl', - ['$scope', '$element', '$timeout', 'RepeatParser', 'uiSelectMinErr', 'uiSelectConfig', - function($scope, $element, $timeout, RepeatParser, uiSelectMinErr, uiSelectConfig) { + ['$scope', '$element', '$timeout', '$filter', 'RepeatParser', 'uiSelectMinErr', 'uiSelectConfig', + function($scope, $element, $timeout, $filter, RepeatParser, uiSelectMinErr, uiSelectConfig) { var ctrl = this; @@ -153,16 +157,19 @@ ctrl.selected = undefined; ctrl.open = false; ctrl.focus = false; - ctrl.focusser = undefined; //Reference to input element used to handle focus events + ctrl.focusser = undefined; //Reference to input element used to handle focus events ctrl.disabled = undefined; // Initialized inside uiSelect directive link function ctrl.searchEnabled = undefined; // Initialized inside uiSelect directive link function ctrl.resetSearchInput = undefined; // Initialized inside uiSelect directive link function ctrl.refreshDelay = undefined; // Initialized inside uiSelectChoices directive link function ctrl.multiple = false; // Initialized inside uiSelect directive link function ctrl.disableChoiceExpression = undefined; // Initialized inside uiSelect directive link function + ctrl.tagging = {isActivated: false, fct: undefined}; + ctrl.taggingTokens = {isActivated: false, tokens: undefined}; ctrl.lockChoiceExpression = undefined; // Initialized inside uiSelect directive link function ctrl.closeOnSelect = true; // Initialized inside uiSelect directive link function ctrl.clickTriggeredSelect = false; + ctrl.$filter = $filter; ctrl.isEmpty = function() { return angular.isUndefined(ctrl.selected) || ctrl.selected === null || ctrl.selected === ''; @@ -194,6 +201,12 @@ ctrl.activeIndex = ctrl.activeIndex >= ctrl.items.length ? 0 : ctrl.activeIndex; + // ensure that the index is set to zero for tagging variants + // that where first option is auto-selected + if ( ctrl.activeIndex === -1 && ctrl.taggingLabel !== false ) { + ctrl.activeIndex = 0; + } + // Give it time to appear before focus $timeout(function() { ctrl.search = initSearchValue || ctrl.search; @@ -256,7 +269,7 @@ var filteredItems = items.filter(function(i) {return ctrl.selected.indexOf(i) < 0;}); setItemsFn(filteredItems); }else{ - setItemsFn(items); + setItemsFn(items); } ctrl.ngModel.$modelValue = null; //Force scope model value and ngModel value to be out of sync to re-run formatters @@ -266,14 +279,16 @@ }); if (ctrl.multiple){ - //Remove already selected items + //Remove already selected items $scope.$watchCollection('$select.selected', function(selectedItems){ var data = ctrl.parserResult.source($scope); if (!selectedItems.length) { - setItemsFn(data); + setItemsFn(data); }else{ - var filteredItems = data.filter(function(i) {return selectedItems.indexOf(i) < 0;}); - setItemsFn(filteredItems); + if ( data !== undefined ) { + var filteredItems = data.filter(function(i) {return selectedItems.indexOf(i) < 0;}); + setItemsFn(filteredItems); + } } ctrl.sizeSearchInput(); }); @@ -308,7 +323,15 @@ }; ctrl.isActive = function(itemScope) { - var isActive = ctrl.open && ctrl.items.indexOf(itemScope[ctrl.itemProperty]) === ctrl.activeIndex; + if ( !ctrl.open ) { + return false; + } + var itemIndex = ctrl.items.indexOf(itemScope[ctrl.itemProperty]); + var isActive = itemIndex === ctrl.activeIndex; + + if ( !isActive || ( itemIndex < 0 && ctrl.taggingLabel !== false ) ||( itemIndex < 0 && ctrl.taggingLabel === false) ) { + return false; + } if (isActive && !angular.isUndefined(ctrl.onHighlightCallback)) { itemScope.$eval(ctrl.onHighlightCallback); @@ -318,7 +341,7 @@ }; ctrl.isDisabled = function(itemScope) { - + if (!ctrl.open) return; var itemIndex = ctrl.items.indexOf(itemScope[ctrl.itemProperty]); @@ -334,35 +357,71 @@ return isDisabled; }; - // When the user clicks on an item inside the dropdown + + // When the user selects an item with ENTER or clicks the dropdown ctrl.select = function(item, skipFocusser, $event) { if (item === undefined || !item._uiSelectChoiceDisabled) { - var locals = {}; - locals[ctrl.parserResult.itemName] = item; - ctrl.onSelectCallback($scope, { - $item: item, - $model: ctrl.parserResult.modelMapper($scope, locals) - }); + if ( ! ctrl.items && ! ctrl.search ) return; - if(ctrl.multiple) { - ctrl.selected.push(item); - ctrl.sizeSearchInput(); - } else { - ctrl.selected = item; - } - if (!ctrl.multiple || ctrl.closeOnSelect) { - ctrl.close(skipFocusser); - } - if ($event && $event.type === 'click') { - ctrl.clickTriggeredSelect = true; + if (!item || !item._uiSelectChoiceDisabled) { + if(ctrl.tagging.isActivated) { + // if taggingLabel is disabled, we pull from ctrl.search val + if ( ctrl.taggingLabel === false ) { + if ( ctrl.activeIndex < 0 ) { + item = ctrl.tagging.fct !== undefined ? ctrl.tagging.fct(ctrl.search) : ctrl.search; + if ( angular.equals( ctrl.items[0], item ) ) { + return; + } + } else { + // keyboard nav happened first, user selected from dropdown + item = ctrl.items[ctrl.activeIndex]; + } + } else { + // tagging always operates at index zero, taggingLabel === false pushes + // the ctrl.search value without having it injected + if ( ctrl.activeIndex === 0 ) { + // ctrl.tagging pushes items to ctrl.items, so we only have empty val + // for `item` if it is a detected duplicate + if ( item === undefined ) return; + // create new item on the fly + item = ctrl.tagging.fct !== undefined ? ctrl.tagging.fct(ctrl.search) : item.replace(ctrl.taggingLabel,''); + } + } + // search ctrl.selected for dupes potentially caused by tagging and return early if found + if ( ctrl.selected && ctrl.selected.filter( function (selection) { return angular.equals(selection, item); }).length > 0 ) { + ctrl.close(skipFocusser); + return; + } + } + + var locals = {}; + locals[ctrl.parserResult.itemName] = item; + + ctrl.onSelectCallback($scope, { + $item: item, + $model: ctrl.parserResult.modelMapper($scope, locals) + }); + + if(ctrl.multiple) { + ctrl.selected.push(item); + ctrl.sizeSearchInput(); + } else { + ctrl.selected = item; + } + if (!ctrl.multiple || ctrl.closeOnSelect) { + ctrl.close(skipFocusser); + } + if ($event && $event.type === 'click') { + ctrl.clickTriggeredSelect = true; + } } } }; // Closes the dropdown ctrl.close = function(skipFocusser) { - if (!ctrl.open) return; + if (!ctrl.open) return; _resetSearchInput(); ctrl.open = false; if (!ctrl.multiple){ @@ -417,7 +476,7 @@ return ctrl.placeholder; }; - var containerSizeWatch; + var containerSizeWatch; ctrl.sizeSearchInput = function(){ var input = _searchInput[0], container = _searchInput.parent().parent()[0]; @@ -451,7 +510,7 @@ break; case KEY.UP: if (!ctrl.open && ctrl.multiple) ctrl.activate(false, true); //In case its the search input in 'multiple' mode - else if (ctrl.activeIndex > 0) { ctrl.activeIndex--; } + else if (ctrl.activeIndex > 0 || (ctrl.search.length === 0 && ctrl.tagging.isActivated)) { ctrl.activeIndex--; } break; case KEY.TAB: if (!ctrl.multiple || ctrl.open) ctrl.select(ctrl.items[ctrl.activeIndex], true); @@ -475,7 +534,7 @@ // Handles selected options in "multiple" mode function _handleMatchSelection(key){ var caretPosition = _getCaretPosition(_searchInput[0]), - length = ctrl.selected.length, + length = ctrl.selected.length, // none = -1, first = 0, last = length-1, @@ -498,7 +557,7 @@ break; case KEY.RIGHT: // Open drop-down - if(!~ctrl.activeMatchIndex || curr === last){ + if(!~ctrl.activeMatchIndex || curr === last){ ctrl.activate(); return false; } @@ -521,7 +580,7 @@ return curr; } else return false; - } + } } newIndex = getNewActiveMatchIndex(); @@ -549,10 +608,21 @@ processed = _handleMatchSelection(key); } - if (!processed && ctrl.items.length > 0) { + if (!processed && (ctrl.items.length > 0 || ctrl.tagging.isActivated)) { processed = _handleDropDownSelection(key); + if ( ctrl.taggingTokens.isActivated ) { + for (var i = 0; i < ctrl.taggingTokens.tokens.length; i++) { + if ( ctrl.taggingTokens.tokens[i] === KEY.MAP[e.keyCode] ) { + // make sure there is a new value to push via tagging + if ( ctrl.search.length > 0 ) { + ctrl.select(null, true); + _searchInput.triggerHandler('tagged'); + } + } + } + } } - + if (processed && key != KEY.TAB) { //TODO Check si el tab selecciona aun correctamente //Crear test @@ -567,12 +637,143 @@ }); + _searchInput.on('keyup', function(e) { + if ( ! KEY.isVerticalMovement(e.which) ) { + $scope.$evalAsync( function () { + ctrl.activeIndex = ctrl.taggingLabel === false ? -1 : 0; + }); + } + // Push a "create new" item into array if there is a search string + if ( ctrl.tagging.isActivated && ctrl.search.length > 0 ) { + + // return early with these keys + if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC || KEY.isVerticalMovement(e.which) ) { + return; + } + // always reset the activeIndex to the first item when tagging + ctrl.activeIndex = ctrl.taggingLabel === false ? -1 : 0; + // taggingLabel === false bypasses all of this + if (ctrl.taggingLabel === false) return; + + var items = angular.copy( ctrl.items ); + var stashArr = angular.copy( ctrl.items ); + var newItem; + var item; + var hasTag = false; + var dupeIndex = -1; + var tagItems; + var tagItem; + + // case for object tagging via transform `ctrl.tagging.fct` function + if ( ctrl.tagging.fct !== undefined) { + tagItems = ctrl.$filter('filter')(items,{'isTag': true}); + if ( tagItems.length > 0 ) { + tagItem = tagItems[0]; + } + // remove the first element, if it has the `isTag` prop we generate a new one with each keyup, shaving the previous + if ( items.length > 0 && tagItem ) { + hasTag = true; + items = items.slice(1,items.length); + stashArr = stashArr.slice(1,stashArr.length); + } + newItem = ctrl.tagging.fct(ctrl.search); + newItem.isTag = true; + // verify the the tag doesn't match the value of an existing item + if ( stashArr.filter( function (origItem) { return angular.equals( origItem, ctrl.tagging.fct(ctrl.search) ); } ).length > 0 ) { + return; + } + // handle newItem string and stripping dupes in tagging string context + } else { + // find any tagging items already in the ctrl.items array and store them + tagItems = ctrl.$filter('filter')(items,function (item) { + return item.match(ctrl.taggingLabel); + }); + if ( tagItems.length > 0 ) { + tagItem = tagItems[0]; + } + item = items[0]; + // remove existing tag item if found (should only ever be one tag item) + if ( item !== undefined && items.length > 0 && tagItem ) { + hasTag = true; + items = items.slice(1,items.length); + stashArr = stashArr.slice(1,stashArr.length); + } + newItem = ctrl.search+' '+ctrl.taggingLabel; + if ( _findApproxDupe(ctrl.selected, ctrl.search) > -1 ) { + return; + } + // verify the the tag doesn't match the value of an existing item from + // the searched data set + if ( stashArr.filter( function (origItem) { return origItem.toUpperCase() === ctrl.search.toUpperCase(); }).length > 0 ) { + // if there is a tag from prev iteration, strip it / queue the change + // and return early + if ( hasTag ) { + items = stashArr; + $scope.$evalAsync( function () { + ctrl.activeIndex = 0; + ctrl.items = items; + }); + } + return; + } + if ( ctrl.selected.filter( function (selection) { return selection.toUpperCase() === ctrl.search.toUpperCase(); } ).length > 0 ) { + // if there is a tag from prev iteration, strip it + if ( hasTag ) { + ctrl.items = stashArr.slice(1,stashArr.length); + } + return; + } + } + if ( hasTag ) dupeIndex = _findApproxDupe(ctrl.selected, newItem); + // dupe found, shave the first item + if ( dupeIndex > -1 ) { + items = items.slice(dupeIndex+1,items.length-1); + } else { + items = []; + items.push(newItem); + items = items.concat(stashArr); + } + $scope.$evalAsync( function () { + ctrl.activeIndex = 0; + ctrl.items = items; + }); + } + }); + + _searchInput.on('tagged', function() { + $timeout(function() { + _resetSearchInput(); + }); + }); + _searchInput.on('blur', function() { $timeout(function() { ctrl.activeMatchIndex = -1; }); }); + function _findApproxDupe(haystack, needle) { + var tempArr = angular.copy(haystack); + var dupeIndex = -1; + for (var i = 0; i view ngModel.$formatters.unshift(function (inputValue) { - var data = $select.parserResult.source (scope, { $select : {search:''}}), //Overwrite $search + var data = $select.parserResult.source (scope, { $select : {search:''}}), //Overwrite $search locals = {}, result; if (data){ @@ -710,7 +911,7 @@ if(attrs.tabindex){ //tabindex might be an expression, wait until it contains the actual value before we set the focusser tabindex attrs.$observe('tabindex', function(value) { - //If we are using multiple, add tabindex to the search input + //If we are using multiple, add tabindex to the search input if($select.multiple){ searchInput.attr("tabindex", value); } else { @@ -765,7 +966,7 @@ if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC || e.which == KEY.ENTER || e.which === KEY.BACKSPACE) { return; } - + $select.activate(focusser.val()); //User pressed some regular key, so we pass it to the search input focusser.val(''); scope.$digest(); @@ -791,6 +992,45 @@ $select.resetSearchInput = resetSearchInput !== undefined ? resetSearchInput : true; }); + attrs.$observe('tagging', function() { + if(attrs.tagging !== undefined) + { + // $eval() is needed otherwise we get a string instead of a boolean + var taggingEval = scope.$eval(attrs.tagging); + $select.tagging = {isActivated: true, fct: taggingEval !== true ? taggingEval : undefined}; + } + else + { + $select.tagging = {isActivated: false, fct: undefined}; + } + }); + + attrs.$observe('taggingLabel', function() { + if(attrs.tagging !== undefined && attrs.taggingLabel !== undefined) + { + // check eval for FALSE, in this case, we disable the labels + // associated with tagging + if ( attrs.taggingLabel === 'false' ) { + $select.taggingLabel = false; + } + else + { + $select.taggingLabel = attrs.taggingLabel !== undefined ? attrs.taggingLabel : '(new)'; + } + } + else + { + $select.taggingLabel = false; + } + }); + + attrs.$observe('taggingTokens', function() { + if (attrs.tagging !== undefined) { + var tokens = attrs.taggingTokens !== undefined ? attrs.taggingTokens.split('|') : [',','ENTER']; + $select.taggingTokens = {isActivated: true, tokens: tokens }; + } + }); + if ($select.multiple){ scope.$watchCollection(function(){ return ngModel.$modelValue; }, function(newValue, oldValue) { if (oldValue != newValue) @@ -895,7 +1135,7 @@ if (!tAttrs.repeat) throw uiSelectMinErr('repeat', "Expected 'repeat' expression."); return function link(scope, element, attrs, $select, transcludeFn) { - + // var repeat = RepeatParser.parse(attrs.repeat); var groupByExp = attrs.groupBy; @@ -928,7 +1168,7 @@ scope.$watch('$select.search', function(newValue) { if(newValue && !$select.open && $select.multiple) $select.activate(false, true); - $select.activeIndex = 0; + $select.activeIndex = $select.tagging.isActivated ? -1 : 0; $select.refresh(attrs.refresh); }); @@ -970,7 +1210,7 @@ }); if($select.multiple){ - $select.sizeSearchInput(); + $select.sizeSearchInput(); } } diff --git a/src/select2/match-multiple.tpl.html b/src/select2/match-multiple.tpl.html index 5d60d11c6..e1578c0ba 100644 --- a/src/select2/match-multiple.tpl.html +++ b/src/select2/match-multiple.tpl.html @@ -9,4 +9,4 @@ - \ No newline at end of file + diff --git a/test/select.spec.js b/test/select.spec.js index 461ec3c13..363ba6938 100644 --- a/test/select.spec.js +++ b/test/select.spec.js @@ -68,6 +68,7 @@ describe('ui-select tests', function() { if (attrs.required !== undefined) { attrsHtml += ' ng-required="' + attrs.required + '"'; } if (attrs.theme !== undefined) { attrsHtml += ' theme="' + attrs.theme + '"'; } if (attrs.tabindex !== undefined) { attrsHtml += ' tabindex="' + attrs.tabindex + '"'; } + if (attrs.tagging !== undefined) { attrsHtml += ' tagging="' + attrs.tagging + '"'; } } return compileTemplate( @@ -260,6 +261,40 @@ describe('ui-select tests', function() { expect(isDropdownOpened(el3)).toEqual(true); }); + it('should allow tagging if the attribute says so', function() { + var el = createUiSelect({tagging: true}); + clickMatch(el); + + $(el).scope().$select.select("I don't exist"); + + expect($(el).scope().$select.selected).toEqual("I don't exist"); + }); + + it('should format new items using the tagging function when the attribute is a function', function() { + scope.taggingFunc = function (name) { + return { + name: name, + email: name + '@email.com', + group: 'Foo', + age: 12 + }; + }; + + var el = createUiSelect({tagging: 'taggingFunc'}); + clickMatch(el); + + $(el).scope().$select.search = 'idontexist'; + $(el).scope().$select.activeIndex = 0; + $(el).scope().$select.select('idontexist'); + + expect($(el).scope().$select.selected).toEqual({ + name: 'idontexist', + email: 'idontexist@email.com', + group: 'Foo', + age: 12 + }); + }); + // See when an item that evaluates to false (such as "false" or "no") is selected, the placeholder is shown https://github.com/angular-ui/ui-select/pull/32 it('should not display the placeholder when item evaluates to false', function() { scope.items = ['false'];