diff --git a/.bowerrc b/.bowerrc new file mode 100644 index 0000000..ebcc9f7 --- /dev/null +++ b/.bowerrc @@ -0,0 +1,3 @@ +{ + "directory": "src/lib/" +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..beffa30 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..36b905b --- /dev/null +++ b/.eslintrc @@ -0,0 +1,11 @@ +{ + "env": { + "es6": true, + "browser": true + }, + "extends": ["eslint:recommended", "angular"], + "rules": { + "no-console": ["error", { allow: ["warn", "error"] }], + "angular/di": [2, "array"] + } +} diff --git a/.gitignore b/.gitignore index e550e4f..3996ab9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,29 +1,3 @@ -# Packages # -############ -# Ignore if there is any compressed files -*.7z -*.dmg -*.gz -*.iso -*.jar -*.rar -*.tar -*.zip - -# Unwanted OS/Editors generated file # -###################################### -.DS_Store -.DS_Store? -._* -.Spotlight-V100 -.Trashes -ehthumbs.db -Thumbs.db -*~ -*.swp -*.bak - -# Node Modules Directory # -########################## -/node_modules/* - +# Dependency directories +node_modules +src/lib diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 5c86cdc..0000000 --- a/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -sudo: required -before_script: - - npm install -g gulp - - npm install gulp - - npm install gulp-clean - - npm install gulp-watch - - npm install run-sequence - - npm install gulp-ngmin - - npm install gulp-order - - npm install gulp-jshint - - npm install gulp-lintspaces - - npm install jshint-summary -script: gulp diff --git a/bower.json b/bower.json index a92e7cd..0fcf609 100644 --- a/bower.json +++ b/bower.json @@ -1,7 +1,7 @@ { - "name": "amitava82-angular-multiselect", - "version": "1.2.0", - "main": "dist/multiselect-tpls.js", + "name": "nithin-angular-multiselect", + "version": "1.3.13", + "main": ["dist/js/multiselect.min.js","dist/js/templates.min.js","dist/css/multiselect.min.css"], "description": "Native AngularJS multiselect directive", "license": "MIT", "ignore": [ @@ -9,7 +9,16 @@ "**/*.txt" ], "dependencies": { - "angular": ">=1.0.4" + "angular": "^1.6.4", + "bootstrap": "^3.3.7" + }, + "overrides": { + "bootstrap": { + "main": [ + "dist/css/bootstrap.min.css", + "dist/fonts/*" + ] + } }, "keywords": [ "angularjs", diff --git a/dist/app.js b/dist/app.js deleted file mode 100644 index 7f14db4..0000000 --- a/dist/app.js +++ /dev/null @@ -1,17 +0,0 @@ -angular.module('app', ['am.multiselect']) - -.controller('appCtrl', ['$scope', function($scope){ - $scope.cars = [ - {id:1, name: 'Audi'}, - {id:2, name: 'BMW'}, - {id:3, name: 'Honda'} - ]; - $scope.selectedCar = []; - - $scope.fruits = [ - {id: 1, name: 'Apple'}, - {id: 2, name: 'Orange'}, - {id: 3, name: 'Banana'} - ]; - $scope.selectedFruit = null; -}]); diff --git a/dist/css/multiselect.css b/dist/css/multiselect.css new file mode 100644 index 0000000..4890e90 --- /dev/null +++ b/dist/css/multiselect.css @@ -0,0 +1,30 @@ +/*csslint box-sizing: false*/ + +am-multiselect .dropdown-menu{ + box-sizing: content-box; + height: 200px; + overflow-x: scroll; + overflow-y: scroll; + padding-left: 5px; + padding-right: 5px; +} + +am-multiselect .dropdown-menu > li > a { + cursor:pointer; + padding: 3px 10px; +} + +am-multiselect .dropdown-menu > .selected { + background-color: #ADD8E6; +} + +am-multiselect .btn-group > button { + border-radius:5px !important; /* csslint allow: known-properties, important */ + overflow:hidden; + white-space: normal; + width:100%; +} + +am-multiselect .btn-group { + width:100%; +} diff --git a/dist/css/multiselect.min.css b/dist/css/multiselect.min.css new file mode 100644 index 0000000..3bca72e --- /dev/null +++ b/dist/css/multiselect.min.css @@ -0,0 +1 @@ +am-multiselect .dropdown-menu{box-sizing:content-box;height:200px;overflow-x:scroll;overflow-y:scroll;padding-left:5px;padding-right:5px}am-multiselect .dropdown-menu>li>a{cursor:pointer;padding:3px 10px}am-multiselect .dropdown-menu>.selected{background-color:#add8e6}am-multiselect .btn-group>button{border-radius:5px!important;overflow:hidden;white-space:normal;width:100%}am-multiselect .btn-group{width:100%} \ No newline at end of file diff --git a/dist/html/multiselect.tmpl.html b/dist/html/multiselect.tmpl.html new file mode 100644 index 0000000..49cbf17 --- /dev/null +++ b/dist/html/multiselect.tmpl.html @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/dist/index.html b/dist/index.html deleted file mode 100644 index b32f42e..0000000 --- a/dist/index.html +++ /dev/null @@ -1,28 +0,0 @@ - - - Angular multiselect - - - - - - - -

Example

- -
- {{selectedCar}} -
- Single select: - -
- {{selectedFruit}} -
- - diff --git a/dist/multiselect-tpls.js b/dist/js/multiselect.js similarity index 82% rename from dist/multiselect-tpls.js rename to dist/js/multiselect.js index 8b8179a..2622f7c 100644 --- a/dist/multiselect-tpls.js +++ b/dist/js/multiselect.js @@ -1,4 +1,5 @@ // Source: https://github.com/amitava82/angular-multiselect + angular.module('am.multiselect', []) // from bootstrap-ui typeahead parser @@ -39,6 +40,7 @@ angular.module('am.multiselect', []) var exp = attrs.options, parsedResult = optionParser.parse(exp), isMultiple = attrs.multiple ? true : false, + isHover = attrs.hover ? true : false, required = false, scope = originalScope.$new(), changeHandler = attrs.change || angular.noop; @@ -47,7 +49,11 @@ angular.module('am.multiselect', []) scope.header = 'Select'; scope.multiple = isMultiple; scope.disabled = false; + scope.searchDisable = false; scope.onBlur = attrs.ngBlur || angular.noop; + scope.hoverText = isHover ? scope.header : ''; + scope.onFocus = attrs.ngFocus; + scope.clazz = attrs.clazz; originalScope.$on('$destroy', function () { scope.$destroy(); @@ -67,11 +73,18 @@ angular.module('am.multiselect', []) // watch disabled state scope.$watch(function () { - return $parse(attrs.disabled)(originalScope); + return $parse(attrs.ngDisabled)(originalScope); }, function (newVal) { scope.disabled = newVal; }); + // watch disabled state for search text box + scope.$watch(function () { + return $parse(attrs.searchDisable)(originalScope); + }, function (newVal) { + scope.searchDisable = newVal; + }); + // watch single/multiple state for dynamically change single to multiple scope.$watch(function () { return $parse(attrs.multiple)(originalScope); @@ -90,22 +103,29 @@ angular.module('am.multiselect', []) // watch model change scope.$watch(function () { return modelCtrl.$modelValue; - }, function (newVal, oldVal) { + }, function (newVal) { + // When the model is assigned a "" or undefined value from controller, need to uncheck all items and clear searchText.label + if(angular.isUndefined(newVal) || newVal==="" || newVal===null) { + scope.uncheckAll(); + if(angular.isDefined(scope.searchText)) + scope.searchText.label=""; + } // when directive initialize, newVal usually undefined. Also, if model value already set in the controller // for preselected list then we need to mark checked in our scope item. But we don't want to do this every time // model changes. We need to do this only if it is done outside directive scope, from controller, for example. - if (angular.isDefined(newVal)) { + else if (angular.isDefined(newVal)) { markChecked(newVal); scope.$eval(changeHandler); } getHeaderText(); + scope.hoverText = isHover ? scope.header : ''; modelCtrl.$setValidity('required', scope.valid()); }, true); function parseModel() { scope.items.length = 0; var model = parsedResult.source(originalScope); - if(!angular.isDefined(model)) return; + if(angular.isUndefined(model)) return; for (var i = 0; i < model.length; i++) { var local = {}; local[parsedResult.itemName] = model[i]; @@ -121,8 +141,16 @@ angular.module('am.multiselect', []) element.append($compile(popUpEl)(scope)); + function getItemLabel(items,model) { + for(var i = 0; i < items.length; i++) { + if(items[i].model==model) { + return items[i].label; + } + } + } + function getHeaderText() { - if (is_empty(modelCtrl.$modelValue)) return scope.header = (attrs.msHeader!==undefined ? attrs.msHeader : 'Select'); + if (is_empty(modelCtrl.$modelValue)) return scope.header = (angular.isDefined(attrs.msHeader) ? attrs.msHeader : 'Select'); if (isMultiple) { if (attrs.msSelected) { @@ -140,11 +168,11 @@ angular.module('am.multiselect', []) } } else { if(angular.isString(modelCtrl.$modelValue)){ - scope.header = modelCtrl.$modelValue; + scope.header = getItemLabel(scope.items,modelCtrl.$modelValue); } else { var local = {}; local[parsedResult.itemName] = modelCtrl.$modelValue; - scope.header = parsedResult.viewMapper(local) || scope.items[modelCtrl.$modelValue].label; + scope.header = parsedResult.viewMapper(local) || getItemLabel(scope.items,modelCtrl.$modelValue); } } } @@ -154,7 +182,7 @@ angular.module('am.multiselect', []) if (obj && obj.length && obj.length > 0) return false; for (var prop in obj) if (obj[prop]) return false; return true; - }; + } scope.valid = function validModel() { if(!required) return true; @@ -178,13 +206,14 @@ angular.module('am.multiselect', []) } function setModelValue(isMultiple) { - var value = null; + var value = undefined; if (isMultiple) { value = []; angular.forEach(scope.items, function (item) { if (item.checked) value.push(item.model); }) + if(value.length==0) value=undefined; } else { angular.forEach(scope.items, function (item) { if (item.checked) { @@ -228,11 +257,13 @@ angular.module('am.multiselect', []) }; scope.uncheckAll = function () { - var items = (scope.searchText && scope.searchText.label.length > 0) ? $filter('filter')(scope.items, scope.searchText) : scope.items; - angular.forEach(items, function (item) { + // need to uncheck from the entire list of items. If user filers with ine text and selects item A. Next time user fileters and selects item B (item A now not in the filtered set). The item A will not get unchecked + // var items = (scope.searchText && scope.searchText.label.length > 0) ? $filter('filter')(scope.items, scope.searchText) : scope.items; + angular.forEach(scope.items, function (item) { item.checked = false; }); - setModelValue(true); + // sending scope.multiple instead of true to setModelValue. Since different values geeting set when single and multiple. + setModelValue(scope.multiple); }; scope.select = function (item) { @@ -252,15 +283,18 @@ angular.module('am.multiselect', []) restrict: 'E', scope: false, replace: true, - templateUrl: function (element, attr) { - return attr.templateUrl || 'multiselect.tmpl.html'; - }, - link: function (scope, element, attrs) { + templateUrl: 'html/multiselect.tmpl.html', + link: function (scope, element) { scope.selectedIndex = null; scope.isVisible = false; scope.filteredItems = null; + scope.ngClazz = { + 'error': !scope.valid() + }; + scope.ngClazz[scope.$eval(scope.clazz)] = true; + scope.toggleSelect = function () { if (element.hasClass('open')) { element.removeClass('open'); @@ -273,6 +307,10 @@ angular.module('am.multiselect', []) } }; + scope.evalFocus = function() { + scope.$parent.$eval(scope.onFocus); + } + function clickHandler(event) { if (elementMatchesAnyInArray(event.target, element.find(event.target.tagName))) { scope.$parent.$eval(scope.onBlur); @@ -322,5 +360,3 @@ angular.module('am.multiselect', []) } } }]); - -angular.module("am.multiselect").run(["$templateCache", function($templateCache) {$templateCache.put("multiselect.tmpl.html","
\n \n \n
\n");}]); \ No newline at end of file diff --git a/dist/js/multiselect.min.js b/dist/js/multiselect.min.js new file mode 100644 index 0000000..fc578be --- /dev/null +++ b/dist/js/multiselect.min.js @@ -0,0 +1 @@ +angular.module("am.multiselect",[]).factory("optionParser",["$parse",function(e){return{parse:function(n){var l=n.match(/^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+([\s\S]+?)$/);if(!l)throw new Error('Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_" but got "'+n+'".');return{itemName:l[3],source:e(l[4]),viewMapper:e(l[2]||l[1]),modelMapper:e(l[1])}}}}]).directive("amMultiselect",["$parse","$document","$compile","$interpolate","$filter","optionParser",function(e,n,l,t,r,a){return{restrict:"E",require:"ngModel",link:function(n,i,c,u){function o(){V.items.length=0;var e=k.source(n);if(!angular.isUndefined(e))for(var l=0;l0)return!1;for(var n in e)if(e[n])return!1;return!0}function m(e){e.checked?V.uncheckAll():(V.uncheckAll(),e.checked=!e.checked),p(!1)}function h(e){e.checked=!e.checked,p(!0)}function p(e){var n=void 0;e?(n=[],angular.forEach(V.items,function(e){e.checked&&n.push(e.model)}),0==n.length&&(n=void 0)):angular.forEach(V.items,function(e){if(e.checked)return n=e.model,!1}),u.$setViewValue(n)}function g(e){angular.isArray(e)?angular.forEach(V.items,function(n){n.checked=!1,angular.forEach(e,function(e){angular.equals(n.model,e)&&(n.checked=!0)})}):angular.forEach(V.items,function(n){if(angular.equals(n.model,e))return V.uncheckAll(),n.checked=!0,p(!1),!1})}var $=u.$isEmpty;u.$isEmpty=function(e){return $(e)||angular.isArray(e)&&0==e.length};var v=c.options,k=a.parse(v),x=!!c.multiple,b=!!c.hover,w=!1,V=n.$new(),y=c.change||angular.noop;V.items=[],V.header="Select",V.multiple=x,V.disabled=!1,V.searchDisable=!1,V.onBlur=c.ngBlur||angular.noop,V.hoverText=b?V.header:"",V.onFocus=c.ngFocus,V.clazz=c.clazz,n.$on("$destroy",function(){V.$destroy()});var E=angular.element("");(c.required||c.ngRequired)&&(w=!0),c.$observe("required",function(e){w=e}),V.$watch(function(){return e(c.ngDisabled)(n)},function(e){V.disabled=e}),V.$watch(function(){return e(c.searchDisable)(n)},function(e){V.searchDisable=e}),V.$watch(function(){return e(c.multiple)(n)},function(e){x=e||!1}),V.$watch(function(){return k.source(n)},function(e){angular.isDefined(e)&&o()},!0),V.$watch(function(){return u.$modelValue},function(e){angular.isUndefined(e)||""===e||null===e?(V.uncheckAll(),angular.isDefined(V.searchText)&&(V.searchText.label="")):angular.isDefined(e)&&(g(e),V.$eval(y)),d(),V.hoverText=b?V.header:"",u.$setValidity("required",V.valid())},!0),o(),i.append(l(E)(V)),V.valid=function(){if(!w)return!0;var e=u.$modelValue;return angular.isArray(e)&&e.length>0||!angular.isArray(e)&&null!=e},V.checkAll=function(){if(x){var e=V.searchText&&V.searchText.label.length>0?r("filter")(V.items,V.searchText):V.items;angular.forEach(e,function(e){e.checked=!0}),p(!0)}},V.uncheckAll=function(){angular.forEach(V.items,function(e){e.checked=!1}),p(V.multiple)},V.select=function(e){!1===x?(m(e),V.toggleSelect()):h(e)}}}}]).directive("amMultiselectPopup",["$document","$filter",function(e,n){return{restrict:"E",scope:!1,replace:!0,templateUrl:"html/multiselect.tmpl.html",link:function(l,t){function r(n){a(n.target,t.find(n.target.tagName))?l.$parent.$eval(l.onBlur):(t.removeClass("open"),e.unbind("click",r),l.$apply())}l.selectedIndex=null,l.isVisible=!1,l.filteredItems=null,l.ngClazz={error:!l.valid()},l.ngClazz[l.$eval(l.clazz)]=!0,l.toggleSelect=function(){t.hasClass("open")?(t.removeClass("open"),e.unbind("click",r),l.$parent.$eval(l.onBlur)):(t.addClass("open"),e.bind("click",r),l.focus())},l.evalFocus=function(){l.$parent.$eval(l.onFocus)},l.focus=function(){var e=t.find("input")[0];e&&e.focus()},l.keydown=function(e){var t=n("filter")(l.items,l.searchText),r=e.keyCode||e.which;13===r?t[l.selectedIndex]&&l.select(t[l.selectedIndex]):l.selectedIndex=38===r?null===l.selectedIndex?t.length-1:l.selectedIndex-1:40===r?null===l.selectedIndex?0:l.selectedIndex+1:null,l.selectedIndex<0?l.selectedIndex=t.length-1:l.selectedIndex>t.length-1&&(l.selectedIndex=0)};var a=function(e,n){for(var l=0;l');}]); \ No newline at end of file diff --git a/dist/js/templates.min.js b/dist/js/templates.min.js new file mode 100644 index 0000000..9b0d69c --- /dev/null +++ b/dist/js/templates.min.js @@ -0,0 +1 @@ +angular.module("am.multiselect").run(["$templateCache",function(l){l.put("html/multiselect.tmpl.html",'
')}]); \ No newline at end of file diff --git a/dist/multiselect.css b/dist/multiselect.css deleted file mode 100644 index 3b817ad..0000000 --- a/dist/multiselect.css +++ /dev/null @@ -1,13 +0,0 @@ -am-multiselect .dropdown-menu{ - padding-left: 5px; - padding-right: 5px; -} - -am-multiselect .dropdown-menu > li > a { - padding: 3px 10px; - cursor:pointer; -} - -am-multiselect .dropdown-menu > li.selected { - background-color: #ADD8E6; -} \ No newline at end of file diff --git a/gulp.config.js b/gulp.config.js new file mode 100644 index 0000000..12e27ce --- /dev/null +++ b/gulp.config.js @@ -0,0 +1,96 @@ +/* global module */ +/* global require */ + +module.exports = function() { + var appSrc='./src/'; + var distDir='./dist/'; + + var config={ + temp: './.tmp/', + +// distFiles: [appSrc+'js/inputDateField.js',appSrc+'html/inputDateField.tmpl.html'], + distJsFiles: appSrc+'js/multiselect.js', + distHtmlFiles: appSrc+'html/multiselect.tmpl.html', + distCssFiles: appSrc+'css/multiselect.css', + + // all js to vet + alljs: ['./*.js','./src/js/*.js'], + allcss: [appSrc+'*.css',appSrc+'css/*.css'], + allhtml: [appSrc+'*.html',appSrc+'html/*.html'], + index: './src/index.html', + js: [appSrc+'js/*.js'], + css: [appSrc+'*.css',appSrc+'css/*.css'], +// less: ['./src/*.less','./src/modules/less/*.less'], + + + bowerJSON: './bower.json', + bower: { + json: require('./bower.json'), + directory: './src/lib/', + ignorePath: '..' + }, + + optimized: { + lib: 'lib.js', + app: 'app.js' + }, + + htmlAngularValidate: { + customattrs: [''], + customtags: [''] + }, + + source: appSrc, + build: './gh-pages/', + dist: distDir, + resources: './src/resources/*', + images: './src/resources/images/*', + json: './src/resources/json/*.json', + + htmltemplates: [appSrc+'**/**/*.html',appSrc+'**/*.html','!'+appSrc+'index.html'], + templateCache: { + file: 'templates.js', + options: { + module: 'am.multiselect', + standAlone: true//, + //root: '/' + } + }, + + htmltemplatesDist: [distDir+'**/**/*.html',distDir+'**/*.html'], + templateCacheDist: { + file: 'templates.js', + options: { + module: 'am.multiselect', + standAlone: true//, + //root: '/' + } + } + + + }; + + config.getWiredepDefaultOptions = function() { + var options = { + bowerJson: config.bower.json, + directory: config.bower.directory, + ignorePath: config.bower.ignorePath + }; + return options; + }; + + config.getWebserverOptions = function() { + var options = { + livereload: true, + directoryListing: false, + open: true, + host: 'localhost', + port: 3000, + path: '/', + https: false + }; + return options; + } + + return config; +} diff --git a/gulpfile.js b/gulpfile.js index c554877..8397004 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,94 +1,338 @@ -/* - Licenced Under MIT - @file gulpfile.js - @author Nagaraja.T (github/naga2raja) - @desc Buildfile for angular-multiselect -*/ - -var gulp = require('gulp'), - del = require('del'), - watch = require('gulp-watch'), - sequence = require('run-sequence'), - mergeStream = require('merge-stream'), - concat = require('gulp-concat'), - lintspaces = require("gulp-lintspaces"), - templateCache = require("gulp-angular-templatecache"), - angularFilesort = require("gulp-angular-filesort"), - lintspacesConfig = { - indentation: 'spaces', - spaces: 4, - trailingspaces: true, - ignores: [ - 'js-comments' - ] - }; - -/* - Gulp monitors the src tree and rebuilds it detects a modification. Used of dev purpose -*/ -gulp.task('watch', function() { - watch("src/**/*.*", function() { - gulp.start("build-clean"); - }); -}); - -gulp.task('default', ['build-clean'], function() { -}); - -/* - Check for lintspaces errors in source code -*/ -gulp.task('whitespace', function() { - return gulp.src([ - 'src/*.js', - 'src/*.html', - 'src/*.css']) - .pipe(lintspaces(lintspacesConfig)) - .pipe(lintspaces.reporter()); -}); - -gulp.task('js', ['build-multiselect-tpls.js'], function () { - return gulp.src('src/*.js') - .pipe(gulp.dest('dist')); -}); - -gulp.task('build-multiselect-tpls.js', function () { - var templateJsStream = gulp - .src('src/multiselect.tmpl.html') - .pipe(templateCache({ module: 'am.multiselect' })); - - var combinedStream = mergeStream(templateJsStream, gulp.src('src/multiselect.js')); - - combinedStream - .pipe(angularFilesort()) - .pipe(concat('multiselect-tpls.js')) - .pipe(gulp.dest('dist')); - -}); - -gulp.task('css', function () { - return gulp.src('src/*.css') - .pipe(gulp.dest('dist')); -}); - -gulp.task('html', function () { - return gulp.src('src/*.html') - .pipe(gulp.dest('dist')); -}); - -gulp.task('clean', function(cb) { - return del(['dist']); -}); - -/* - Need to finish clean before building all the other stuff, and all other stuff - needs to complete before injecting build artifacts into index.html. -*/ -gulp.task('build-clean', function () { - console.log("----------------------"); - sequence('clean', - 'whitespace', - ['js','css'], - 'html' - ); +/* global require */ +/* eslint angular/typecheck-object: "off" */ +/* eslint angular/definedundefined: "off" */ + +const gulp = require('gulp'); +const args = require('yargs').argv; +var config = require('./gulp.config')(); +const del = require('del'); +var $ = require('gulp-load-plugins')({lazy: true}); +var serveStatic=require('serve-static'); +var parseurl = require('parseurl') + +gulp.task('help', $.taskListing); +gulp.task('default',['help']); + +gulp.task('lintjs', function () { + log('Analyzing JS files'); + return gulp.src(config.alljs) + .pipe($.if(args.verbose,$.print())) + .pipe($.eslint()) + .pipe($.eslint.format()) + .pipe($.eslint.failAfterError()); +}); + +gulp.task('lintcss', function () { + log('Analyzing CSS files'); + return gulp.src(config.allcss) + .pipe($.if(args.verbose,$.print())) + .pipe($.csslint()) + .pipe($.csslint.formatter()) + .pipe($.csslint.failFormatter()); +}); + +gulp.task('linthtml', function () { + log('Analyzing HTML files'); + var options = { + customattrs: config.htmlAngularValidate.customattrs, + customtags: config.htmlAngularValidate.customtags, + emitError: true, + reportpath: null, + reportCheckstylePath: null, + reportFn:function(fileFailures){ + for (var i = 0; i < fileFailures.length; i++) { + var fileResult = fileFailures[i]; + $.util.log(fileResult.filepath); + for (var j = 0; j < fileResult.errors.length; j++) { + var err = fileResult.errors[j]; + if (err.line !== undefined) { + $.util.log('[line' +err.line +', col: ' + err.col +'] ' +err.msg); + } else { + $.util.log(err.msg); + } + } + } + } + }; + return gulp.src(config.allhtml) + .pipe($.if(args.verbose,$.print())) + .pipe($.htmlAngularValidate(options)); +}); + +gulp.task('lintjson', function () { + log('Analyzing JSON files'); + return gulp.src(config.json) + .pipe($.if(args.verbose,$.print())) + .pipe($.jsonlint()) + .pipe($.jsonlint.reporter(myCustomReporter)) + .pipe($.jsonlint.failAfterError()); +}); + +gulp.task('wireindex',['lintjs','linthtml','lintcss','lintjson'] , function() { + var options = config.getWiredepDefaultOptions(); + var wiredep = require('wiredep').stream; + log('Linking all js/css files into index.html'); + return gulp + .src(config.index) + .pipe($.if(args.verbose,$.print())) + .pipe(wiredep(options)) + .pipe($.inject(gulp.src(config.js,{read:false}),{relative:true})) + .pipe($.inject(gulp.src(config.css,{read:false}),{relative:true})) + .pipe(gulp.dest(config.source)); +}); + +gulp.task('serve', function() { + log('Starting webserver for - '+($.util.env.production?'PROD':'DEV')); + var options = config.getWebserverOptions(); + gulp + .src($.util.env.production?config.build:config.source) + .pipe($.if(args.verbose,$.print())) + .pipe($.webserver(options)); +}); + +gulp.task('resources', function() { + log('copying resources'); + return gulp + .src(config.resources) + .pipe(gulp.dest(config.build+'resources')); +}); + +gulp.task('jsoncopy', function() { + log('copying and minifying json resources'); + return gulp + .src(config.json) + .pipe($.jsonminify()) + .pipe(gulp.dest(config.build+'resources/json')); +}); + +gulp.task('htmlcopy', ['fontscopy'], function() { + log('copying html files for making it ready for templatecache'); + return gulp + .src(config.htmltemplates) + .pipe($.flatten()) + .pipe(gulp.dest(config.temp+'html/')); +}); + +gulp.task('fontscopy', ['clean'], function() { + log('copying bootstrap and other fonts'); + var filterFonts = $.filter('**/*.{eot,svg,ttf,woff,woff2}', { restore: true }); + + return gulp + .src(config.bowerJSON) + .pipe($.mainBowerFiles()) + .pipe(filterFonts) + .pipe($.flatten()) + .pipe(gulp.dest(config.build+'fonts/')); +}); + +gulp.task('images', function() { + log('copying and compressing images'); + return gulp + .src(config.images) + .pipe($.imagemin({optimizationLevel:4})) + .pipe(gulp.dest(config.build+'resources/images')); +}); + +gulp.task('clean', function(done){ + var delconfig=[].concat(config.build,config.temp,config.dist); + log('cleaning '+$.util.colors.blue(delconfig)); + return del(delconfig,done); +}); + +gulp.task('templatecache',['fontscopy'],function(){ + log('creating AngularJS $templateCache'); + return gulp + .src(config.htmltemplates) + .pipe($.if(args.verbose,$.print())) + .pipe($.minifyHtml({empty:true})) + .pipe($.angularTemplatecache( + config.templateCache.file, + config.templateCache.options + )) + .pipe(gulp.dest(config.temp)); }); + +gulp.task('optimize',['wireindex','templatecache'],function() { + log('optimizing the js/html/css files'); + var assets = $.useref({searchPath: './src/'}); + var templateCache = config.temp+config.templateCache.file; + var cssFilter = $.filter('**/*.css',{restore:true}); + var jsLibFilter = $.filter('**/'+config.optimized.lib,{restore:true}); + var jsAppFilter = $.filter('**/'+config.optimized.app,{restore:true}); + return gulp + .src(config.index) + //.pipe($.plumber()) + .pipe($.inject(gulp.src(templateCache,{read:false}), { + starttag: '', + relative: true + })) + .pipe(assets) + .pipe(cssFilter) + .pipe($.csso()) + .pipe($.rev()) + .pipe(cssFilter.restore) + .pipe(jsLibFilter) + .pipe($.uglify()) + .pipe($.rev()) + .pipe(jsLibFilter.restore) + .pipe(jsAppFilter) + .pipe($.ngAnnotate({add:true})) + .pipe($.uglify()) + .pipe($.rev()) + .pipe(jsAppFilter.restore) + .pipe($.revReplace()) + .pipe(gulp.dest(config.build)) + .pipe($.rev.manifest()) + .pipe(gulp.dest(config.build)); +}); + +gulp.task('testTask', function() { + return gulp + .src(config.bowerJSON) + .pipe($.if(args.verbose,$.print())) + .pipe($.mainBowerFiles()) + .pipe(gulp.dest(config.temp)); +}); + +gulp.task('copyDistJS', ['clean'], function() { + var copyconfig=[].concat(config.distJsFiles); + log('copying distribution JS files'); + return gulp + .src(copyconfig) + .pipe(gulp.dest(config.dist+'js/')); +}); + +gulp.task('copyDistHTML', ['copyDistJS'], function() { + var copyconfig=[].concat(config.distHtmlFiles); + log('copying distribution HTML files'); + return gulp + .src(copyconfig) + .pipe(gulp.dest(config.dist+'html/')); +}); + +gulp.task('copyDistCSS', ['copyDistHTML'], function() { + var copyconfig=[].concat(config.distCssFiles); + log('copying distribution CSS files'); + return gulp + .src(copyconfig) + .pipe(gulp.dest(config.dist+'css/')); +}); + +gulp.task('templatecachedist',['copyDistCSS'],function(){ + log('creating AngularJS $templateCache for dist'); + return gulp + .src(config.htmltemplatesDist) + .pipe($.if(args.verbose,$.print())) + .pipe($.minifyHtml({empty:true})) + .pipe($.angularTemplatecache( + config.templateCacheDist.file, + config.templateCacheDist.options + )) + .pipe(gulp.dest(config.dist+'js/')); +}); + +gulp.task('distribute', ['templatecachedist'], function() { + log('optimizing the dist files'); + var cssFilter = $.filter('**/*.css',{restore:true}); + var jsFilter = $.filter('**/*.js',{restore:true}); + var htmlFilter = $.filter('**/*.html',{restore:true}); + + return gulp + .src(config.dist+'**/*') + .pipe(cssFilter) + .pipe($.if(args.verbose,$.print())) + .pipe($.csso()) + .pipe($.rename({suffix: '.min'})) + .pipe(cssFilter.restore) + .pipe(jsFilter) + .pipe($.if(args.verbose,$.print())) + .pipe($.uglify()) + .pipe($.rename({suffix: '.min'})) + .pipe(jsFilter.restore) + .pipe(htmlFilter) + .pipe($.if(args.verbose,$.print())) + .pipe($.minifyHtml({empty:true})) + .pipe(htmlFilter.restore) + .pipe(gulp.dest(config.dist)); +}); + +gulp.task('serve-dev',['wireindex'],function() { + log('Starting webserver for - DEV'); + var options = config.getWebserverOptions(); + options.middleware=[getStatic({route: /^\/json/, handle: serveStatic('resources')})]; + gulp + .src(config.source) + .pipe($.if(args.verbose,$.print())) + .pipe($.webserver(options)); +}); + +gulp.task('serve-prod',['optimize'],function() { + log('Starting webserver for - PROD'); + var options = config.getWebserverOptions(); + options.middleware=[getStatic({route: /^\/json/, handle: serveStatic('resources')})]; + gulp + .src(config.build) + .pipe($.if(args.verbose,$.print())) + .pipe($.webserver(options)); +}); + +//gulp.task('bump', function() { +// // not using this -- mainly for bumping the npm version in package.json +//}); + +//gulp.task('lintHTML', function () { +// log('Analyzing HTML files'); +// return gulp.src(config.allhtml) +// .pipe($.if(args.verbose,$.print())) +// .pipe($.htmllint({failOnError: true}, htmllintReporter)); +//}); + +//gulp.task('styles', function() { +// log('compiling Less --> CSS'); +// +// return gulp +// .src(config.less) +// .pipe($.less()) +// .pipe($.autoprefixer({browsers: ['last 2 version','> 5%']})) +// .pipe(gulp.dest(config.temp)); +// +//}); + +//////////////////// + +function log(msg) { + if(typeof(msg)==='object') { + for(var item in msg) { + if(msg.hasOwnProperty(item)) { + $.util.log($.util.colors.blue(msg[item])); + } + } + } else { + $.util.log($.util.colors.blue(msg)); + } +} + +var myCustomReporter = function (file) { + $.util.log('File ' + file.path + ' is not valid JSON.'); +}; + +function getStatic(opts) { + return function(req, res, next) { + if (parseurl(req).pathname.match(opts.route)) { + return opts.handle(req, res, next); + } else { + return next(); + } + } +} + +//function htmllintReporter(filepath, issues) { +// if (issues.length > 0) { +// issues.forEach(function (issue) { +// $.util.log($.util.colors.cyan('[gulp-htmllint] ') + $.util.colors.white(filepath + ' [' + issue.line + ',' + issue.column + ']: ') + $.util.colors.red('(' + issue.code + ') ' + issue.msg)); +// }); +// //process.exitCode = 1; +// this.emit('error', new $.util.PluginError('gulp-htmllint', 'Linter errors occurred!')); +// this.emit('end'); +// } +//} diff --git a/package.json b/package.json index 9d486b1..c54ff5b 100644 --- a/package.json +++ b/package.json @@ -1,27 +1,51 @@ { "name": "angular-multiselect", - "version": "1.2.0", + "version": "1.3.13", "description": "A Native AngularJS multiselect directive", "main": "gulpfile.js", "dependencies": {}, "devDependencies": { - "del": "^2.0.2", - "gulp": "^3.9.0", - "gulp-angular-filesort": "^1.1.1", - "gulp-angular-templatecache": "^1.7.0", - "gulp-concat": "^2.6.0", - "gulp-lintspaces": "^0.3.1", - "gulp-uglify": "^1.4.1", - "gulp-watch": "^4.3.5", - "merge-stream": "^1.0.0", - "run-sequence": "^1.1.4" + "del": "^2.2.2", + "eslint-config-angular": "^0.5.0", + "eslint-plugin-angular": "^2.4.0", + "gulp": "^3.9.1", + "gulp-angular-templatecache": "^2.0.0", + "gulp-csslint": "^1.0.0", + "gulp-csso": "^3.0.0", + "gulp-eslint": "^3.0.1", + "gulp-filter": "^5.0.0", + "gulp-flatten": "^0.3.1", + "gulp-html-angular-validate": "^0.2.0", + "gulp-if": "^2.0.2", + "gulp-imagemin": "^3.2.0", + "gulp-inject": "^4.2.0", + "gulp-jsonlint": "^1.2.0", + "gulp-jsonminify": "^1.0.0", + "gulp-load-plugins": "^1.5.0", + "gulp-main-bower-files": "^1.6.2", + "gulp-minify-html": "^1.0.6", + "gulp-ng-annotate": "^2.0.0", + "gulp-plumber": "^1.1.0", + "gulp-print": "^2.0.1", + "gulp-rename": "^1.2.2", + "gulp-rev": "^7.1.2", + "gulp-rev-replace": "^0.4.3", + "gulp-task-listing": "^1.0.1", + "gulp-uglify": "^2.1.2", + "gulp-useref": "^3.1.2", + "gulp-util": "^3.0.8", + "gulp-webserver": "^0.9.1", + "parseurl": "^1.3.1", + "serve-static": "^1.12.2", + "wiredep": "^4.0.0", + "yargs": "^8.0.1" }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { "type": "git", - "url": "git+https://github.com/amitava82/angular-multiselect.git" + "url": "git+https://github.com/NithinBiliya/angular-multiselect.git" }, "keywords": [ "angular-multiselect", @@ -29,20 +53,10 @@ "angular", "angulajs-multiselect" ], - "author": "amitava82 (github.com/amitava82)", - "contributors": [ - { - "name": "Nagaraja T", - "email": "naga2raja@gmail.com" - }, - { - "name": "Zach Lysobey", - "email": "zlysobey@gmail.com" - } - ], + "author": "Nithin Biliya", "license": "MIT", "bugs": { - "url": "https://github.com/amitava82/angular-multiselect/issues" + "url": "https://github.com/NithinBiliya/angular-multiselect/issues" }, - "homepage": "https://github.com/amitava82/angular-multiselect#readme" + "homepage": "https://github.com/NithinBiliya/angular-multiselect#readme" } diff --git a/src/app.js b/src/app.js deleted file mode 100644 index 7f14db4..0000000 --- a/src/app.js +++ /dev/null @@ -1,17 +0,0 @@ -angular.module('app', ['am.multiselect']) - -.controller('appCtrl', ['$scope', function($scope){ - $scope.cars = [ - {id:1, name: 'Audi'}, - {id:2, name: 'BMW'}, - {id:3, name: 'Honda'} - ]; - $scope.selectedCar = []; - - $scope.fruits = [ - {id: 1, name: 'Apple'}, - {id: 2, name: 'Orange'}, - {id: 3, name: 'Banana'} - ]; - $scope.selectedFruit = null; -}]); diff --git a/src/css/multiselect.css b/src/css/multiselect.css new file mode 100644 index 0000000..4890e90 --- /dev/null +++ b/src/css/multiselect.css @@ -0,0 +1,30 @@ +/*csslint box-sizing: false*/ + +am-multiselect .dropdown-menu{ + box-sizing: content-box; + height: 200px; + overflow-x: scroll; + overflow-y: scroll; + padding-left: 5px; + padding-right: 5px; +} + +am-multiselect .dropdown-menu > li > a { + cursor:pointer; + padding: 3px 10px; +} + +am-multiselect .dropdown-menu > .selected { + background-color: #ADD8E6; +} + +am-multiselect .btn-group > button { + border-radius:5px !important; /* csslint allow: known-properties, important */ + overflow:hidden; + white-space: normal; + width:100%; +} + +am-multiselect .btn-group { + width:100%; +} diff --git a/dist/multiselect.tmpl.html b/src/html/multiselect.tmpl.html similarity index 74% rename from dist/multiselect.tmpl.html rename to src/html/multiselect.tmpl.html index 4a57429..4bb8d58 100644 --- a/dist/multiselect.tmpl.html +++ b/src/html/multiselect.tmpl.html @@ -1,11 +1,11 @@
-
diff --git a/dist/singleselect.tmpl.html b/src/html/singleselect.tmpl.html similarity index 98% rename from dist/singleselect.tmpl.html rename to src/html/singleselect.tmpl.html index 5d629d1..cc894a8 100644 --- a/dist/singleselect.tmpl.html +++ b/src/html/singleselect.tmpl.html @@ -9,4 +9,4 @@ {{i.label}} - \ No newline at end of file + diff --git a/src/index.html b/src/index.html index b32f42e..05662f1 100644 --- a/src/index.html +++ b/src/index.html @@ -1,28 +1,55 @@ + - + + + + + + + + + + + + + Angular multiselect - - - - - - - -

Example

- -
- {{selectedCar}} -
- Single select: - -
- {{selectedFruit}} -
- + + + +

Example

+ +
+ {{vm.selectedCar}} +
+ Single select: + +
+ {{vm.selectedFruit}} +
+ + + + + + + + + + + + + + + + + diff --git a/src/js/app.js b/src/js/app.js new file mode 100644 index 0000000..ed95899 --- /dev/null +++ b/src/js/app.js @@ -0,0 +1,106 @@ +/* eslint angular/controller-name: "off" */ + +angular.module('app', ['am.multiselect']) + .controller('appCtrl', ['$log',function ($log) { + var vm=this; + + vm.cars = [ + { + id: 1, + name: 'Audi' + }, + { + id: 2, + name: 'BMW' + }, + { + id: 3, + name: 'Honda' + }, + { + id: 4, + name: 'Audi' + }, + { + id: 5, + name: 'BMW' + }, + { + id: 6, + name: 'Honda' + }, + { + id: 7, + name: 'Audi' + }, + { + id: 8, + name: 'BMW' + }, + { + id: 9, + name: 'Honda' + }, + { + id: 10, + name: 'Audi' + }, + { + id: 11, + name: 'BMW' + }, + { + id: 12, + name: 'Honda' + }, + { + id: 13, + name: 'Audi' + }, + { + id: 14, + name: 'BMW' + }, + { + id: 15, + name: 'Honda' + }, + { + id: 16, + name: 'Audi' + }, + { + id: 17, + name: 'BMW' + }, + { + id: 18, + name: 'Honda' + } + ]; + vm.selectedCar = []; + + vm.fruits = [ + { + id: 1, + name: 'Apple' + }, + { + id: 2, + name: 'Orange' + }, + { + id: 3, + name: 'Banana' + }, + { + id: 4, + name: 'BananaBananaBananaBananaBananaBananaBananaBananaBananaBananaBananaBananaBananaBananaBananaBananaBanana' + } + ]; + vm.selectedFruit = null; + + vm.focusHanadler=function(prop1) { + $log.info("inside focusHandler - "+prop1); + } +}]); diff --git a/dist/multiselect.js b/src/js/multiselect.js similarity index 81% rename from dist/multiselect.js rename to src/js/multiselect.js index 94987b9..2622f7c 100644 --- a/dist/multiselect.js +++ b/src/js/multiselect.js @@ -1,4 +1,5 @@ // Source: https://github.com/amitava82/angular-multiselect + angular.module('am.multiselect', []) // from bootstrap-ui typeahead parser @@ -39,6 +40,7 @@ angular.module('am.multiselect', []) var exp = attrs.options, parsedResult = optionParser.parse(exp), isMultiple = attrs.multiple ? true : false, + isHover = attrs.hover ? true : false, required = false, scope = originalScope.$new(), changeHandler = attrs.change || angular.noop; @@ -47,7 +49,11 @@ angular.module('am.multiselect', []) scope.header = 'Select'; scope.multiple = isMultiple; scope.disabled = false; + scope.searchDisable = false; scope.onBlur = attrs.ngBlur || angular.noop; + scope.hoverText = isHover ? scope.header : ''; + scope.onFocus = attrs.ngFocus; + scope.clazz = attrs.clazz; originalScope.$on('$destroy', function () { scope.$destroy(); @@ -67,11 +73,18 @@ angular.module('am.multiselect', []) // watch disabled state scope.$watch(function () { - return $parse(attrs.disabled)(originalScope); + return $parse(attrs.ngDisabled)(originalScope); }, function (newVal) { scope.disabled = newVal; }); + // watch disabled state for search text box + scope.$watch(function () { + return $parse(attrs.searchDisable)(originalScope); + }, function (newVal) { + scope.searchDisable = newVal; + }); + // watch single/multiple state for dynamically change single to multiple scope.$watch(function () { return $parse(attrs.multiple)(originalScope); @@ -90,22 +103,29 @@ angular.module('am.multiselect', []) // watch model change scope.$watch(function () { return modelCtrl.$modelValue; - }, function (newVal, oldVal) { + }, function (newVal) { + // When the model is assigned a "" or undefined value from controller, need to uncheck all items and clear searchText.label + if(angular.isUndefined(newVal) || newVal==="" || newVal===null) { + scope.uncheckAll(); + if(angular.isDefined(scope.searchText)) + scope.searchText.label=""; + } // when directive initialize, newVal usually undefined. Also, if model value already set in the controller // for preselected list then we need to mark checked in our scope item. But we don't want to do this every time // model changes. We need to do this only if it is done outside directive scope, from controller, for example. - if (angular.isDefined(newVal)) { + else if (angular.isDefined(newVal)) { markChecked(newVal); scope.$eval(changeHandler); } getHeaderText(); + scope.hoverText = isHover ? scope.header : ''; modelCtrl.$setValidity('required', scope.valid()); }, true); function parseModel() { scope.items.length = 0; var model = parsedResult.source(originalScope); - if(!angular.isDefined(model)) return; + if(angular.isUndefined(model)) return; for (var i = 0; i < model.length; i++) { var local = {}; local[parsedResult.itemName] = model[i]; @@ -121,8 +141,16 @@ angular.module('am.multiselect', []) element.append($compile(popUpEl)(scope)); + function getItemLabel(items,model) { + for(var i = 0; i < items.length; i++) { + if(items[i].model==model) { + return items[i].label; + } + } + } + function getHeaderText() { - if (is_empty(modelCtrl.$modelValue)) return scope.header = (attrs.msHeader!==undefined ? attrs.msHeader : 'Select'); + if (is_empty(modelCtrl.$modelValue)) return scope.header = (angular.isDefined(attrs.msHeader) ? attrs.msHeader : 'Select'); if (isMultiple) { if (attrs.msSelected) { @@ -140,11 +168,11 @@ angular.module('am.multiselect', []) } } else { if(angular.isString(modelCtrl.$modelValue)){ - scope.header = modelCtrl.$modelValue; + scope.header = getItemLabel(scope.items,modelCtrl.$modelValue); } else { var local = {}; local[parsedResult.itemName] = modelCtrl.$modelValue; - scope.header = parsedResult.viewMapper(local) || scope.items[modelCtrl.$modelValue].label; + scope.header = parsedResult.viewMapper(local) || getItemLabel(scope.items,modelCtrl.$modelValue); } } } @@ -154,7 +182,7 @@ angular.module('am.multiselect', []) if (obj && obj.length && obj.length > 0) return false; for (var prop in obj) if (obj[prop]) return false; return true; - }; + } scope.valid = function validModel() { if(!required) return true; @@ -178,13 +206,14 @@ angular.module('am.multiselect', []) } function setModelValue(isMultiple) { - var value = null; + var value = undefined; if (isMultiple) { value = []; angular.forEach(scope.items, function (item) { if (item.checked) value.push(item.model); }) + if(value.length==0) value=undefined; } else { angular.forEach(scope.items, function (item) { if (item.checked) { @@ -228,11 +257,13 @@ angular.module('am.multiselect', []) }; scope.uncheckAll = function () { - var items = (scope.searchText && scope.searchText.label.length > 0) ? $filter('filter')(scope.items, scope.searchText) : scope.items; - angular.forEach(items, function (item) { + // need to uncheck from the entire list of items. If user filers with ine text and selects item A. Next time user fileters and selects item B (item A now not in the filtered set). The item A will not get unchecked + // var items = (scope.searchText && scope.searchText.label.length > 0) ? $filter('filter')(scope.items, scope.searchText) : scope.items; + angular.forEach(scope.items, function (item) { item.checked = false; }); - setModelValue(true); + // sending scope.multiple instead of true to setModelValue. Since different values geeting set when single and multiple. + setModelValue(scope.multiple); }; scope.select = function (item) { @@ -252,15 +283,18 @@ angular.module('am.multiselect', []) restrict: 'E', scope: false, replace: true, - templateUrl: function (element, attr) { - return attr.templateUrl || 'multiselect.tmpl.html'; - }, - link: function (scope, element, attrs) { + templateUrl: 'html/multiselect.tmpl.html', + link: function (scope, element) { scope.selectedIndex = null; scope.isVisible = false; scope.filteredItems = null; + scope.ngClazz = { + 'error': !scope.valid() + }; + scope.ngClazz[scope.$eval(scope.clazz)] = true; + scope.toggleSelect = function () { if (element.hasClass('open')) { element.removeClass('open'); @@ -273,6 +307,10 @@ angular.module('am.multiselect', []) } }; + scope.evalFocus = function() { + scope.$parent.$eval(scope.onFocus); + } + function clickHandler(event) { if (elementMatchesAnyInArray(event.target, element.find(event.target.tagName))) { scope.$parent.$eval(scope.onBlur); diff --git a/src/multiselect.css b/src/multiselect.css deleted file mode 100644 index 3b817ad..0000000 --- a/src/multiselect.css +++ /dev/null @@ -1,13 +0,0 @@ -am-multiselect .dropdown-menu{ - padding-left: 5px; - padding-right: 5px; -} - -am-multiselect .dropdown-menu > li > a { - padding: 3px 10px; - cursor:pointer; -} - -am-multiselect .dropdown-menu > li.selected { - background-color: #ADD8E6; -} \ No newline at end of file diff --git a/src/multiselect.js b/src/multiselect.js deleted file mode 100644 index 94987b9..0000000 --- a/src/multiselect.js +++ /dev/null @@ -1,324 +0,0 @@ -// Source: https://github.com/amitava82/angular-multiselect -angular.module('am.multiselect', []) - -// from bootstrap-ui typeahead parser -.factory('optionParser', ['$parse', function ($parse) { - // 00000111000000000000022200000000000000003333333333333330000000000044000 - var TYPEAHEAD_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+([\s\S]+?)$/; - return { - parse:function (input) { - var match = input.match(TYPEAHEAD_REGEXP); - if (!match) { - throw new Error( - 'Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_"' + - ' but got "' + input + '".'); - } - return { - itemName:match[3], - source:$parse(match[4]), - viewMapper:$parse(match[2] || match[1]), - modelMapper:$parse(match[1]) - }; - } - }; -}]) - -.directive('amMultiselect', ['$parse', '$document', '$compile', '$interpolate', '$filter', 'optionParser', - - function ($parse, $document, $compile, $interpolate, $filter, optionParser) { - return { - restrict: 'E', - require: 'ngModel', - link: function (originalScope, element, attrs, modelCtrl) { - // Redefine isEmpty - this allows this to work on at least Angular 1.2.x - var isEmpty = modelCtrl.$isEmpty; - modelCtrl.$isEmpty = function(value) { - return isEmpty(value) || (angular.isArray(value) && value.length == 0); - }; - - var exp = attrs.options, - parsedResult = optionParser.parse(exp), - isMultiple = attrs.multiple ? true : false, - required = false, - scope = originalScope.$new(), - changeHandler = attrs.change || angular.noop; - - scope.items = []; - scope.header = 'Select'; - scope.multiple = isMultiple; - scope.disabled = false; - scope.onBlur = attrs.ngBlur || angular.noop; - - originalScope.$on('$destroy', function () { - scope.$destroy(); - }); - - var popUpEl = angular.element(''); - - // required validator - if (attrs.required || attrs.ngRequired) { - required = true; - } - attrs.$observe('required', function(newVal) { - required = newVal; - }); - - // watch disabled state - scope.$watch(function () { - return $parse(attrs.disabled)(originalScope); - }, function (newVal) { - scope.disabled = newVal; - }); - - // watch single/multiple state for dynamically change single to multiple - scope.$watch(function () { - return $parse(attrs.multiple)(originalScope); - }, function (newVal) { - isMultiple = newVal || false; - }); - - // watch option changes for options that are populated dynamically - scope.$watch(function () { - return parsedResult.source(originalScope); - }, function (newVal) { - if (angular.isDefined(newVal)) - parseModel(); - }, true); - - // watch model change - scope.$watch(function () { - return modelCtrl.$modelValue; - }, function (newVal, oldVal) { - // when directive initialize, newVal usually undefined. Also, if model value already set in the controller - // for preselected list then we need to mark checked in our scope item. But we don't want to do this every time - // model changes. We need to do this only if it is done outside directive scope, from controller, for example. - if (angular.isDefined(newVal)) { - markChecked(newVal); - scope.$eval(changeHandler); - } - getHeaderText(); - modelCtrl.$setValidity('required', scope.valid()); - }, true); - - function parseModel() { - scope.items.length = 0; - var model = parsedResult.source(originalScope); - if(!angular.isDefined(model)) return; - for (var i = 0; i < model.length; i++) { - var local = {}; - local[parsedResult.itemName] = model[i]; - scope.items.push({ - label: parsedResult.viewMapper(local), - model: parsedResult.modelMapper(local), - checked: false - }); - } - } - - parseModel(); - - element.append($compile(popUpEl)(scope)); - - function getHeaderText() { - if (is_empty(modelCtrl.$modelValue)) return scope.header = (attrs.msHeader!==undefined ? attrs.msHeader : 'Select'); - - if (isMultiple) { - if (attrs.msSelected) { - scope.header = $interpolate(attrs.msSelected)(scope); - } else { - if (modelCtrl.$modelValue.length == 1) { - for(var i = 0; i < scope.items.length; i++) { - if(scope.items[i].model === modelCtrl.$modelValue[0]) { - scope.header = scope.items[i].label; - } - } - } else { - scope.header = modelCtrl.$modelValue.length + ' ' + 'selected'; - } - } - } else { - if(angular.isString(modelCtrl.$modelValue)){ - scope.header = modelCtrl.$modelValue; - } else { - var local = {}; - local[parsedResult.itemName] = modelCtrl.$modelValue; - scope.header = parsedResult.viewMapper(local) || scope.items[modelCtrl.$modelValue].label; - } - } - } - - function is_empty(obj) { - if (angular.isNumber(obj)) return false; - if (obj && obj.length && obj.length > 0) return false; - for (var prop in obj) if (obj[prop]) return false; - return true; - }; - - scope.valid = function validModel() { - if(!required) return true; - var value = modelCtrl.$modelValue; - return (angular.isArray(value) && value.length > 0) || (!angular.isArray(value) && value != null); - }; - - function selectSingle(item) { - if (item.checked) { - scope.uncheckAll(); - } else { - scope.uncheckAll(); - item.checked = !item.checked; - } - setModelValue(false); - } - - function selectMultiple(item) { - item.checked = !item.checked; - setModelValue(true); - } - - function setModelValue(isMultiple) { - var value = null; - - if (isMultiple) { - value = []; - angular.forEach(scope.items, function (item) { - if (item.checked) value.push(item.model); - }) - } else { - angular.forEach(scope.items, function (item) { - if (item.checked) { - value = item.model; - return false; - } - }) - } - modelCtrl.$setViewValue(value); - } - - function markChecked(newVal) { - if (!angular.isArray(newVal)) { - angular.forEach(scope.items, function (item) { - if (angular.equals(item.model, newVal)) { - scope.uncheckAll(); - item.checked = true; - setModelValue(false); - return false; - } - }); - } else { - angular.forEach(scope.items, function (item) { - item.checked = false; - angular.forEach(newVal, function (i) { - if (angular.equals(item.model, i)) { - item.checked = true; - } - }); - }); - } - } - - scope.checkAll = function () { - if (!isMultiple) return; - var items = (scope.searchText && scope.searchText.label.length > 0) ? $filter('filter')(scope.items, scope.searchText) : scope.items; - angular.forEach(items, function (item) { - item.checked = true; - }); - setModelValue(true); - }; - - scope.uncheckAll = function () { - var items = (scope.searchText && scope.searchText.label.length > 0) ? $filter('filter')(scope.items, scope.searchText) : scope.items; - angular.forEach(items, function (item) { - item.checked = false; - }); - setModelValue(true); - }; - - scope.select = function (item) { - if (isMultiple === false) { - selectSingle(item); - scope.toggleSelect(); - } else { - selectMultiple(item); - } - } - } - }; -}]) - -.directive('amMultiselectPopup', ['$document', '$filter', function ($document, $filter) { - return { - restrict: 'E', - scope: false, - replace: true, - templateUrl: function (element, attr) { - return attr.templateUrl || 'multiselect.tmpl.html'; - }, - link: function (scope, element, attrs) { - - scope.selectedIndex = null; - scope.isVisible = false; - scope.filteredItems = null; - - scope.toggleSelect = function () { - if (element.hasClass('open')) { - element.removeClass('open'); - $document.unbind('click', clickHandler); - scope.$parent.$eval(scope.onBlur); - } else { - element.addClass('open'); - $document.bind('click', clickHandler); - scope.focus(); - } - }; - - function clickHandler(event) { - if (elementMatchesAnyInArray(event.target, element.find(event.target.tagName))) { - scope.$parent.$eval(scope.onBlur); - } else { - element.removeClass('open'); - $document.unbind('click', clickHandler); - scope.$apply(); - } - } - - scope.focus = function focus(){ - var searchBox = element.find('input')[0]; - if (searchBox) { - searchBox.focus(); - } - } - - scope.keydown = function (event) { - var list = $filter('filter')(scope.items, scope.searchText); - var keyCode = (event.keyCode || event.which); - - if(keyCode === 13){ // On enter - if(list[scope.selectedIndex]){ - scope.select(list[scope.selectedIndex]); // (un)select item - } - }else if(keyCode === 38){ // On arrow up - scope.selectedIndex = scope.selectedIndex===null ? list.length-1 : scope.selectedIndex-1; - }else if(keyCode === 40){ // On arrow down - scope.selectedIndex = scope.selectedIndex===null ? 0 : scope.selectedIndex+1; - }else{ // On any other key - scope.selectedIndex = null; - } - - if(scope.selectedIndex < 0){ // Select last in list - scope.selectedIndex = list.length-1; - }else if(scope.selectedIndex > list.length-1){ // Set selection to first item in list - scope.selectedIndex = 0; - } - }; - - var elementMatchesAnyInArray = function (element, elementArray) { - for (var i = 0; i < elementArray.length; i++) - if (element == elementArray[i]) - return true; - return false; - } - } - } -}]); diff --git a/src/multiselect.tmpl.html b/src/multiselect.tmpl.html deleted file mode 100644 index 4a57429..0000000 --- a/src/multiselect.tmpl.html +++ /dev/null @@ -1,19 +0,0 @@ -
- - -
diff --git a/src/singleselect.tmpl.html b/src/singleselect.tmpl.html deleted file mode 100644 index 5d629d1..0000000 --- a/src/singleselect.tmpl.html +++ /dev/null @@ -1,12 +0,0 @@ - \ No newline at end of file