diff --git a/dist/angular-chosen.js b/dist/angular-chosen.js index 26be804..5871569 100644 --- a/dist/angular-chosen.js +++ b/dist/angular-chosen.js @@ -26,7 +26,7 @@ }); chosenModule.directive('chosen', [ - 'chosen', '$timeout', function(config, $timeout) { + 'chosen', '$timeout', '$parse', function(config, $timeout, $parse) { var CHOSEN_OPTION_WHITELIST, NG_OPTIONS_REGEXP, isEmpty, snakeCase; NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/; CHOSEN_OPTION_WHITELIST = ['persistentCreateOption', 'createOptionText', 'createOption', 'skipNoResults', 'noResultsText', 'allowSingleDeselect', 'disableSearchThreshold', 'disableSearch', 'enableSplitWordSearch', 'inheritSelectClasses', 'maxSelectedOptions', 'placeholderTextMultiple', 'placeholderTextSingle', 'searchContains', 'singleBackstrokeDelete', 'displayDisabledOptions', 'displaySelectedOptions', 'width', 'includeGroupLabelInSelected', 'maxShownResults']; @@ -50,13 +50,18 @@ }; return { restrict: 'A', - require: '?ngModel', + require: ['select', '?ngModel'], priority: 1, - link: function(scope, element, attr, ngModel) { - var chosen, directiveOptions, empty, initOrUpdate, match, options, origRender, startLoading, stopLoading, updateMessage, valuesExpr, viewWatch; + link: function(scope, element, attr, ctrls) { + var $render, chosen, directiveOptions, empty, init, match, ngModel, ngSelect, options, startLoading, stopLoading, trackBy, updateMessage, valuesExpr, viewWatch; scope.disabledValuesHistory = scope.disabledValuesHistory ? scope.disabledValuesHistory : []; element = $(element); element.addClass('localytics-chosen'); + ngSelect = ctrls[0]; + ngModel = ctrls[1]; + match = attr.ngOptions && attr.ngOptions.match(NG_OPTIONS_REGEXP); + valuesExpr = match && $parse(match[7]); + trackBy = match && match[8]; directiveOptions = scope.$eval(attr.chosen) || {}; options = angular.copy(config); angular.extend(options, directiveOptions); @@ -84,22 +89,10 @@ }; chosen = null; empty = false; - initOrUpdate = function() { - var defaultText, dropListDom; - if (chosen) { - dropListDom = $(element.next('.chosen-with-drop')); - if (dropListDom && dropListDom.length > 0) { - return; - } - return element.trigger('chosen:updated'); - } else { - scope.$evalAsync(function() { - chosen = element.chosen(options).data('chosen'); - }); - if (angular.isObject(chosen)) { - return defaultText = chosen.default_text; - } - } + init = function() { + return scope.$evalAsync(function() { + return chosen = element.chosen(options).data('chosen'); + }); }; updateMessage = function() { if (chosen && empty) { @@ -109,11 +102,23 @@ } return element.trigger('chosen:updated'); }; + init(); if (ngModel) { - origRender = ngModel.$render; + $render = ngModel.$render; ngModel.$render = function() { - origRender(); - return initOrUpdate(); + var isNotPrimitive, nextValue, previousValue, valueChanged; + try { + previousValue = ngSelect.readValue(); + } catch (error) {} + $render(); + try { + nextValue = ngSelect.readValue(); + } catch (error) {} + isNotPrimitive = trackBy || attr.multiple; + valueChanged = isNotPrimitive ? !angular.equals(previousValue, nextValue) : previousValue !== nextValue; + if (valueChanged) { + return element.trigger('chosen:updated'); + } }; element.on('chosen:hiding_dropdown', function() { return scope.$apply(function() { @@ -126,15 +131,11 @@ }; scope.$watch(viewWatch, ngModel.$render, true); } - } else { - initOrUpdate(); } attr.$observe('disabled', function() { return element.trigger('chosen:updated'); }); if (attr.ngOptions && ngModel) { - match = attr.ngOptions.match(NG_OPTIONS_REGEXP); - valuesExpr = match[7]; scope.$watchCollection(valuesExpr, function(newVal, oldVal) { var timer; return timer = $timeout(function() { diff --git a/dist/angular-chosen.min.js b/dist/angular-chosen.min.js index 5171c5c..a3c46f9 100644 --- a/dist/angular-chosen.min.js +++ b/dist/angular-chosen.min.js @@ -4,4 +4,4 @@ * @link http://github.com/leocaseiro/angular-chosen * @license MIT */ -(function(){var e,t=[].indexOf||function(e){for(var t=0,n=this.length;t=0)return u.$observe(n,function(e){var t;return t=String(l.attr(u.$attr[n])).slice(0,2),b[s(n)]="{{"===t?e:o.$eval(e),w()})}),y=function(){return l.addClass("loading").attr("disabled",!0).trigger("chosen:updated")},S=function(){return l.removeClass("loading"),angular.isDefined(u.disabled)?l.attr("disabled",u.disabled):l.attr("disabled",!1),l.trigger("chosen:updated")},d=null,h=!1,g=function(){var e,t;if(d){if(t=$(l.next(".chosen-with-drop")),t&&t.length>0)return;return l.trigger("chosen:updated")}if(o.$evalAsync(function(){d=l.chosen(b).data("chosen")}),angular.isObject(d))return e=d.default_text},w=function(){return d&&h?l.attr("data-placeholder",d.results_none_found).attr("disabled",!0):l.removeAttr("data-placeholder"),l.trigger("chosen:updated")},c?(v=c.$render,c.$render=function(){return v(),g()},l.on("chosen:hiding_dropdown",function(){return o.$apply(function(){return c.$setTouched()})}),u.multiple&&(m=function(){return c.$viewValue},o.$watch(m,c.$render,!0))):g(),u.$observe("disabled",function(){return l.trigger("chosen:updated")}),u.ngOptions&&c)return p=u.ngOptions.match(i),O=p[7],o.$watchCollection(O,function(e,t){var r;return r=n(function(){return angular.isUndefined(e)?y():(h=a(e),S(),w())})}),o.$on("$destroy",function(e){if("undefined"!=typeof timer&&null!==timer)return n.cancel(timer)})}}}])}).call(this); \ No newline at end of file +(function(){var e,t=[].indexOf||function(e){for(var t=0,n=this.length;t=0)return c.$observe(n,function(e){var t;return t=String(u.attr(c.$attr[n])).slice(0,2),w[l(n)]="{{"===t?e:o.$eval(e),C()})}),m=function(){return u.addClass("loading").attr("disabled",!0).trigger("chosen:updated")},O=function(){return u.removeClass("loading"),angular.isDefined(c.disabled)?u.attr("disabled",c.disabled):u.attr("disabled",!1),u.trigger("chosen:updated")},h=null,g=!1,v=function(){return o.$evalAsync(function(){return h=u.chosen(w).data("chosen")})},C=function(){return h&&g?u.attr("data-placeholder",h.results_none_found).attr("disabled",!0):u.removeAttr("data-placeholder"),u.trigger("chosen:updated")},v(),y&&(f=y.$render,y.$render=function(){var e,t,n,r;try{n=S.readValue()}catch(a){}f();try{t=S.readValue()}catch(a){}if(e=x||c.multiple,r=e?!angular.equals(n,t):n!==t)return u.trigger("chosen:updated")},u.on("chosen:hiding_dropdown",function(){return o.$apply(function(){return y.$setTouched()})}),c.multiple&&(V=function(){return y.$viewValue},o.$watch(V,y.$render,!0))),c.$observe("disabled",function(){return u.trigger("chosen:updated")}),c.ngOptions&&y)return o.$watchCollection(T,function(e,t){var r;return r=n(function(){return angular.isUndefined(e)?m():(g=s(e),O(),C())})}),o.$on("$destroy",function(e){if("undefined"!=typeof timer&&null!==timer)return n.cancel(timer)})}}}])}).call(this); \ No newline at end of file diff --git a/src/chosen.coffee b/src/chosen.coffee index 11de7ab..7e470a6 100644 --- a/src/chosen.coffee +++ b/src/chosen.coffee @@ -12,7 +12,7 @@ chosenModule.provider 'chosen', -> options } -chosenModule.directive 'chosen', ['chosen', '$timeout', (config, $timeout) -> +chosenModule.directive 'chosen', ['chosen', '$timeout', '$parse', (config, $timeout, $parse) -> # coffeelint: disable=max_line_length # This is stolen from Angular... NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/ @@ -51,13 +51,20 @@ chosenModule.directive 'chosen', ['chosen', '$timeout', (config, $timeout) -> true restrict: 'A' - require: '?ngModel' + require: ['select', '?ngModel'] priority: 1 - link: (scope, element, attr, ngModel) -> + link: (scope, element, attr, ctrls) -> scope.disabledValuesHistory = if scope.disabledValuesHistory then scope.disabledValuesHistory else [] element = $ element # Use real JQuery if it wasn't loaded before Angular. element.addClass('localytics-chosen') + ngSelect = ctrls[0] + ngModel = ctrls[1] + + match = attr.ngOptions && attr.ngOptions.match(NG_OPTIONS_REGEXP) + valuesExpr = match && $parse(match[7]) + trackBy = match && match[8] + # Take a hash of options from the chosen directive directiveOptions = scope.$eval(attr.chosen) or {} @@ -91,19 +98,9 @@ chosenModule.directive 'chosen', ['chosen', '$timeout', (config, $timeout) -> chosen = null empty = false - initOrUpdate = -> - if chosen - # Fix #56 Don't scroll to top when selecting multiple items with ctrl - dropListDom = $(element.next('.chosen-with-drop')) #uses jQuery instead of Angular. - if dropListDom && dropListDom.length > 0 - return - element.trigger('chosen:updated') - else - scope.$evalAsync -> - chosen = element.chosen(options).data('chosen') - return - if angular.isObject(chosen) - defaultText = chosen.default_text + init = -> + scope.$evalAsync -> + chosen = element.chosen(options).data('chosen') # Use Chosen's placeholder or no results found text depending on whether there are options available updateMessage = -> @@ -113,12 +110,25 @@ chosenModule.directive 'chosen', ['chosen', '$timeout', (config, $timeout) -> element.removeAttr('data-placeholder') element.trigger('chosen:updated') + init() + # Watch the underlying ngModel for updates and trigger an update when they occur. if ngModel - origRender = ngModel.$render + $render = ngModel.$render ngModel.$render = -> - origRender() - initOrUpdate() + # We need to try and detect if the select value was changed from outside of chosen + try previousValue = ngSelect.readValue() + $render() + try nextValue = ngSelect.readValue() + + isNotPrimitive = trackBy || attr.multiple + valueChanged = if isNotPrimitive + then !angular.equals(previousValue, nextValue) + else previousValue != nextValue + + # If it was changed, then we trigger a chosen re-render + if (valueChanged) + element.trigger('chosen:updated') element.on 'chosen:hiding_dropdown', -> scope.$apply -> ngModel.$setTouched() @@ -128,9 +138,6 @@ chosenModule.directive 'chosen', ['chosen', '$timeout', (config, $timeout) -> if attr.multiple viewWatch = -> ngModel.$viewValue scope.$watch viewWatch, ngModel.$render, true - # If we're not using ngModel (and therefore also not using ngOptions, which requires ngModel), - # just initialize chosen immediately since there's no need to wait for ngOptions to render first - else initOrUpdate() # Watch the disabled attribute (could be set by ngDisabled) attr.$observe 'disabled', -> element.trigger('chosen:updated') @@ -138,9 +145,6 @@ chosenModule.directive 'chosen', ['chosen', '$timeout', (config, $timeout) -> # Watch the collection in ngOptions and update chosen when it changes. This works with promises! # ngOptions doesn't do anything unless there is an ngModel, so neither do we. if attr.ngOptions and ngModel - match = attr.ngOptions.match(NG_OPTIONS_REGEXP) - valuesExpr = match[7] - scope.$watchCollection valuesExpr, (newVal, oldVal) -> # Defer execution until DOM is loaded timer = $timeout(->