Repository: localytics/angular-chosen Branch: master Commit: 5675f52e9916 Files: 35 Total size: 48.9 KB Directory structure: gitextract_uvf_2ben/ ├── .gitignore ├── .npmignore ├── .travis.yml ├── ISSUE_TEMPLATE.md ├── LICENSE ├── README.md ├── bower.json ├── chosen-spinner.css ├── chosen.js ├── dist/ │ └── angular-chosen.js ├── example/ │ ├── index.html │ └── index.js ├── gulpfile.js ├── package.json ├── src/ │ ├── .editorconfig │ ├── chosen.coffee │ └── coffeelint.json └── test/ ├── base.spec.js ├── chosenAttrSpec.js ├── chosenOptions/ │ ├── allowSingleDeselected.spec.js │ ├── disableSearch.spec.js │ ├── disableSearchThreshold.spec.js │ ├── inheritSelectClasses.spec.js │ ├── maxShownResults.spec.js │ ├── noResultsText.spec.js │ ├── placeholderTextMultiple.spec.js │ ├── placeholderTextSingle.spec.js │ ├── searchContains.spec.js │ └── width.spec.js ├── form.spec.js ├── issues/ │ └── 179-ng-if-breaks-inherit-select-classes.spec.js └── support/ ├── caseConvertFilter.js ├── chosenSelectHelper.js ├── karma.conf.js └── specHelper.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ bower_components/ node_modules/ ================================================ FILE: .npmignore ================================================ gulpfile.js src test .gitignore .travis.yml ================================================ FILE: .travis.yml ================================================ sudo: false language: node_js cache: directories: - node_modules notifications: email: false node_js: - '6' before_install: - npm i -g npm@^3.10.7 before_script: - npm prune script: - npm run test branches: except: - "/^v\\d+\\.\\d+\\.\\d+$/" ================================================ FILE: ISSUE_TEMPLATE.md ================================================ > If you are going to be lazy asking me properly, I'll be lazy answering to you (@leocaseiro) ### Please make sure you can mark all the options, before open this Issue. I'll prioritise the issues that are consistent and completed. - [ ] I've read the documentation on http://leocaseiro.github.io/angular-chosen. - [ ] I've searched on [github issues](https://github.com/leocaseiro/angular-chosen/issues?utf8=%E2%9C%93&q=is%3Aissue) and [stackoverflow](http://stackoverflow.com/questions/tagged/angular-chosen) before open this issue. - [ ] I've tested with the [angular native select input](https://docs.angularjs.org/api/ng/directive/select) without the chosen directive - [ ] I've tested with the oficial [jquery chosen](https://harvesthq.github.io/chosen/) without the angular chosen directive ## Please, post your plunker link here: > (plunker link goes here) If you need a starter plunker for a single select, use this one: https://plnkr.co/edit/Ec9l1C?p=preview If you need a starter for a multiple select, use this one: https://plnkr.co/edit/vaRw1x?p=preview ## Write your issue: ================================================ FILE: LICENSE ================================================ The MIT License Copyright (c) 2013 Localytics http://www.localytics.com Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ Angular Chosen Localytics [![Bower](https://img.shields.io/bower/v/angular-chosen-localytics.svg)](https://github.com/leocaseiro/angular-chosen) [![npm](https://img.shields.io/npm/v/angular-chosen-localytics.svg)](https://www.npmjs.com/package/angular-chosen-localytics) ============== AngularJS Chosen directive This directive brings the [Chosen](http://harvesthq.github.com/chosen/) jQuery plugin into AngularJS with ngModel and ngOptions integration. To use, include `localytics.directives` as a dependency in your Angular module. You can now use the `chosen` directive as an attribute on any select element. Angular version 1.3+ is required, but recommended 1.4.9+. # [Full Documentation with Examples](http://leocaseiro.github.io/angular-chosen/) * Documentation on [Github Page](http://leocaseiro.github.io/angular-chosen/) * Examples on [example/index.html](http://htmlpreview.github.io/?https://github.com/leocaseiro/angular-chosen/blob/master/example/index.html) ## Installation (npm or bower) Via bower $ bower install angular-chosen-localytics --save Via npm $ npm install angular-chosen-localytics --save Via [cdn](https://cdnjs.com/libraries/angular-chosen-localytics) ```html ``` Or download zip file > [Download v1.9.2](https://github.com/leocaseiro/angular-chosen/archive/1.9.2.zip) ## Yeoman or Bower install If you use Yeoman or Bower install, you need to rename the `chosen.jquery.js` or the `chosen.proto.js` to `chosen.js`. Otherwise Chosen won't be included in your `index.html`. # Features * Works with `ngModel` and `ngOptions` * Supports usage of promises in `ngOptions` * Provides a 'loading' animation when 'ngOptions' collection is a promise backed by a remote source * Pass options to `Chosen` via attributes or by passing an object to the Chosen directive * Provider with default values with `chosenProvider` ([read: Added config-provider](https://github.com/leocaseiro/angular-chosen/pull/231)) [since 1.6.0] # Usage ### Simple example Similar to `$("#states").chosen()` ```html ``` ### Pass any chosen options as attributes ```html ``` > Note: the empty option element is mandatory when using `allow-single-deselect` ### Integration with `ngModel` and `ngOptions` ```html ``` > Note: don't try to use `ngModel` with `ngRepeat`. It won't work. Use `ngOptions`. It's better that way. > Also important: if your `ngModel` is null or undefined, you must manually include an empty option inside your ` ``` > This annoying behavior is alluded to in the examples in the [documentation for ngOptions](http://docs.angularjs.org/api/ng.directive:select). #### Works well with other AngularJS directives ```html ``` ### Loading from a remote data source Include `chosen-spinner.css` and `spinner.gif` to show an Ajax spinner icon while your data is loading. If the collection comes back empty, the directive will disable the element and show a default "No values available" message. You can customize this message by passing in `noResultsText` in your options. ##### app.js ```js angular.module('App', ['ngResource', 'localytics.directives']) .controller('BeerCtrl', function($scope, $resource) { $scope.beers = $resource('api/beers').query() }); ``` ##### index.html ```html
``` Image of select defined above in loading state: `` > Note: Assigning promises directly to scope is now deprecated in Angular 1.2+. Assign the results of the promise to scope once the promise returns. The loader animation will still work as long as the collection expression evaluates to `undefined` while your data is loading! ### Default values with chosenProvider (thanks @zlodes) [since 1.6.0] ```javascript angular.module('chosenExampleApp', ['localytics.directives']) .config(['chosenProvider', function (chosenProvider) { chosenProvider.setOption({ no_results_text: 'There is no results!', placeholder_text_multiple: 'Choose one or more!' }); }]); ``` ================================================ FILE: bower.json ================================================ { "name": "angular-chosen-localytics", "repository": { "type": "git", "url": "git://github.com/leocaseiro/angular-chosen.git" }, "main": "dist/angular-chosen.js", "ignore": [ "gulpfile.js", "src", "test" ], "dependencies": { "jquery": "^2.0.3", "chosen": "^1.5.1", "angular": "^1.4.9" }, "license": "MIT" } ================================================ FILE: chosen-spinner.css ================================================ /* Additional styles to display a spinner image while options are loading */ .localytics-chosen.loading+.chosen-container-multi .chosen-choices { background-image: url('spinner.gif'); background-repeat: no-repeat; background-position: 95%; } .localytics-chosen.loading+.chosen-container-single .chosen-single span { background: url('spinner.gif') no-repeat right; } .localytics-chosen.loading+.chosen-container-single .chosen-single .search-choice-close { display: none; } ================================================ FILE: chosen.js ================================================ console.warn('the file ./chosen.js is deprecated, you must include ./dist/angular-chosen.js or ./dist/angular-chosen.min.js instead'); ================================================ FILE: dist/angular-chosen.js ================================================ /** * angular-chosen-localytics - Angular Chosen directive is an AngularJS Directive that brings the Chosen jQuery in a Angular way * @version v1.9.2 * @link http://github.com/leocaseiro/angular-chosen * @license MIT */ (function() { var chosenModule, indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; angular.module('localytics.directives', []); chosenModule = angular.module('localytics.directives'); chosenModule.provider('chosen', function() { var options; options = {}; return { setOption: function(newOpts) { angular.extend(options, newOpts); }, $get: function() { return options; } }; }); chosenModule.directive('chosen', [ '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 = ['allowSingleDeselect', 'disableSearch', 'disableSearchThreshold', 'enableSplitWordSearch', 'inheritSelectClasses', 'maxSelectedOptions', 'noResultsText', 'placeholderTextMultiple', 'placeholderTextSingle', 'searchContains', 'groupSearch', 'singleBackstrokeDelete', 'width', 'displayDisabledOptions', 'displaySelectedOptions', 'includeGroupLabelInSelected', 'maxShownResults', 'caseSensitiveSearch', 'hideResultsOnSelect', 'rtl']; snakeCase = function(input) { return input.replace(/[A-Z]/g, function($1) { return "_" + ($1.toLowerCase()); }); }; isEmpty = function(value) { var key; if (angular.isArray(value)) { return value.length === 0; } else if (angular.isObject(value)) { for (key in value) { if (value.hasOwnProperty(key)) { return false; } } } return true; }; return { restrict: 'A', require: ['select', '?ngModel'], priority: 1, link: function(scope, element, attr, ctrls) { var $render, chosen, directiveOptions, disableIfEmpty, empty, initIfNotInitialized, match, ngModel, ngSelect, options, startLoading, stopLoading, timer, trackBy, 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); angular.forEach(attr, function(value, key) { if (indexOf.call(CHOSEN_OPTION_WHITELIST, key) >= 0) { return attr.$observe(key, function(value) { var prefix; prefix = String(element.attr(attr.$attr[key])).slice(0, 2); options[snakeCase(key)] = prefix === '{{' ? value : scope.$eval(value); return disableIfEmpty(); }); } }); startLoading = function() { return element.addClass('loading').attr('disabled', true).trigger('chosen:updated'); }; stopLoading = function() { element.removeClass('loading'); if (angular.isDefined(attr.disabled)) { element.attr('disabled', attr.disabled); } else { element.attr('disabled', false); } return element.trigger('chosen:updated'); }; chosen = null; empty = false; initIfNotInitialized = function() { if (!chosen) { return scope.$evalAsync(function() { if (!chosen) { return chosen = element.chosen(options).data('chosen'); } }); } }; disableIfEmpty = function() { if (chosen && empty) { element.attr('disabled', true); } return element.trigger('chosen:updated'); }; if (ngModel) { $render = ngModel.$render; ngModel.$render = function() { var isPrimitive, nextValue, previousValue, valueChanged; initIfNotInitialized(); try { previousValue = ngSelect.readValue(); } catch (error) {} $render(); try { nextValue = ngSelect.readValue(); } catch (error) {} isPrimitive = !trackBy && !attr.multiple; valueChanged = isPrimitive ? previousValue !== nextValue : !angular.equals(previousValue, nextValue); if (valueChanged) { return element.trigger('chosen:updated'); } }; element.on('chosen:hiding_dropdown', function() { return scope.$applyAsync(function() { return ngModel.$setTouched(); }); }); if (attr.multiple) { viewWatch = function() { return ngModel.$viewValue; }; scope.$watch(viewWatch, ngModel.$render, true); } } else { initIfNotInitialized(); } attr.$observe('disabled', function() { return element.trigger('chosen:updated'); }); if (attr.ngOptions && ngModel) { timer = null; scope.$watchCollection(valuesExpr, function(newVal, oldVal) { return timer = $timeout(function() { if (angular.isUndefined(newVal)) { return startLoading(); } else { empty = isEmpty(newVal); stopLoading(); return disableIfEmpty(); } }); }); return scope.$on('$destroy', function(event) { if (timer != null) { return $timeout.cancel(timer); } }); } } }; } ]); }).call(this); ================================================ FILE: example/index.html ================================================

Chosen Directive Example Usage

Chosen with a promise: {{foo | json}}

Chosen with a promised hash: {{baz | json}}

Chosen with static options: {{ bar }}

Passing options in as a hash: {{ woo }}

Disabling the element with a message when the collection is empty: {{ whatever }}

Empty?

Changing the ngModel externally: {{ myPets }}

Disabling the selection:


Form validation

Shows error after user selects nothing (works with Angular 1.3+, uses $touched + $invalid)

Error: {{form.fun.$error}}

#179 - inherit-select-class inside ng-if:


#59 - Don't scroll to top when selecting multiple items with ctrl

================================================ FILE: example/index.js ================================================ // Generated by CoffeeScript 1.6.2 (function() { angular.module('chosenExampleApp', ['localytics.directives']) .config(['chosenProvider', function (chosenProvider) { chosenProvider.setOption({ no_results_text: 'Haha! There is no results!', placeholder_text_multiple: 'Choose a few!' }); }]) .controller('IndexCtrl', [ '$scope', '$q', '$timeout', function($scope, $q, $timeout) { var simulateAjax; simulateAjax = function(result) { var deferred, fn; deferred = $q.defer(); fn = function() { return deferred.resolve(result); }; $timeout(fn, 3000); return deferred.promise; }; simulateAjax(['grooo', 'wowowowow', 'lakakalakakl', 'yadayada', 'insight', 'delve', 'synergy']).then(function(result) { return $scope.optionsFromQuery = result; }); $scope.optionsFromQueryAsHash = (function() { var result; result = { win: "Brilliant Escape", fail: "Untimely Demise" }; return simulateAjax(result); })(); $scope.$watch('emptyCollection', function(empty) { return $scope.emptyOptions = simulateAjax(empty ? [] : ['look', 'i', 'have', 'data']); }); $scope.directiveOptions = { no_results_text: "SO SORRY" }; $scope.ngIfInherit = true; $scope.myPets = ['cat']; $scope.pets = { cat: 'Cat', dog: 'Dog', hamster: 'Hamster' }; $scope.state = ['California', 'Arizona']; $scope.states = { "AL": "Alabama", "AK": "Alaska", "AS": "American Samoa", "AZ": "Arizona", "AR": "Arkansas", "CA": "California", "CO": "Colorado", "CT": "Connecticut", "DE": "Delaware", "DC": "District Of Columbia", "FM": "Federated States Of Micronesia", "FL": "Florida", "GA": "Georgia", "GU": "Guam", "HI": "Hawaii", "ID": "Idaho", "IL": "Illinois", "IN": "Indiana", "IA": "Iowa", "KS": "Kansas", "KY": "Kentucky", "LA": "Louisiana", "ME": "Maine", "MH": "Marshall Islands", "MD": "Maryland", "MA": "Massachusetts", "MI": "Michigan", "MN": "Minnesota", "MS": "Mississippi", "MO": "Missouri", "MT": "Montana", "NE": "Nebraska", "NV": "Nevada", "NH": "New Hampshire", "NJ": "New Jersey", "NM": "New Mexico", "NY": "New York", "NC": "North Carolina", "ND": "North Dakota", "MP": "Northern Mariana Islands", "OH": "Ohio", "OK": "Oklahoma", "OR": "Oregon", "PW": "Palau", "PA": "Pennsylvania", "PR": "Puerto Rico", "RI": "Rhode Island", "SC": "South Carolina", "SD": "South Dakota", "TN": "Tennessee", "TX": "Texas", "UT": "Utah", "VT": "Vermont", "VI": "Virgin Islands", "VA": "Virginia", "WA": "Washington", "WV": "West Virginia", "WI": "Wisconsin", "WY": "Wyoming" }; $timeout(function() { return $scope.$apply(function() { return $scope.myPets.push('hamster'); }); }, 1000); return $scope.disabled = true; } ]); }).call(this); ================================================ FILE: gulpfile.js ================================================ var config = { test: './test', src: './src/', dist: './dist/', file: 'angular-chosen' }; var banner = ['/**', ' * <%= pkg.name %> - <%= pkg.description %>', ' * @version v<%= pkg.version %>', ' * @link <%= pkg.homepage %>', ' * @license <%= pkg.license %>', ' */', ''].join('\n'); var args = require('yargs').argv, gulp = require('gulp'), karma = require('karma'), del = require('del'), $ = require('gulp-load-plugins')({ lazy: true }), pkg = require('./package.json'); // List Tasks by default gulp.task('default', $.taskListing.withFilters(null, ['build-hint'])); gulp.task('build-hint', function() { return gulp.src(config.src + '/*.coffee') .pipe($.if(args.debug, $.debug())) .pipe($.plumber()) .pipe($.coffeelint('./src/coffeelint.json')) .pipe($.coffeelint.reporter()); }); /** * Compile CoffeeScript into ./dist/angular-chose.js */ gulp.task('build-coffee-script', ['build-hint'], function() { return gulp.src(config.src + '/*.coffee') .pipe($.if(args.debug, $.debug())) .pipe($.plumber()) .pipe($.coffee().on('error', $.util.log)) .pipe($.rename(config.file + '.js')) .pipe($.header(banner, { pkg : pkg })) .pipe(gulp.dest(config.dist)); }); /** * Minify ./dist/angular-chose.js into ./dist/angular-chose.min.js */ gulp.task('build-minify', ['build-coffee-script'], function() { return gulp.src(config.dist + '/angular-chosen.js') .pipe($.if(args.debug, $.debug())) .pipe($.plumber()) .pipe($.uglify({mangle: true})) .pipe($.rename(config.file + '.min.js')) .pipe($.header(banner, {pkg : pkg})) .pipe(gulp.dest(config.dist)); }); /** * Run Clean Javascripts, than minify(coffee-script) */ gulp.task('build', ['build-clean-javascripts'], function() { gulp.start('build-minify'); }); /** * Clean Javascripts from .dist/ */ gulp.task('build-clean-javascripts', function() { return del(config.dist); }); /** * Watch files and compile Coffee Script in real-time */ gulp.task('watcher', ['tests'], function() { gulp.watch([config.src + '*.coffee', config.dist + '*.js'], ['tests']); }); gulp.task('test', ['build'], function (done) { new karma.Server({ configFile: __dirname + '/test/support/karma.conf.js', singleRun: true }, done).start(); }); gulp.task('tests', ['build'], function (done) { new karma.Server({ configFile: __dirname + '/test/support/karma.conf.js', singleRun: false }, done).start(); }); ================================================ FILE: package.json ================================================ { "name": "angular-chosen-localytics", "filename": "chosen.js", "main": "dist/angular-chosen.js", "version": "1.9.2", "description": "Angular Chosen directive is an AngularJS Directive that brings the Chosen jQuery in a Angular way", "homepage": "http://github.com/leocaseiro/angular-chosen", "repository": { "type": "git", "url": "git://github.com/leocaseiro/angular-chosen" }, "keywords": [ "angularjs", "select", "multiselect", "dropdown", "form", "input", "ui" ], "license": "MIT", "author": "jr314159", "contributors": [ "abuggia", "abyx", "charandas", "dariusriggins", "darlanalves", "dougludlow", "failpunk", "frnan", "gaui", "iamnewspecies", "ilychkov", "kfarst", "leocaseiro", "lpsBetty", "lukeMason", "nike-17", "odi55555", "paulpflug", "simison", "slobo", "stefanvermaas", "vantanev", "vstene", "zlodes" ], "dependencies": { "angular": "^1.5.7", "chosen-js": "^1.6.1", "jquery": "^3.0.0" }, "bugs": { "url": "https://github.com/leocaseiro/angular-chosen/issues" }, "devDependencies": { "angular-mocks": "^1.4.9", "del": "^2.2.0", "gulp": "^3.9.1", "gulp-coffee": "^2.3.1", "gulp-coffeelint": "^0.6.0", "gulp-debug": "^2.1.2", "gulp-header": "^1.7.1", "gulp-if": "^2.0.0", "gulp-load-plugins": "^1.2.0", "gulp-plumber": "^1.1.0", "gulp-rename": "^1.2.2", "gulp-task-listing": "^1.0.1", "gulp-uglify": "^1.5.3", "gulp-util": "^3.0.7", "jasmine-core": "^2.4.1", "karma": "^0.13.22", "karma-coffee-preprocessor": "^0.3.0", "karma-jasmine": "^0.3.7", "karma-jasmine-matchers": "^2.0.2", "karma-mocha-reporter": "^2.0.0", "karma-phantomjs-launcher": "^1.0.0", "phantomjs-prebuilt": "^2.1.5", "yargs": "^4.2.0" }, "scripts": { "test": "gulp test" } } ================================================ FILE: src/.editorconfig ================================================ root = true [*] charset = utf-8 end_of_line = lf insert_final_newline = true indent_style = space indent_size = 2 trim_trailing_whitespace = false ================================================ FILE: src/chosen.coffee ================================================ angular.module('localytics.directives', []) chosenModule = angular.module('localytics.directives') chosenModule.provider 'chosen', -> options = {} { setOption: (newOpts) -> angular.extend options, newOpts return $get: -> options } 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]+?))?$/ # coffeelint: enable=max_line_length # Whitelist of options that will be parsed from the element's attributes and passed into Chosen # # Can be updated by running the following script on the options page # https://harvesthq.github.io/chosen/options.html # # Array.from( # document.querySelectorAll('table:first-of-type tr td:first-of-type') # ) # .map(node => node.textContent) # .map(option => option.replace(/_(\w)/g, (_, letter) => letter.toUpperCase())) # CHOSEN_OPTION_WHITELIST = [ 'allowSingleDeselect' 'disableSearch' 'disableSearchThreshold' 'enableSplitWordSearch' 'inheritSelectClasses' 'maxSelectedOptions' 'noResultsText' 'placeholderTextMultiple' 'placeholderTextSingle' 'searchContains' 'groupSearch' 'singleBackstrokeDelete' 'width' 'displayDisabledOptions' 'displaySelectedOptions' 'includeGroupLabelInSelected' 'maxShownResults' 'caseSensitiveSearch' 'hideResultsOnSelect' 'rtl' ] snakeCase = (input) -> input.replace /[A-Z]/g, ($1) -> "_#{$1.toLowerCase()}" isEmpty = (value) -> if angular.isArray(value) return value.length is 0 else if angular.isObject(value) return false for key of value when value.hasOwnProperty(key) true restrict: 'A' require: ['select', '?ngModel'] priority: 1 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 {} # Clone options from configProvider options = angular.copy(config) # Merge options from directive with options from configProvider angular.extend(options, directiveOptions) # Options defined as attributes take precedence angular.forEach attr, (value, key) -> if key in CHOSEN_OPTION_WHITELIST # Observe attributes # Update the value in options. Set the default texts again. Update message. attr.$observe key, (value) -> prefix = String(element.attr(attr.$attr[key])).slice(0, 2) options[snakeCase(key)] = if prefix is '{{' then value else scope.$eval(value) disableIfEmpty() startLoading = -> element.addClass('loading').attr('disabled', true).trigger('chosen:updated') stopLoading = -> element.removeClass('loading') if angular.isDefined attr.disabled element.attr 'disabled', attr.disabled else element.attr 'disabled', false element.trigger('chosen:updated') chosen = null empty = false # async initialize chosen if it's not initialized yet initIfNotInitialized = -> if !chosen scope.$evalAsync -> if !chosen then chosen = element.chosen(options).data('chosen') # Use Chosen's placeholder or no results found text depending on whether there are options available disableIfEmpty = -> if chosen && empty element.attr('disabled', true) element.trigger('chosen:updated') # Watch the underlying ngModel for updates and trigger an update when they occur. if ngModel $render = ngModel.$render ngModel.$render = -> initIfNotInitialized() # 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() isPrimitive = !trackBy && !attr.multiple valueChanged = if isPrimitive then previousValue != nextValue else !angular.equals(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.$applyAsync -> ngModel.$setTouched() # This is basically taken from angular ngOptions source. ngModel watches reference, not value, # so when values are added or removed from array ngModels, $render won't be fired. if attr.multiple viewWatch = -> ngModel.$viewValue scope.$watch viewWatch, ngModel.$render, true else initIfNotInitialized() # Watch the disabled attribute (could be set by ngDisabled) attr.$observe 'disabled', -> element.trigger('chosen:updated') # 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 timer = null scope.$watchCollection valuesExpr, (newVal, oldVal) -> # Defer execution until DOM is loaded timer = $timeout(-> if angular.isUndefined(newVal) startLoading() else empty = isEmpty(newVal) stopLoading() disableIfEmpty() ) scope.$on '$destroy', (event) -> $timeout.cancel timer if timer? ] ================================================ FILE: src/coffeelint.json ================================================ { "coffeescript_error": { "level": "error" }, "arrow_spacing": { "name": "arrow_spacing", "level": "warn" }, "no_tabs": { "name": "no_tabs", "level": "error" }, "no_trailing_whitespace": { "name": "no_trailing_whitespace", "level": "warn", "allowed_in_comments": false, "allowed_in_empty_lines": false }, "max_line_length": { "name": "max_line_length", "value": 120, "level": "error", "limitComments": true }, "line_endings": { "name": "line_endings", "level": "ignore", "value": "unix" }, "no_trailing_semicolons": { "name": "no_trailing_semicolons", "level": "error" }, "indentation": { "name": "indentation", "value": 2, "level": "error" }, "camel_case_classes": { "name": "camel_case_classes", "level": "error" }, "colon_assignment_spacing": { "name": "colon_assignment_spacing", "level": "warn", "spacing": { "left": 0, "right": 1 } }, "no_implicit_braces": { "name": "no_implicit_braces", "level": "ignore", "strict": true }, "no_plusplus": { "name": "no_plusplus", "level": "ignore" }, "no_throwing_strings": { "name": "no_throwing_strings", "level": "error" }, "no_backticks": { "name": "no_backticks", "level": "error" }, "no_implicit_parens": { "name": "no_implicit_parens", "level": "ignore" }, "no_empty_param_list": { "name": "no_empty_param_list", "level": "warn" }, "no_stand_alone_at": { "name": "no_stand_alone_at", "level": "ignore" }, "space_operators": { "name": "space_operators", "level": "warn" }, "duplicate_key": { "name": "duplicate_key", "level": "error" }, "empty_constructor_needs_parens": { "name": "empty_constructor_needs_parens", "level": "ignore" }, "cyclomatic_complexity": { "name": "cyclomatic_complexity", "value": 10, "level": "ignore" }, "newlines_after_classes": { "name": "newlines_after_classes", "value": 3, "level": "ignore" }, "no_unnecessary_fat_arrows": { "name": "no_unnecessary_fat_arrows", "level": "warn" }, "missing_fat_arrows": { "name": "missing_fat_arrows", "level": "ignore" }, "non_empty_constructor_needs_parens": { "name": "non_empty_constructor_needs_parens", "level": "ignore" } } ================================================ FILE: test/base.spec.js ================================================ describe('base functionality', function() { var element; it('should work without ngModel', function() { element = $compile('')($scope); $scope.$apply(); var chosenSelected = element.next().find('.chosen-single span'); expect(chosenSelected.length).toBe(1) }) beforeEach(function() { $scope.currentLanguage = 'german'; $scope.languages = ['german', 'english']; // Compile a piece of HTML containing the directive element = $compile('')($scope); $scope.$apply(); $timeout.flush(); element.trigger('chosen:open.chosen'); // fills dropdown (triggers chosen:showing_dropdown when finished) element.trigger('chosen:close.chosen'); }); it('should add chosen dropdown', function() { var chosenContainer = element.next(); var chosenSelected = chosenContainer.find('.chosen-single span'); var chosenList = chosenContainer.find('.chosen-drop ul li'); expect(chosenSelected.text()).toBe($scope.currentLanguage); expect(chosenList.length).toBe(2); expect(chosenList.first().text()).toBe($scope.languages[0]); expect(chosenList.last().text()).toBe($scope.languages[1]); }); it('should work when current model updates', function() { var chosenContainer = element.next(); var chosenSelected = chosenContainer.find('.chosen-single span'); expect(chosenSelected.text()).toBe('german'); $scope.currentLanguage = 'english'; $scope.$apply(); expect(chosenSelected.text()).toBe('english'); }); it('should trigger ngChange function when selecting a chosen result', function() { $scope.changed = false; // Compile a piece of HTML containing the directive element = $compile('')($scope); $scope.$apply(); $timeout.flush(); element.trigger('chosen:open.chosen'); var chosenContainer = element.next(); var chosenSelected = chosenContainer.find('.chosen-single span'); var chosenList = chosenContainer.find('.chosen-drop ul li'); expect(chosenSelected.text()).toBe('german'); chosenList.last().trigger('mouseup'); // select english = last item expect(chosenSelected.text()).toBe('english'); expect($scope.changed).toBe(true); }); it('should disable chosen with ngDisabled', function() { $scope.disabled = true; // Compile a piece of HTML containing the directive element = $compile('')($scope); $scope.$apply(); $timeout.flush(); var chosenContainer = element.next(); expect(chosenContainer.hasClass('chosen-disabled')).toBe(true); $scope.disabled = false; $scope.$apply(); expect(chosenContainer.hasClass('chosen-disabled')).toBe(false); }); }); ================================================ FILE: test/chosenAttrSpec.js ================================================ describe('chosen attributes', function() { var element; it('should inherit class customclass from select', function() { var customClass = 'customclass'; var chosenContainer; var select = function(inheritSelectClasses, customClass) { return ''; }; //inherit-select-classes = true element = $compile(select(true, customClass))($scope); $scope.$apply(); chosenContainer = element.next(); expect(chosenContainer.hasClass(customClass)).toBe(true); //inherit-select-classes = false element = $compile(select(false, customClass))($scope); $scope.$apply(); chosenContainer = element.next(); expect(chosenContainer.hasClass(customClass)).toBe(false); }); }); ================================================ FILE: test/chosenOptions/allowSingleDeselected.spec.js ================================================ describe('Chosen options: allow-single-deselect', function () { beforeEach(function () { chosenSelectHelper.compileWithAttributes({ allowSingleDeselect: true }); }); it('removes the model value when deselected', function () { var removeButton = chosenSelectHelper.chosenContainer().find('abbr.search-choice-close'); removeButton.trigger('mouseup.chosen'); expect($scope.currentLanguage).toBeNull(); }); }); ================================================ FILE: test/chosenOptions/disableSearch.spec.js ================================================ describe('Chosen options: disable-search', function () { beforeEach(function () { chosenSelectHelper.compileWithAttributes({ disableSearch: true }); }); it('hides the search input', function () { expect(chosenSelectHelper.searchInput().attr('readOnly')).toBeTruthy(); }); }); ================================================ FILE: test/chosenOptions/disableSearchThreshold.spec.js ================================================ describe('Chosen options: disable-search-threshold', function () { it('disables the search when the results are lower than the threshold', function () { chosenSelectHelper.compileWithAttributes({ disableSearchThreshold: 3 }); expect(chosenSelectHelper.searchInput().attr('readOnly')).toBeTruthy(); }); it('does not disable the search when the results are higher than the threshold', function () { chosenSelectHelper.compileWithAttributes({ disableSearchThreshold: 1 }); expect(chosenSelectHelper.searchInput().attr('readOnly')).toBeFalsy(); }); }); ================================================ FILE: test/chosenOptions/inheritSelectClasses.spec.js ================================================ describe('Chosen options: inherit-select-classes', function () { it('passes classes from the select tag to the chosen container', function () { chosenSelectHelper.compileWithAttributes({ inheritSelectClasses: true, 'class': 'inherited-class' }); expect(chosenSelectHelper.chosenContainer().hasClass('inherited-class')).toBeTruthy(); }); it('dont pass classes from the select tag to the chosen container', function () { chosenSelectHelper.compileWithAttributes({ inheritSelectClasses: false, 'class': 'inherited-class' }); expect(chosenSelectHelper.chosenContainer().hasClass('inherited-class')).toBeFalsy(); }); }); ================================================ FILE: test/chosenOptions/maxShownResults.spec.js ================================================ describe('Chosen options: max-shown-results', function () { beforeEach(function () { chosenSelectHelper.compileWithAttributes({ maxShownResults: 1 }); chosenSelectHelper.selectTag.trigger('chosen:open.chosen'); chosenSelectHelper.chosenResults().last().trigger('mouseup.chosen'); }); it('displays only the maximum number of results specified', function () { expect(chosenSelectHelper.chosenResults().length).toBe(1); }); }); ================================================ FILE: test/chosenOptions/noResultsText.spec.js ================================================ describe('Chosen options: no-results-text', function () { beforeEach(function () { chosenSelectHelper.compileWithAttributes({ noResultsText: "'testing no results text'" }); chosenSelectHelper.searchInput().val('foo'); chosenSelectHelper.searchInput().trigger('keyup.chosen'); }); it('displays the specified message when a search returns no results', function () { expect(chosenSelectHelper.chosenResults().html()).toMatch('testing no results text'); }); }); ================================================ FILE: test/chosenOptions/placeholderTextMultiple.spec.js ================================================ describe('Chosen options: placeholder-text-multiple', function () { beforeEach(function () { $scope.emptyModel = []; chosenSelectHelper.compileWithAttributes({ multiple: true, placeholderTextMultiple: "'testing multi placeholder text'", ngModel: 'emptyModel' }); }); it('shows the specified placeholder text for a multi-select chosen tag', function () { expect(chosenSelectHelper.chosenContainer().find('.search-field input').attr('value')).toMatch('testing multi placeholder text'); }); }); ================================================ FILE: test/chosenOptions/placeholderTextSingle.spec.js ================================================ describe('Chosen options: placeholder-text-single', function () { beforeEach(function () { chosenSelectHelper.compileWithAttributes({ placeholderTextSingle: "'testing single placeholder text'", ngModel: 'emptyModel' }); }); it('shows the specified placeholder text for a single-select chosen tag', function () { expect(chosenSelectHelper.chosenContainer().find('.chosen-default span').text()).toMatch('testing single placeholder text'); }); }); ================================================ FILE: test/chosenOptions/searchContains.spec.js ================================================ describe('Chosen options: search-contains', function () { beforeEach(function () { chosenSelectHelper.compileWithAttributes({ searchContains: true }); chosenSelectHelper.searchInput().val('erma'); chosenSelectHelper.searchInput().trigger('keyup.chosen'); }); it('finds a match in the search result that is not at the beginning of the option text', function () { expect(chosenSelectHelper.chosenResults().length).toBeTruthy(); }); }); ================================================ FILE: test/chosenOptions/width.spec.js ================================================ describe('Chosen options: width', function () { beforeEach(function () { chosenSelectHelper.compileWithAttributes({ width: 250 }); }); it('sets the dropdown width', function () { expect(chosenSelectHelper.chosenContainer().attr('style')).toContain('width: 250'); }); }); ================================================ FILE: test/form.spec.js ================================================ describe('form validations', function() { var element, ngModel; beforeEach(function() { $scope.currentLanguage = null; $scope.languages = ['german', 'english']; // Compile a piece of HTML containing the directive var form = $compile('
')($scope); element = form.find('select'); ngModel = $scope.form.language; $scope.$apply(); }); it('should work with required form validation', function() { expect(ngModel.$valid).toBe(false); expect(ngModel.$error.required).toBe(true); $scope.currentLanguage = 'german'; $scope.$apply(); expect(ngModel.$valid).toBe(true); }); it('should set $touched of ngModel to true (for e.g. form validation)', function() { expect(ngModel.$touched).toBe(false); element.trigger('chosen:open.chosen'); $scope.$apply(); element.trigger('chosen:hiding_dropdown'); $scope.$apply(); expect(ngModel.$touched).toBe(true); }); }); ================================================ FILE: test/issues/179-ng-if-breaks-inherit-select-classes.spec.js ================================================ describe('#179 inherit-select-classes inside ng-if', function () { it('passes classes from the select tag to the chosen container inside ng-if', function () { var element; var customClass = 'customclass'; var chosenContainer; $scope.test = true; var select = function(inheritSelectClasses, customClass) { return '
{{test}}
'; }; //inherit-select-classes = true element = $compile(select(true, customClass))($scope); $scope.$apply(); chosenContainer = element.find('select').next(); expect(chosenContainer.hasClass(customClass)).toBe(true); }); }); ================================================ FILE: test/support/caseConvertFilter.js ================================================ angular.module('caseConvertFilter', []) .filter('caseConvert', function () { var SNAKE_CASE_REGEXP = /[A-Z]/g; return function (name, separator) { var separator = separator || '_'; return name.replace(SNAKE_CASE_REGEXP, function (letter, pos) { return (pos ? separator : '') + letter.toLowerCase(); }); }; }); ================================================ FILE: test/support/chosenSelectHelper.js ================================================ angular.module('chosenSelectHelper', []) .service('chosenSelectHelper', ['$filter', function ($filter) { return { compileWithAttributes: function (attributes) { var attributes = attributes || {}; if (!attributes['ngModel']) { $scope.currentLanguage = 'german'; attributes['ngModel'] = 'currentLanguage'; } if (!attributes['ngOptions']) { $scope.languages = ['german', 'english']; attributes['ngOptions'] = 'lang for lang in languages'; } for (attrName in attributes) { this.selectTag.attr($filter('caseConvert')(attrName, '-'), attributes[attrName]); } $compile(this.selectTag)($scope); $scope.$apply(); $timeout.flush(); }, selectTag: angular.element(''), chosenContainer: function () { return this.selectTag.next(); }, chosenResults: function () { return this.chosenContainer().find('.chosen-results li'); }, searchInput: function () { return this.chosenContainer().find('.chosen-search input'); } }; }]); ================================================ FILE: test/support/karma.conf.js ================================================ 'use strict'; module.exports = function(config) { config.set({ basePath: '../../', frameworks: [ 'jasmine', 'jasmine-matchers' ], files: [ 'node_modules/jquery/dist/jquery.js', 'node_modules/chosen-js/chosen.jquery.js', 'node_modules/angular/angular.js', 'node_modules/angular-mocks/angular-mocks.js', 'src/**/*.coffee', 'test/support/*.js', 'test/**/*.spec.js' ], preprocessors: { 'src/**/*.coffee': ['coffee'] }, reporters: ['mocha'], // Start these browsers, currently available: // - Chrome // - ChromeCanary // - Firefox // - Opera // - Safari (only Mac) // - PhantomJS // - IE (only Windows) browsers: [ // 'PhantomJS', 'Chrome', 'Firefox', 'Safari' 'PhantomJS' ], // Which plugins to enable plugins: [ 'karma-mocha-reporter', 'karma-phantomjs-launcher', // 'karma-chrome-launcher', // 'karma-firefox-launcher', // 'karma-safari-launcher', 'karma-jasmine', 'karma-jasmine-matchers', 'karma-coffee-preprocessor' ], singleRun: true }); }; ================================================ FILE: test/support/specHelper.js ================================================ beforeEach(function() { angular.module('testApp', [ 'localytics.directives', 'caseConvertFilter', 'chosenSelectHelper' ]); angular.mock.module('testApp'); }); var $scope; var $timeout; var $compile; var chosenSelectHelper; /** * Assigns $scope, $timeout and $compile, these will be used in every test. */ beforeEach(inject(function(_$rootScope_, _$timeout_, _$compile_, _chosenSelectHelper_) { $scope = _$rootScope_.$new(); $timeout = _$timeout_; $compile = _$compile_; chosenSelectHelper = _chosenSelectHelper_; }));