├── TODO.md ├── src ├── module.js ├── style.css ├── form-directive.js ├── model-directive.js └── helper.js ├── dist ├── angular-validation-messages.css ├── angular-validation-messages.min.js └── angular-validation-messages.js ├── .gitignore ├── .idea ├── scopes │ └── scope_settings.xml ├── encodings.xml ├── vcs.xml ├── modules.xml ├── libraries │ └── Node_js_Dependencies_for_angular_validation_messages.xml ├── angular-validation-messages.iml └── misc.xml ├── .jscrc ├── CHANGELOG.md ├── .travis.yml ├── coverage └── PhantomJS 1.9.8 (Mac OS X) │ ├── prettify.css │ ├── src │ ├── time-heatmap │ │ ├── time-directive.js.html │ │ └── index.html │ ├── module.js.html │ ├── directive.js.html │ ├── heatmap │ │ └── index.html │ ├── form-directive.js.html │ └── index.html │ ├── index.html │ └── prettify.js ├── bower.json ├── .jshintrc ├── simpletest.html ├── LICENSE ├── package.json ├── karma.dist.conf.js ├── karma.conf.js ├── Gruntfile.js ├── test ├── lib │ ├── jasmine-dom-fixtures.js │ └── jasmine-dom-matchers.js ├── form-directive.js ├── helper.js └── model-directive.js └── README.md /TODO.md: -------------------------------------------------------------------------------- 1 | - add nice looking styles 2 | - add readme 3 | -------------------------------------------------------------------------------- /src/module.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | angular.module('gg.vmsgs', []); 3 | -------------------------------------------------------------------------------- /dist/angular-validation-messages.css: -------------------------------------------------------------------------------- 1 | form .validation-message{color:#9a1919;display:block} -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | form .validation-message{ 2 | color: #9a1919; 3 | display:block; 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .gitignore support plugin (hsz.mobi) 2 | bower_components 3 | node_modules 4 | .idea 5 | coverage 6 | -------------------------------------------------------------------------------- /.idea/scopes/scope_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.jscrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "crockford", 3 | "validateIndentation": 2, 4 | "disallowMultipleVarDecl": null, 5 | "requireMultipleVarDecl": null, 6 | "requireCamelCaseOrUpperCaseIdentifiers": "ignoreProperties", 7 | "disallowDanglingUnderscores": null 8 | } 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Version numbers correspond to `bower.json` version 2 | 3 | # 0.2.4 4 | 5 | ## Features 6 | -Add parentSuccessClassName option, to support adding success classes to parent containers 7 | -Allow customization of error keys to allow custom validation directives (thanks @pkalkman) 8 | 9 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | 5 | before_script: 6 | - export DISPLAY=:99.0 7 | - sh -e /etc/init.d/xvfb start 8 | - npm install --quiet -g grunt-cli karma 9 | - npm install bower 10 | - npm install 11 | - bower install 12 | 13 | script: grunt build 14 | 15 | after_script: 16 | - cat coverage/*/lcov.info | ./node_modules/coveralls/bin/coveralls.js 17 | -------------------------------------------------------------------------------- /.idea/libraries/Node_js_Dependencies_for_angular_validation_messages.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/angular-validation-messages.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /coverage/PhantomJS 1.9.8 (Mac OS X)/prettify.css: -------------------------------------------------------------------------------- 1 | .pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} 2 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-validation-messages", 3 | "version": "0.2.7", 4 | "main": "dist/angular-validation-messages.js", 5 | "homepage": "http://gabigrin.github.io/angular-validation-messages/", 6 | "authors": [ 7 | "GabiGrin " 8 | ], 9 | "description": "Add error messages to your forms by adding a simple attribute", 10 | "keywords": [ 11 | "angular", 12 | "validation", 13 | "validations", 14 | "vmsg", 15 | "vmsg-form", 16 | "form", 17 | "ux", 18 | "messages", 19 | "usability", 20 | "inputs", 21 | "directive" 22 | ], 23 | "license": "MIT", 24 | "ignore": [ 25 | "**/.*", 26 | "node_modules", 27 | "bower_components", 28 | "test", 29 | "tests" 30 | ], 31 | "dependencies": { 32 | "angular": "~1.3.0" 33 | }, 34 | "devDependencies": { 35 | "angular-mocks": "~1.3.2" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": true, 3 | "camelcase": true, 4 | "curly": true, 5 | "eqeqeq": true, 6 | "immed": true, 7 | "indent": 2, 8 | "latedef": true, 9 | "newcap": true, 10 | "noarg": true, 11 | "quotmark": "single", 12 | "browser": true, 13 | "undef": true, 14 | "unused": true, 15 | "strict": true, 16 | "globalstrict": true, 17 | "trailing": true, 18 | "smarttabs": true, 19 | "white": true, 20 | "globals": { 21 | "_": false, 22 | "angular": false, 23 | "require": false, 24 | "module": false, 25 | "$": false, 26 | "window": false, 27 | "console": true, 28 | "unitTestUtils": false, 29 | 30 | "describe": false, 31 | "it": false, 32 | "iit": false, 33 | "inject": false, 34 | "beforeEach": false, 35 | "afterEach": false, 36 | "expect": false, 37 | "spyOn": false, 38 | "runs": false, 39 | "waitsFor": false 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /simpletest.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |
10 | 11 | 12 | 13 | 14 |
15 | 16 |
17 | 18 | 21 | 22 | 35 | 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 GabiGrin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/form-directive.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | angular.module('gg.vmsgs') 4 | .directive('vmsgForm', function ($parse) { 5 | return { 6 | restrict: 'A', 7 | require: 'form', 8 | priority: -1, 9 | compile: function () { 10 | return { 11 | pre: function preLink(scope, elem, attrs) { 12 | 13 | scope.formOpts = $parse(attrs.vmsgForm || '')(scope); 14 | elem.attr('novalidate', 'novalidate'); 15 | scope.$watch(function () { 16 | return attrs.vmsgForm; 17 | }, function (opts) { 18 | scope.formOpts = $parse(opts || '')(scope); 19 | }); 20 | }, 21 | post: function postLink(scope, elem, attrs, formCtrl) { 22 | elem.bind('submit', function (e) { 23 | if (formCtrl.$invalid) { 24 | e.stopImmediatePropagation(); 25 | scope.$broadcast('submit'); 26 | } 27 | }); 28 | } 29 | }; 30 | }, 31 | controller: function ($scope) { 32 | this.getOptions = function () { 33 | return $scope.formOpts; 34 | }; 35 | } 36 | }; 37 | }); 38 | })(); 39 | 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-validation-messages", 3 | "version": "0.0.1", 4 | "description": "", 5 | "main": "dist/angular-validation-messages.min.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/GabiGrin/angular-validation-messages.git" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "devDependencies": { 16 | "connect-livereload": "^0.5.0", 17 | "coveralls": "^2.11.2", 18 | "grunt": "^0.4.5", 19 | "grunt-contrib-clean": "^0.6.0", 20 | "grunt-contrib-concat": "^0.5.0", 21 | "grunt-contrib-connect": "^0.8.0", 22 | "grunt-contrib-cssmin": "^0.10.0", 23 | "grunt-contrib-jshint": "^0.10.0", 24 | "grunt-contrib-uglify": "^0.6.0", 25 | "grunt-contrib-watch": "^0.6.1", 26 | "grunt-coveralls": "^1.0.0", 27 | "grunt-karma": "^0.9.0", 28 | "grunt-ng-annotate": "^0.7.0", 29 | "grunt-notify": "^0.3.1", 30 | "grunt-regarde": "^0.1.1", 31 | "jshint-growl-reporter": "^0.1.1", 32 | "karma": "^0.12.24", 33 | "karma-chrome-launcher": "^0.1.5", 34 | "karma-coverage": "^0.2.6", 35 | "karma-jasmine": "^0.1.5", 36 | "karma-notify-reporter": "^0.1.1", 37 | "karma-osx-reporter": "^0.1.0", 38 | "karma-phantomjs-launcher": "^0.1.4", 39 | "matchdep": "^0.3.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /karma.dist.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Wed Oct 29 2014 12:17:06 GMT+0200 (IST) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['jasmine'], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | // 'bower_components/jquery/dist/jquery.js', 19 | 'bower_components/angular/angular.js', 20 | 'bower_components/angular-mocks/angular-mocks.js', 21 | 'dist/angular-validation-messages.min.js', 22 | 'test/**/*.js' 23 | ], 24 | 25 | 26 | // list of files to exclude 27 | exclude: [ 28 | ], 29 | 30 | 31 | // preprocess matching files before serving them to the browser 32 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 33 | preprocessors: { 34 | 'src/**/*.js': ['coverage'] 35 | }, 36 | 37 | 38 | // test results reporter to use 39 | // possible values: 'dots', 'progress' 40 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 41 | reporters: ['progress'], 42 | 43 | 44 | // web server port 45 | port: 9876, 46 | 47 | 48 | // enable / disable colors in the output (reporters and logs) 49 | colors: true, 50 | 51 | 52 | // level of logging 53 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 54 | logLevel: config.LOG_INFO, 55 | 56 | 57 | // enable / disable watching file and executing tests whenever any file changes 58 | autoWatch: true, 59 | 60 | 61 | // start these browsers 62 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 63 | browsers: ['PhantomJS'], 64 | 65 | // Continuous Integration mode 66 | // if true, Karma captures browsers, runs the tests and exits 67 | singleRun: true 68 | }); 69 | }; 70 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Wed Oct 29 2014 12:17:06 GMT+0200 (IST) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['jasmine'], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | // 'bower_components/jquery/dist/jquery.js', 19 | 'bower_components/angular/angular.js', 20 | 'bower_components/angular-mocks/angular-mocks.js', 21 | // 'dist/**/*.js', 22 | 'src/module.js', 23 | 'src/**/*.js', 24 | 'test/**/*.js' 25 | ], 26 | 27 | 28 | // list of files to exclude 29 | exclude: [ 30 | ], 31 | 32 | 33 | // preprocess matching files before serving them to the browser 34 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 35 | preprocessors: { 36 | 'src/**/*.js': ['coverage'] 37 | }, 38 | 39 | 40 | // test results reporter to use 41 | // possible values: 'dots', 'progress' 42 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 43 | reporters: ['progress', 'coverage', 'notify'], 44 | 45 | 46 | // web server port 47 | port: 9876, 48 | 49 | 50 | // enable / disable colors in the output (reporters and logs) 51 | colors: true, 52 | 53 | 54 | // level of logging 55 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 56 | logLevel: config.LOG_INFO, 57 | 58 | 59 | // enable / disable watching file and executing tests whenever any file changes 60 | autoWatch: true, 61 | 62 | 63 | // start these browsers 64 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 65 | browsers: ['PhantomJS'], 66 | 67 | coverageReporter: { 68 | type : 'lcov', 69 | dir : 'coverage/' 70 | }, 71 | 72 | 73 | // Continuous Integration mode 74 | // if true, Karma captures browsers, runs the tests and exits 75 | singleRun: true 76 | }); 77 | }; 78 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (grunt) { 4 | 5 | require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks); 6 | 7 | grunt.initConfig({ 8 | concat: { 9 | options: { 10 | // define a string to put between each file in the concatenated output 11 | separator: ';' 12 | }, 13 | dist: { 14 | // the files to concatenate 15 | src: ['src/**/module.js', 'src/**/*.js'], 16 | // the location of the resulting JS file 17 | dest: 'dist/angular-validation-messages.js' 18 | } 19 | }, 20 | ngAnnotate: { 21 | options: { 22 | singleQuotes: true 23 | }, 24 | main: { 25 | files: { 26 | 'dist/angular-validation-messages.js': ['dist/angular-validation-messages.js'] 27 | } 28 | } 29 | }, 30 | clean: ['dist'], 31 | uglify: { 32 | 'dist/angular-validation-messages.min.js': 'dist/angular-validation-messages.js' 33 | }, 34 | jshint: { 35 | options: { 36 | jshintrc: true 37 | }, 38 | all: [ 39 | 'Gruntfile.js', 40 | 'src/**/*.js', 41 | 'test/**/*.js', 42 | '!test/lib/**/*.js' 43 | ] 44 | }, 45 | watch: { 46 | options: { 47 | livereload: 30000 48 | }, 49 | scripts: { 50 | files: ['src/**/*.js', 'Gruntfile.js'], 51 | tasks: ['package', 'test'] 52 | }, 53 | test: { 54 | files: ['test/**/*.js'], 55 | tasks: ['test'] 56 | }, 57 | html: { 58 | files: ['demo/**/*.html'], 59 | tasks: ['package'] 60 | }, 61 | styles: { 62 | files: ['src/**/*.css'], 63 | tasks: ['package'] 64 | } 65 | }, 66 | cssmin: { 67 | combine: { 68 | files: { 69 | 'dist/angular-validation-messages.css': ['src/style.css'] 70 | } 71 | } 72 | }, 73 | coveralls: { 74 | options: { 75 | // LCOV coverage file relevant to every target 76 | force: true 77 | }, 78 | all: { 79 | // Target-specific LCOV coverage file 80 | src: 'coverage/*/lcov.info' 81 | } 82 | }, 83 | connect: { 84 | server: { 85 | options: { 86 | port: 9001, 87 | open: true 88 | } 89 | } 90 | }, 91 | karma: { 92 | unit: { 93 | configFile: 'karma.conf.js' 94 | }, 95 | dist: { 96 | configFile: 'karma.dist.conf.js' 97 | } 98 | } 99 | } 100 | ); 101 | grunt.registerTask('package', ['cssmin', 'jshint', 'concat', 'ngAnnotate', 'uglify']); 102 | grunt.registerTask('test', ['karma:unit', 'karma:dist']); 103 | grunt.registerTask('serve', ['package', 'test', 'connect', 'watch']); 104 | grunt.registerTask('build', ['clean', 'package', 'test', 'coveralls']); 105 | }; 106 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 44 | 45 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 1.6 71 | 72 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /src/model-directive.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Gabriel_Grinberg on 11/15/14. 3 | */ 4 | (function () { 5 | 'use strict'; 6 | angular.module('gg.vmsgs') 7 | .directive('vmsg', function (ValidationMessagesHelper, $parse) { 8 | return { 9 | require: ['ngModel', '^vmsgForm'], 10 | restrict: 'A', 11 | compile: function () { 12 | return function postLink(scope, elem, attrs, ctrls) { 13 | var localOpts = $parse(attrs.vmsg || '')(scope); 14 | var formOpts = ctrls[1].getOptions(); 15 | var opts = ValidationMessagesHelper.getOptions(localOpts, formOpts); 16 | var messageElem = ValidationMessagesHelper.createMessageElement(scope, opts); 17 | var ngModelCtrl = ctrls[0]; 18 | var elemParent = elem.parent(); 19 | 20 | var showStatusMessage = function () { 21 | if (ngModelCtrl.$invalid) { 22 | window.ngModelCtrl = ngModelCtrl; 23 | messageElem.removeClass(opts.hideClassName); 24 | if (elem.parent().hasClass(opts.parentContainerClassName)) { 25 | elemParent.addClass(opts.parentErrorClassName); 26 | } 27 | } else { 28 | if (elemParent.hasClass(opts.parentContainerClassName)) { 29 | elemParent.addClass(opts.parentSuccessClassName); 30 | } 31 | } 32 | }; 33 | 34 | var hideMessage = function () { 35 | messageElem.addClass(opts.hideClassName); 36 | if (elemParent.hasClass(opts.parentContainerClassName)) { 37 | elemParent.removeClass(opts.parentErrorClassName); 38 | elemParent.removeClass(opts.parentSuccessClassName); 39 | } 40 | }; 41 | 42 | elem.data('message-element', messageElem); 43 | elem.after(messageElem); 44 | 45 | //set up show message trigger 46 | switch (opts.showTrigger) { 47 | case 'blur': 48 | case 'keydown': 49 | case 'keyup': 50 | elem.on(opts.showTrigger, function () { 51 | showStatusMessage(); 52 | }); 53 | break; 54 | case 'submit': 55 | //we always show the errors when its submitted.. this option is for clarification only 56 | break; 57 | default: 58 | throw 'Show trigger "' + opts.showTrigger + '" is not supported'; 59 | } 60 | 61 | //we'll always show 62 | scope.$on('submit', function () { 63 | showStatusMessage(); 64 | }); 65 | 66 | //set up hide message trigger 67 | switch (opts.hideTrigger) { 68 | case 'valid': 69 | scope.$watch(function () { 70 | return ngModelCtrl.$valid; 71 | }, function (isValid, wasValid) { 72 | if (isValid !== wasValid) { 73 | hideMessage(); 74 | } 75 | }); 76 | break; 77 | case 'keydown': 78 | case 'keyup': 79 | elem.on(opts.hideTrigger, function () { 80 | hideMessage(); 81 | }); 82 | break; 83 | default: 84 | throw 'Unsupported hide trigger used'; 85 | } 86 | 87 | //each time the error changes, update the error message 88 | scope.$watch(function () { 89 | return ngModelCtrl.$error; 90 | }, function (newError) { 91 | if (ngModelCtrl.$invalid) { 92 | messageElem.find('msg').text(ValidationMessagesHelper.getErrorMessage(newError, elem, opts)); 93 | } 94 | }, true); 95 | 96 | }; 97 | } 98 | }; 99 | }); 100 | })(); 101 | -------------------------------------------------------------------------------- /dist/angular-validation-messages.min.js: -------------------------------------------------------------------------------- 1 | "use strict";angular.module("gg.vmsgs",[]),function(){angular.module("gg.vmsgs").directive("vmsgForm",["$parse",function(a){return{restrict:"A",require:"form",priority:-1,compile:function(){return{pre:function(b,c,d){b.formOpts=a(d.vmsgForm||"")(b),c.attr("novalidate","novalidate"),b.$watch(function(){return d.vmsgForm},function(c){b.formOpts=a(c||"")(b)})},post:function(a,b,c,d){b.bind("submit",function(b){d.$invalid&&(b.stopImmediatePropagation(),a.$broadcast("submit"))})}}},controller:["$scope",function(a){this.getOptions=function(){return a.formOpts}}]}}])}(),function(){angular.module("gg.vmsgs").factory("ValidationMessagesHelper",["$compile",function(a){var b=["required","email","url","minlength","maxlength","pattern","min","max"],c={hideClassName:"ng-hide",messageClassName:"validation-message",messageTemplate:"",showTrigger:"blur",hideTrigger:"valid",dummy:!1,errorMessages:{},parentContainerClassName:"form-group",parentErrorClassName:"has-error",parentSuccessClassName:""};return{setOptions:function(a){c=angular.extend({},c,a||{}),a&&angular.forEach(a,function(c,d){"ERRORMESSAGES"===d.toUpperCase()&&angular.forEach(a[d],function(a,c){-1===b.indexOf(c)&&b.push(c)})})},getOptions:function(a,b){return angular.extend({},c,b||{},a||{})},showTriggers:["blur","submit","keydown","keyup"],hideTriggers:["valid","keydown","keyup"],errorKeys:b,errorMessages:{required:{"default":"This field is required",email:"A valid e-mail address is required",number:"A valid number is required",date:"A valid date is required",week:"A valid week is required",url:"A valid url is required",month:"A valid month is required"},url:"A valid url is required",email:"A valid e-mail address is required",minlength:"This field must be at least {minlength} chars",maxlength:"This field must be less than {maxlength} chars",min:{"default":"This field must be higher than {min}",number:"This number must be higher than {min}",date:"This date must be after {min}",week:"This week must be after {min}",month:"This month must be after {min}"},max:{"default":"This field must be lower than {max}",number:"This number must be lower than {max}",date:"This date must be before {max}",week:"This week must be before {max}",month:"This month must be before {max}"},pattern:"This field must match a specific pattern {pattern}"},renderError:function(a){var b;return b=arguments,2===b.length&&null!==b[1]&&"object"==typeof b[1]&&(b=b[1]),a.replace(/{([^}]*)}/g,function(a,c){return"undefined"!=typeof b[c]?b[c]:a})},_getErrorMessage:function(a,b,c,d){var e=d&&d.errorMessages&&d.errorMessages[a],f=e||this.errorMessages[a];if(f)return this.renderError(f[b]||f["default"]||f,c);throw"Error message not supported for type "+a+" and inputType "+b},getRenderParameters:function(a){var b={};return["minlength","maxlength","min","max","pattern"].forEach(function(c){var d=a.attr(c)||a.attr("ng-"+c);d&&(b[c]=d)}),b},getErrorMessage:function(a,b,c){var d=b.attr("type"),e="",f=this.getRenderParameters(b);return this.errorKeys.forEach(function(b){!e&&a[b]&&(e=b)}),this._getErrorMessage(e,d,f,c)},createMessageElement:function(b,c){return a(c.messageTemplate)(b).addClass(c.messageClassName).addClass(c.hideClassName)}}}])}(),function(){angular.module("gg.vmsgs").directive("vmsg",["ValidationMessagesHelper","$parse",function(a,b){return{require:["ngModel","^vmsgForm"],restrict:"A",compile:function(){return function(c,d,e,f){var g=b(e.vmsg||"")(c),h=f[1].getOptions(),i=a.getOptions(g,h),j=a.createMessageElement(c,i),k=f[0],l=d.parent(),m=function(){k.$invalid?(window.ngModelCtrl=k,j.removeClass(i.hideClassName),d.parent().hasClass(i.parentContainerClassName)&&l.addClass(i.parentErrorClassName)):l.hasClass(i.parentContainerClassName)&&l.addClass(i.parentSuccessClassName)},n=function(){j.addClass(i.hideClassName),l.hasClass(i.parentContainerClassName)&&(l.removeClass(i.parentErrorClassName),l.removeClass(i.parentSuccessClassName))};switch(d.data("message-element",j),d.after(j),i.showTrigger){case"blur":case"keydown":case"keyup":d.on(i.showTrigger,function(){m()});break;case"submit":break;default:throw'Show trigger "'+i.showTrigger+'" is not supported'}switch(c.$on("submit",function(){m()}),i.hideTrigger){case"valid":c.$watch(function(){return k.$valid},function(a,b){a!==b&&n()});break;case"keydown":case"keyup":d.on(i.hideTrigger,function(){n()});break;default:throw"Unsupported hide trigger used"}c.$watch(function(){return k.$error},function(b){k.$invalid&&j.find("msg").text(a.getErrorMessage(b,d,i))},!0)}}}}])}(); -------------------------------------------------------------------------------- /test/lib/jasmine-dom-fixtures.js: -------------------------------------------------------------------------------- 1 | /*jsl:declare jasmine*/ 2 | function readFixtures() 3 | { 4 | return jasmine.getFixtures()._proxyCallTo('read', arguments); 5 | } 6 | 7 | function loadFixtures() 8 | { 9 | jasmine.getFixtures()._proxyCallTo('load', arguments); 10 | } 11 | 12 | function setFixtures(html) 13 | { 14 | jasmine.getFixtures().set(html); 15 | } 16 | 17 | function sandbox(attributes) 18 | { 19 | return jasmine.getFixtures().sandbox(attributes); 20 | } 21 | 22 | 23 | jasmine.getFixtures = function() 24 | { 25 | return jasmine._currentFixtures = jasmine._currentFixtures || new jasmine.Fixtures(); 26 | } 27 | 28 | jasmine.Fixtures = function() 29 | { 30 | this.containerId = 'jasmine-fixtures'; 31 | this._fixturesCache = {}; 32 | } 33 | 34 | jasmine.Fixtures.XHR= window.XMLHttpRequest || (function(){ 35 | var progIdCandidates= ['Msxml2.XMLHTTP.4.0', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP']; 36 | var len= progIdCandidates.length; 37 | 38 | var progId; 39 | var xhr; 40 | 41 | function ConstructXhr() 42 | { 43 | return new window.ActiveXObject(ConstructXhr.progId); 44 | } 45 | 46 | while (len--) 47 | { 48 | try 49 | { 50 | progId= progIdCandidates[len]; 51 | xhr= new window.ActiveXObject(progId); 52 | // ActiveXObject constructor throws an exception 53 | // if the component isn't available. 54 | xhr= null; 55 | ConstructXhr.progId= progId; 56 | return ConstructXhr; 57 | } 58 | catch (e) 59 | { 60 | // Ignore the error 61 | } 62 | } 63 | throw new Error('No XMLHttpRequest implementation found'); 64 | })(); 65 | 66 | jasmine.Fixtures.prototype= { 67 | 68 | set: function(html) 69 | { 70 | this.cleanUp(); 71 | this._createContainer(html); 72 | }, 73 | 74 | load: function() 75 | { 76 | this.cleanUp(); 77 | this._createContainer(this.read.apply(this, arguments)); 78 | }, 79 | 80 | read: function() 81 | { 82 | var htmlChunks = []; 83 | 84 | var fixtureUrls = arguments; 85 | for (var urlCount = fixtureUrls.length, urlIndex = 0; urlIndex < urlCount; urlIndex++) 86 | htmlChunks.push(this._getFixtureHtml(fixtureUrls[urlIndex])); 87 | 88 | return htmlChunks.join(''); 89 | }, 90 | 91 | clearCache: function() 92 | { 93 | this._fixturesCache = {}; 94 | }, 95 | 96 | cleanUp: function() 97 | { 98 | var container= document.getElementById(this.containerId); 99 | if (container) 100 | container.parentNode.removeChild(container); 101 | }, 102 | 103 | sandbox: function(attributes) 104 | { 105 | var attributesToSet = attributes || {}; 106 | var sandbox= document.createElement('div'); 107 | sandbox.id= 'sandbox'; 108 | 109 | if ("string"===typeof(attributes)) 110 | { 111 | sandbox.innerHTML= attributes; 112 | if (1===sandbox.childNodes.length && 1===sandbox.firstChild.nodeType) 113 | { 114 | sandbox= sandbox.firstChild; 115 | if (!sandbox.id) 116 | sandbox.id= 'sandbox'; 117 | } 118 | return sandbox; 119 | } 120 | 121 | for (var attr in attributesToSet) 122 | sandbox.setAttribute(attr, attributesToSet[attr]); 123 | 124 | return sandbox; 125 | }, 126 | 127 | _createContainer: function(html) 128 | { 129 | var container = document.createElement('div'); 130 | container.id= this.containerId; 131 | 132 | if (html && html.nodeType===1) 133 | container.appendChild(html); 134 | else 135 | container.innerHTML= html; 136 | 137 | document.body.appendChild(container); 138 | }, 139 | 140 | _getFixtureHtml: function(url) 141 | { 142 | if (void(0)===this._fixturesCache[url]) 143 | this._loadFixtureIntoCache(url); 144 | return this._fixturesCache[url]; 145 | }, 146 | 147 | _loadFixtureIntoCache: function(url) 148 | { 149 | var self= this; 150 | var xhr= new jasmine.Fixtures.XHR(); 151 | xhr.open('GET', url, true); 152 | 153 | xhr.onreadystatechange= function() 154 | { 155 | if (4!==xhr.readyState) 156 | return; 157 | var status= xhr.status; 158 | var succeeded= 0===status || (status>=200 && status<300) || 304==status; 159 | 160 | if (!succeeded) 161 | throw new Error('Failed to load resource: status=' + status + ' url=' + url); 162 | 163 | self._fixturesCache[url]= xhr.responseText; 164 | xhr.onreadystatechange= null; 165 | xhr= null; 166 | } 167 | xhr.send(null); 168 | }, 169 | 170 | _proxyCallTo: function(methodName, passedArguments) 171 | { 172 | return this[methodName].apply(this, passedArguments); 173 | } 174 | 175 | }; 176 | -------------------------------------------------------------------------------- /test/form-directive.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | describe('form directive tests', function () { 3 | var form, 4 | $compile, 5 | $scope, 6 | body, 7 | service; 8 | 9 | function createElem(template) { 10 | var elem = $compile(template)($scope); 11 | body.append(elem); 12 | return elem; 13 | } 14 | 15 | beforeEach(function () { 16 | module('gg.vmsgs'); 17 | form = null; 18 | inject(function (_$compile_, _$rootScope_, $document, ValidationMessagesHelper) { 19 | $compile = _$compile_; 20 | $scope = _$rootScope_; 21 | body = $document.find('body').empty(); 22 | service = ValidationMessagesHelper; 23 | }); 24 | 25 | }); 26 | 27 | it('should not work in non ng-form elements', function () { 28 | expect(function () { 29 | createElem('
'); 30 | }).toThrow(); 31 | 32 | expect(function () { 33 | createElem('

'); 34 | }).toThrow(); 35 | }); 36 | 37 | it('adds novalidate to forms', function () { 38 | var form = createElem('

'); 39 | expect(form.attr('novalidate')).toBeDefined(); 40 | }); 41 | 42 | it('should not let invalid forms submit themselves', function () { 43 | var form = createElem('
'); 44 | $scope.submit = function () { 45 | console.log('whoops'); 46 | }; 47 | spyOn($scope, 'submit'); 48 | 49 | $scope.form.$setValidity('some-rule', false); 50 | form.triggerHandler('submit'); 51 | expect($scope.submit).not.toHaveBeenCalled(); 52 | 53 | $scope.form.$setValidity('some-rule', true); 54 | form.triggerHandler('submit'); 55 | expect($scope.submit).toHaveBeenCalled(); 56 | }); 57 | 58 | it('should make all children validation errors visible when trying to submit', function () { 59 | var form = createElem('
' + 60 | '' + 61 | '' + 62 | '
'); 63 | var input1 = angular.element(form.children()[0]); 64 | var input2 = angular.element(form.children()[0]); 65 | var vmsg1 = input1.data('message-element')[0]; 66 | var vmsg2 = input2.data('message-element')[0]; 67 | $scope.value1 = ''; 68 | $scope.value2 = 'abc'; 69 | 70 | $scope.$digest(); 71 | expect(vmsg1).toBeHidden(); 72 | expect(vmsg2).toBeHidden(); 73 | 74 | form.triggerHandler('submit'); 75 | $scope.$digest(); 76 | 77 | expect(vmsg1).toBeVisible(); 78 | expect(vmsg2).toBeVisible(); 79 | }); 80 | 81 | it('should support custom error messages from form', function () { 82 | $scope.opts = {errorMessages: {pattern: 'Must be the secret word!'}}; 83 | var form = createElem('
' + 84 | '' + 85 | '
'); 86 | var input = form.find('input'); 87 | var messageElement = input.data('message-element'); 88 | $scope.test = 'not the word'; 89 | $scope.$digest(); 90 | input.triggerHandler('blur'); 91 | $scope.$digest(); 92 | expect(messageElement.text()).toBe('Must be the secret word!'); 93 | }); 94 | 95 | it('should work with various inputs', function () { 96 | var form = createElem('
' + 97 | '' + 98 | '' + 99 | '' + 100 | '' + 101 | '
'); 102 | var nameInput = angular.element(form.find('input')[0]); 103 | var emailInput = angular.element(form.find('input')[1]); 104 | var urlInput = angular.element(form.find('input')[2]); 105 | var ageInput = angular.element(form.find('input')[3]); 106 | 107 | var nameMessage = nameInput.data('message-element')[0]; 108 | var emailMessage = emailInput.data('message-element')[0]; 109 | var urlMessage = urlInput.data('message-element')[0]; 110 | var ageMessage = ageInput.data('message-element')[0]; 111 | 112 | $scope.$digest(); 113 | form.triggerHandler('submit'); 114 | $scope.$digest(); 115 | 116 | expect(nameMessage).toBeVisible(); 117 | expect(emailMessage).toBeVisible(); 118 | expect(urlMessage).toBeHidden(); 119 | expect(ageMessage).toBeVisible(); 120 | 121 | expect(nameMessage).toHaveText(service._getErrorMessage('required')); 122 | expect(emailMessage).toHaveText(service._getErrorMessage('required', 'email')); 123 | expect(ageMessage).toHaveText(service._getErrorMessage('required', 'number')); 124 | 125 | $scope.url = 'not an url'; 126 | $scope.name = 'Bob'; 127 | $scope.email = 'not an email'; 128 | $scope.age = 22; 129 | 130 | $scope.$digest(); 131 | form.triggerHandler('submit'); 132 | $scope.$digest(); 133 | expect(nameMessage).toBeHidden(); 134 | expect(emailMessage).toBeVisible(); 135 | expect(urlMessage).toBeVisible(); 136 | expect(ageMessage).toBeHidden(); 137 | 138 | expect(emailMessage).toHaveText(service._getErrorMessage('email')); 139 | expect(urlMessage).toHaveText(service._getErrorMessage('url')); 140 | }); 141 | }); 142 | -------------------------------------------------------------------------------- /test/lib/jasmine-dom-matchers.js: -------------------------------------------------------------------------------- 1 | /*jsl:declare jasmine*/ 2 | /*jsl:declare Sizzle*/ 3 | /*jsl:declare Prototype*/ 4 | /*jsl:declare jQuery*/ 5 | 6 | jasmine.DOM = {}; 7 | 8 | jasmine.DOM.browserTagCaseIndependentHtml = function(html) 9 | { 10 | var div= document.createElement('div'); 11 | div.innerHTML= html; 12 | return div.innerHTML; 13 | } 14 | 15 | jasmine.DOM.elementToString = function(element) 16 | { 17 | var div= document.createElement('div'); 18 | div.appendChild(element.cloneNode(true)); 19 | return div.innerHTML; 20 | } 21 | 22 | jasmine.DOM.trim= function(string) 23 | { 24 | var str= string.replace(/^\s+/, ''); 25 | for (var i = str.length - 1; i > 0; --i) 26 | if (/\S/.test(str.charAt(i))) 27 | { 28 | str = str.substring(0, i + 1); 29 | break; 30 | } 31 | return str; 32 | } 33 | 34 | jasmine.DOM.slice= function(arrayLike, startIndex) 35 | { 36 | return [].slice.call(arrayLike, startIndex||0); 37 | } 38 | 39 | jasmine.DOM.uniqueId= 1; 40 | jasmine.DOM.assignId= function(element) 41 | { 42 | return element.id || (element.id=('jasmine_id_' + jasmine.DOM.uniqueId++)); 43 | }; 44 | 45 | /** 46 | jasmine.DOM.queryAll(selector[, scope]) -> array 47 | */ 48 | jasmine.DOM.queryAll= (function(){ 49 | if ('undefined'!==typeof(Sizzle)) 50 | return Sizzle; 51 | if ('undefined'!==typeof(Prototype)) 52 | return function(selector, node) 53 | { 54 | return Element.getElementsBySelector(node||document, selector); 55 | }; 56 | if ('undefined'!==typeof(jQuery)) 57 | return function(selector, node) 58 | { 59 | var result= jQuery(selector, node); 60 | var nodes= []; 61 | var len= result.length; 62 | 63 | for (var i=0; i 0; 162 | } 163 | }; 164 | 165 | function comparePropertyValues(actualValue, expectedValue) 166 | { 167 | if (void(0) === expectedValue) 168 | return void(0) !== actualValue; 169 | return actualValue == expectedValue; 170 | } 171 | 172 | function bindMatcher(methodName) 173 | { 174 | var originalMatcher = jasmine.Matchers.prototype[methodName]; 175 | 176 | jasmine.DOM.matchers[methodName] = function() 177 | { 178 | // If the actual value is a DOM node... 179 | if (this.actual && this.actual.nodeType) 180 | { 181 | var result = matchers[methodName].apply(this, arguments); 182 | this.actual = jasmine.DOM.elementToString(this.actual); 183 | return result; 184 | } 185 | 186 | if (originalMatcher) 187 | return originalMatcher.apply(this, arguments); 188 | 189 | return false; 190 | } 191 | 192 | } 193 | 194 | for (var methodName in matchers) 195 | bindMatcher(methodName); 196 | 197 | })(); 198 | 199 | beforeEach(function() { 200 | this.addMatchers(jasmine.DOM.matchers); 201 | }); 202 | 203 | afterEach(function() { 204 | jasmine.getFixtures().cleanUp(); 205 | }); 206 | -------------------------------------------------------------------------------- /src/helper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Gabriel_Grinberg on 11/14/14. 3 | */ 4 | (function () { 5 | 'use strict'; 6 | angular.module('gg.vmsgs') 7 | .factory('ValidationMessagesHelper', function ($compile) { 8 | 9 | var defaultErrorKeys = ['required', 'email', 'url', 'minlength', 'maxlength', 'pattern', 'min', 'max']; 10 | 11 | var defaultOptions = { 12 | hideClassName: 'ng-hide', 13 | messageClassName: 'validation-message', 14 | messageTemplate: '', 15 | showTrigger: 'blur', 16 | hideTrigger: 'valid', 17 | dummy: false, 18 | 19 | //for overriding error messages 20 | errorMessages: {}, 21 | 22 | //to support boostrap styling 23 | parentContainerClassName: 'form-group', 24 | parentErrorClassName: 'has-error', 25 | parentSuccessClassName: '' 26 | }; 27 | 28 | return { 29 | setOptions: function (opts) { 30 | defaultOptions = angular.extend({}, defaultOptions, opts || {}); 31 | if (opts) { 32 | angular.forEach(opts, function (value, key) { 33 | if (key.toUpperCase() === 'ERRORMESSAGES') { 34 | angular.forEach(opts[key], function (eValue, eKey) { 35 | if (defaultErrorKeys.indexOf(eKey) === -1) { 36 | defaultErrorKeys.push(eKey); 37 | } 38 | }); 39 | } 40 | }); 41 | } 42 | }, 43 | getOptions: function (opts, formOpts) { 44 | return angular.extend({}, defaultOptions, formOpts || {}, opts || {}); 45 | }, 46 | showTriggers: ['blur', 'submit', 'keydown', 'keyup'], 47 | hideTriggers: ['valid', 'keydown', 'keyup'], 48 | 49 | //these also define the order of rendering 50 | errorKeys: defaultErrorKeys, 51 | errorMessages: { 52 | required: { 53 | default: 'This field is required', 54 | email: 'A valid e-mail address is required', 55 | number: 'A valid number is required', 56 | date: 'A valid date is required', 57 | week: 'A valid week is required', 58 | url: 'A valid url is required', 59 | month: 'A valid month is required' 60 | }, 61 | url: 'A valid url is required', 62 | email: 'A valid e-mail address is required', 63 | minlength: 'This field must be at least {minlength} chars', 64 | maxlength: 'This field must be less than {maxlength} chars', 65 | min: { 66 | default: 'This field must be higher than {min}', 67 | number: 'This number must be higher than {min}', 68 | date: 'This date must be after {min}', 69 | week: 'This week must be after {min}', 70 | month: 'This month must be after {min}' 71 | }, 72 | max: { 73 | default: 'This field must be lower than {max}', 74 | number: 'This number must be lower than {max}', 75 | date: 'This date must be before {max}', 76 | week: 'This week must be before {max}', 77 | month: 'This month must be before {max}' 78 | }, 79 | pattern: 'This field must match a specific pattern {pattern}' 80 | }, 81 | renderError: function (msg) { 82 | var args; 83 | args = arguments; 84 | if (args.length === 2 && args[1] !== null && typeof args[1] === 'object') { 85 | args = args[1]; 86 | } 87 | return msg.replace(/{([^}]*)}/g, function (match, key) { 88 | return (typeof args[key] !== 'undefined' ? args[key] : match); 89 | }); 90 | }, 91 | _getErrorMessage: function (errorKey, inputType, params, opts) { 92 | var errorMessageFromOpts = opts && opts.errorMessages && opts.errorMessages[errorKey]; 93 | var errorMessageObject = errorMessageFromOpts || this.errorMessages[errorKey]; 94 | 95 | if (errorMessageObject) { 96 | return this.renderError(errorMessageObject[inputType] || errorMessageObject.default || errorMessageObject, params); 97 | } else { 98 | throw 'Error message not supported for type ' + errorKey + ' and inputType ' + inputType; 99 | } 100 | }, 101 | getRenderParameters: function (elem) { 102 | var params = {}; 103 | ['minlength', 'maxlength', 'min', 'max', 'pattern'].forEach(function (attr) { 104 | var calculatedAttr = elem.attr(attr) || elem.attr('ng-' + attr); 105 | if (calculatedAttr) { 106 | params[attr] = calculatedAttr; 107 | } 108 | }); 109 | return params; 110 | }, 111 | getErrorMessage: function ($error, elem, opts) { 112 | var inputType = elem.attr('type'); 113 | var selectedError = ''; 114 | var renderParameters = this.getRenderParameters(elem); 115 | this.errorKeys.forEach(function (errorKey) { 116 | if (!selectedError && $error[errorKey]) { 117 | selectedError = errorKey; 118 | } 119 | }); 120 | return this._getErrorMessage(selectedError, inputType, renderParameters, opts); 121 | }, 122 | createMessageElement: function (scope, opts) { 123 | return $compile(opts.messageTemplate)(scope) 124 | .addClass(opts.messageClassName) 125 | .addClass(opts.hideClassName); 126 | } 127 | }; 128 | }); 129 | })(); 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/GabiGrin/angular-validation-messages.svg?branch=master)](https://travis-ci.org/GabiGrin/angular-validation-messages) 2 | [![Coverage Status](https://coveralls.io/repos/GabiGrin/angular-validation-messages/badge.png?branch=master)](https://coveralls.io/r/GabiGrin/angular-validation-messages?branch=master) 3 | [![Code Climate](https://codeclimate.com/github/GabiGrin/angular-validation-messages/badges/gpa.svg)](https://codeclimate.com/github/GabiGrin/angular-validation-messages) 4 | angular-validation-messages 5 | =========================== 6 | 7 | Add validation messages to your forms in a breeze 8 | 9 | ##Demo 10 | ###[Here](http://gabigrin.github.io/angular-validation-messages/) 11 | PS: It doesn't cover everything yet, more to come. 12 | 13 | ##What it does 14 | 15 | This directive adds automatic validation messages to your forms, by just adding it to your form and fields. 16 | It also prevents your form from being submitted unless it's valid. 17 | For example: 18 | ![example image](http://s27.postimg.org/amxtpgwsz/Screen_Shot_2014_11_25_at_10_56_40_PM.png) 19 | 20 | When writing forms, a you will soon find yourself writing the same code over and over again, just to display what is invalid in the form. 21 | This directive turns this: 22 | ``` 23 |
24 | 25 |
26 | This field is required 27 | Please enter at least 20 chars 28 |
29 | 30 | 31 |
32 | Please enter a valid URL 33 |
34 | 35 | 36 |
37 | Please enter a valid number 38 |
39 |
40 | 41 | //inside controller 42 | $scope.doSomething(){ 43 | if (myForm.$valid){ 44 | //then actually do something 45 | } 46 | } 47 | ``` 48 | 49 | into this: 50 | ``` 51 |
52 | 53 | 54 | 55 |
56 | 57 | //inside controller 58 | $scope.doSomething(){ 59 | //actually do something 60 | } 61 | ``` 62 | 63 | It comes with a bunch of predefined messages for commonly used validation messages, such as email, number, minlength, url and more. _You can always override them if you think the stock messages suck!_ 64 | 65 | ##What it doesn't do 66 | It doesn't create custom validation rules, there are plenty of modules for that. 67 | It doesn't make your forms prettier, use Bootstrap/Foundation/Zimit/Custom css for that. 68 | 69 | ##How 70 | Use bower: 71 | ``` 72 | bower install angular-validation-messages 73 | ``` 74 | Or just [download](https://github.com/GabiGrin/angular-validation-messages/archive/master.zip) the files and add the following files to your html: 75 | ``` 76 | 77 | 78 | ``` 79 | Add `'gg.vmsgs'` to your app dependencies (`angular.module('yourApp', ['gg.vmsgs']`) 80 | 81 | Now just add the "vmsg-form" directive to your forms, and "vmsg" to the inputs you wish to show validation messages for. 82 | 83 | ##Features 84 | * Just-add-water form validation messages 85 | * Allows custom message templates to be used 86 | * Does not require "name" attribute to be intact 87 | * Submit only valid forms 88 | * Customize error messages 89 | * Override options either globally, per form or per control 90 | * Supports different show/hide message triggers 91 | * 100% test coverage 92 | * No jQuery used, no other dependencies - 100% AngularJS 93 | * Out of the box bootstrap parent form-group has-error support 94 | 95 | ##Overriding options 96 | To override global options, use `ValidationMessagesHelper.setOptions(yourOptions)` in a run block. 97 | vmsg-form and vmsg directive can also receive an options object. When multiple overrides are used, the most specific one "wins". 98 | 99 | ###Supported options 100 | 101 | Name | Explanation | Accepts | Default 102 | --- | --- | --- | --- 103 | showTrigger | trigger to show messages | blur/keydown/keyup/submit | blur 104 | hideTrigger | trigger to hide messages | valid/keydown/keyup | valid 105 | messageClassName | class name added to the message dom element | any string | 'validation-message' 106 | messageTemplate | html to use as the template for the messages. | any valid html, with 1 root, containing a element (which will receive the message | 107 | hideClassName | class that is added to hide messages | any classname | ng-hide 108 | parentErrorClassName | adds (or removes) a class name to a field's parent element, if it matches the 'parentContainerClassName' option. Good for using with bootstrap, where you want to add 'has-error' to the parent div.form-group | any string | 'has-error' 109 | parentSuccessClassName | adds (or removes) a class name to a field's parent element, if it matches the 'parentContainerClassName' option. Good for using with bootstrap, where you want to add 'has-success' to the parent div.form-group.| any string | '' 110 | parentContainerClassName | only if this class name is present in the parent element, parentErrorClassName and parentSuccessClassName will be applied | any string | 'form-group' 111 | errorMessages | allows you to override error messages for specific errors. The error messages passed will be combined with the default ones (using angular.extend) | any valid object | please check src/helper.js:36 112 | 113 | 114 | ##Caveats 115 | *The vmsg-form directive will add the "novalidate" attribute automatically, so the browser doesn't catch the validation issues by itself. 116 | *It doesn't support hiding the messages when the form is pristine at the moment. Will be added in the near future. 117 | *When overriding the message html template, you must have only one root element, and include the where you want the message displayed. There is no validation for this yet, so be aware. 118 | 119 | ##Contributing 120 | PR are more than welcome! 121 | Please make sure you do not brake the build, and add tests for any new features added, maintaining 100% coverage (TDD is highly recommended). 122 | To start hacking just clone the repository and run npm install + bower install, then run 'grunt serve'. 123 | 124 | -------------------------------------------------------------------------------- /test/helper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Gabriel_Grinberg on 11/14/14. 3 | */ 4 | 'use strict'; 5 | describe('helper', function () { 6 | var helper; 7 | 8 | beforeEach(function () { 9 | module('gg.vmsgs'); 10 | inject(function (ValidationMessagesHelper) { 11 | helper = ValidationMessagesHelper; 12 | }); 13 | }); 14 | 15 | it('should have a helper service', function () { 16 | expect(helper).toBeDefined(); 17 | }); 18 | 19 | it('can change default options', function () { 20 | var original = helper.getOptions(); 21 | helper.setOptions(); 22 | expect(helper.getOptions()).toEqual(original); 23 | 24 | helper.setOptions({showTrigger: 'submit'}); 25 | expect(helper.getOptions()).toEqual(angular.extend({}, original, {showTrigger: 'submit'})); 26 | }); 27 | 28 | it('should be able to render errors', function () { 29 | expect(helper.renderError('test')).toBe('test'); 30 | expect(helper.renderError('test {0} with {1}', {0: 'one', 1: 'parameters'})).toBe('test one with parameters'); 31 | expect(helper.renderError('Number must be between {min} and {max}', {min: 5, max: 10})).toBe('Number must be between 5 and 10'); 32 | expect(helper.renderError('Hello {world}', {notWorld: 'world'})).toBe('Hello {world}'); 33 | }); 34 | 35 | it('should have error messages for all known validators', function () { 36 | helper.errorKeys.forEach(function (errorKey) { 37 | expect(helper.errorMessages[errorKey]).toBeTruthy(); 38 | }); 39 | }); 40 | 41 | it('should extract render parameters from input', function () { 42 | var elem1 = angular.element(''); 43 | expect(helper.getRenderParameters(elem1)).toEqual( 44 | { 45 | min: '2', 46 | max: '4', 47 | minlength: '10', 48 | maxlength: '50', 49 | pattern: 'pat' 50 | }); 51 | }); 52 | 53 | it('should extract render parameters from input even when using ng-[attr]', function () { 54 | var elem1 = angular.element(''); 55 | expect(helper.getRenderParameters(elem1)).toEqual( 56 | { 57 | min: '2', 58 | max: '4', 59 | minlength: '10', 60 | maxlength: '50', 61 | pattern: '/pat/' 62 | }); 63 | }); 64 | 65 | it('should fall back to default when type is not provided when getting error message', function () { 66 | expect(helper._getErrorMessage('required', 'bogus')).toBe(helper._getErrorMessage('required', 'default')); 67 | expect(helper._getErrorMessage('maxlength', 'bogus2')).toBe(helper._getErrorMessage('maxlength', 'default')); 68 | }); 69 | 70 | it('should throw error when trying to get bad error messages', function () { 71 | expect(function () { 72 | helper._getErrorMessage('pumpkin'); 73 | }).toThrow(); 74 | 75 | expect(function () { 76 | helper._getErrorMessage('spongebob'); 77 | }).toThrow(); 78 | }); 79 | 80 | it('should get error messages based on error object and element', function () { 81 | var input1 = angular.element(''); 82 | var input2 = angular.element(''); 83 | var $error1 = {required: true}; 84 | var $error2 = {minlength: true}; 85 | 86 | expect(helper.getErrorMessage($error1, input1)).toBe(helper._getErrorMessage('required')); 87 | expect(helper.getErrorMessage($error2, input1)).toBe(helper._getErrorMessage('minlength', null, {minlength: 20})); 88 | expect(helper.getErrorMessage($error1, input2)).toBe(helper._getErrorMessage('required', 'number')); 89 | }); 90 | 91 | it('should prioritize error messages, and show the most important one only', function () { 92 | var input1 = angular.element(''); 93 | var $error1 = {required: true, pattern: true, minlength: true}; 94 | var $error2 = {pattern: true, minlength: true}; 95 | var $error3 = {pattern: true, someother: true}; 96 | 97 | expect(helper.getErrorMessage($error1, input1)).toBe(helper._getErrorMessage('required')); 98 | expect(helper.getErrorMessage($error2, input1)).toBe(helper._getErrorMessage('minlength', 'text', {minlength: 20})); 99 | expect(helper.getErrorMessage($error3, input1)).toBe(helper._getErrorMessage('pattern', 'text', {pattern: 'test'})); 100 | }); 101 | 102 | it('should use custom error messages if available', function () { 103 | var input1 = angular.element(''); 104 | var input2 = angular.element(''); 105 | var $error1 = {required: true}; 106 | var $error2 = {minlength: true}; 107 | var $error3 = {pattern: true}; 108 | 109 | var opts = { 110 | errorMessages: { 111 | required: { 112 | default: 'Custom msg #1', 113 | number: 'Number here!' 114 | }, 115 | minlength: 'Custom msg #2', 116 | pattern: { 117 | text: 'Text must match pattern', 118 | number: 'Numbers must be only 0-4' 119 | } 120 | } 121 | }; 122 | 123 | var opts2 = { 124 | errorMessages: { 125 | required: 'Required' 126 | } 127 | }; 128 | 129 | //should use the new custom message, because text goes to default 130 | expect(helper.getErrorMessage($error1, input1, opts)).toBe('Custom msg #1'); 131 | expect(helper.getErrorMessage($error1, input1, opts2)).toBe('Required'); 132 | expect(helper.getErrorMessage($error1, input2, opts)).toBe('Number here!'); 133 | 134 | expect(helper.getErrorMessage($error2, input1, opts)).toBe('Custom msg #2'); 135 | expect(helper.getErrorMessage($error3, input1, opts)).toBe('Text must match pattern'); 136 | expect(helper.getErrorMessage($error3, input2, opts)).toBe('Numbers must be only 0-4'); 137 | }); 138 | 139 | }) 140 | ; 141 | 142 | describe('helper', function () { 143 | var helper; 144 | var $scope; 145 | 146 | beforeEach(function () { 147 | module('gg.vmsgs'); 148 | inject(function (ValidationMessagesHelper, $rootScope) { 149 | helper = ValidationMessagesHelper; 150 | $scope = $rootScope.$new(); 151 | }); 152 | }); 153 | 154 | it('should be able to create empty message element', function () { 155 | var opts = helper.getOptions(); 156 | var messageElement = helper.createMessageElement($scope, opts); 157 | $scope.$digest(); 158 | expect(opts.messageClassName).toBeTruthy(); 159 | expect(messageElement.attr('class')).toContain(opts.messageClassName); 160 | expect(messageElement.text()).toBe(''); 161 | }); 162 | 163 | it('should add custom error key if not yet present', function () { 164 | var input1 = angular.element(''); 165 | var $error1 = {someNewErrorKey: true}; 166 | 167 | expect(helper.errorKeys.length).toBe(8); 168 | 169 | var opts = { 170 | errorMessages: { 171 | required: { 172 | default: 'Custom msg #1', 173 | number: 'Number here!' 174 | }, 175 | someNewErrorKey: 'Some new error key message' 176 | } 177 | }; 178 | 179 | helper.setOptions(opts); 180 | expect(helper.errorKeys.length).toBe(9); 181 | 182 | expect(helper.getErrorMessage($error1, input1, opts)).toBe('Some new error key message'); 183 | }); 184 | 185 | }); 186 | 187 | -------------------------------------------------------------------------------- /test/model-directive.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | describe('base usage: model directive', function () { 3 | var form, 4 | $compile, 5 | $scope, 6 | helper, 7 | body; 8 | 9 | function createElem(template, parent) { 10 | var elem = $compile(template)($scope); 11 | parent = parent || body; 12 | parent.append(elem); 13 | $scope.$digest(); 14 | return elem; 15 | } 16 | 17 | beforeEach(function () { 18 | module('gg.vmsgs'); 19 | inject(function (_$compile_, _$rootScope_, $document, _ValidationMessagesHelper_) { 20 | $compile = _$compile_; 21 | $scope = _$rootScope_.$new(); 22 | body = $document.find('body').empty(); 23 | helper = _ValidationMessagesHelper_; 24 | }); 25 | form = createElem('
'); 26 | }); 27 | 28 | it('should not work without parent form', function () { 29 | expect(function () { 30 | createElem(''); 31 | }).toThrow(); 32 | 33 | expect(function () { 34 | createElem('
'); 35 | }).not.toThrow(); 36 | }); 37 | 38 | it('should have an empty message element', function () { 39 | var form = createElem('
'); 40 | var input = form.find('input'); 41 | var messageElement = input.data('message-element'); 42 | expect(messageElement).toBeTruthy(); 43 | expect(messageElement.text()).toBe(''); 44 | expect(messageElement[0]).toBeHidden(); 45 | }); 46 | 47 | it('shows error message when item is invalid and blurred', function () { 48 | var form = createElem('
'); 49 | var input = form.find('input'); 50 | var messageElement = input.data('message-element'); 51 | expect(messageElement[0]).toBeHidden(); 52 | 53 | input.triggerHandler('blur'); 54 | $scope.$digest(); 55 | expect(messageElement[0]).toBeVisible(); 56 | 57 | $scope.test = 'something'; 58 | input.triggerHandler('blur'); 59 | $scope.$digest(); 60 | expect(messageElement[0]).toBeHidden(); 61 | }); 62 | 63 | it('hides the error message as soon as the item is valid', function () { 64 | var form = createElem('
'); 65 | var input = form.find('input'); 66 | var messageElement = input.data('message-element'); 67 | input.triggerHandler('blur'); 68 | $scope.$digest(); 69 | expect(messageElement[0]).toBeVisible(); 70 | 71 | $scope.test = 'bla bla'; 72 | $scope.$digest(); 73 | input.triggerHandler('blur'); 74 | expect(messageElement[0]).toBeHidden(); 75 | }); 76 | 77 | it('changes the error type when needed', function () { 78 | var form = createElem('
'); 79 | var input = form.find('input'); 80 | var messageElement = input.data('message-element'); 81 | $scope.$digest(); 82 | expect(messageElement.text()).toBe(helper.getErrorMessage({required: true}, input)); 83 | $scope.test = 'bla'; 84 | $scope.$digest(); 85 | expect(messageElement.text()).toBe(helper.getErrorMessage({minlength: true}, input)); 86 | }); 87 | 88 | }); 89 | 90 | describe('option overrides: model directive', function () { 91 | var $compile, 92 | $scope, 93 | helper, 94 | body; 95 | 96 | function createElem(template, parent) { 97 | var elem = $compile(template)($scope); 98 | parent = parent || body; 99 | parent.append(elem); 100 | $scope.$digest(); 101 | return elem; 102 | } 103 | 104 | beforeEach(function () { 105 | module('gg.vmsgs'); 106 | inject(function (_$compile_, _$rootScope_, $document, _ValidationMessagesHelper_) { 107 | $compile = _$compile_; 108 | $scope = _$rootScope_.$new(); 109 | body = $document.find('body').empty(); 110 | helper = _ValidationMessagesHelper_; 111 | }); 112 | }); 113 | 114 | //show triggers 115 | it('should support show keydown trigger', function () { 116 | helper.setOptions({showTrigger: 'keydown'}); 117 | var form = createElem('
'); 118 | var input = form.find('input'); 119 | var messageElement = input.data('message-element'); 120 | 121 | $scope.test = 'something'; 122 | $scope.$digest(); 123 | $scope.test = ''; 124 | $scope.$digest(); 125 | input.triggerHandler('keydown'); 126 | $scope.$digest(); 127 | expect(messageElement[0]).toBeVisible(); 128 | }); 129 | 130 | it('should support show keyup trigger', function () { 131 | helper.setOptions({showTrigger: 'keyup'}); 132 | var form = createElem('
'); 133 | var input = form.find('input'); 134 | var messageElement = input.data('message-element'); 135 | 136 | $scope.test = 'something'; 137 | $scope.$digest(); 138 | $scope.test = ''; 139 | $scope.$digest(); 140 | input.triggerHandler('keyup'); 141 | $scope.$digest(); 142 | expect(messageElement[0]).toBeVisible(); 143 | }); 144 | 145 | it('should support show submit trigger', function () { 146 | helper.setOptions({showTrigger: 'submit'}); 147 | 148 | var form = createElem('
'); 149 | var input = form.find('input'); 150 | var messageElement = input.data('message-element'); 151 | 152 | $scope.test = ''; 153 | $scope.$digest(); 154 | form.triggerHandler('submit'); 155 | $scope.$digest(); 156 | expect(messageElement[0]).toBeVisible(); 157 | }); 158 | 159 | it('throws error when unsupported show trigger is used', function () { 160 | helper.setOptions({showTrigger: 'unsupported'}); 161 | expect(function () { 162 | createElem('
'); 163 | }).toThrow(); 164 | }); 165 | 166 | it('should support change hide trigger', function () { 167 | helper.setOptions({hideTrigger: 'keydown'}); 168 | var form = createElem('
'); 169 | var input = form.find('input'); 170 | var messageElement = input.data('message-element'); 171 | 172 | input.triggerHandler('blur'); 173 | $scope.$digest(); 174 | expect(messageElement[0]).toBeVisible(); 175 | 176 | input.triggerHandler('keydown'); 177 | $scope.$digest(); 178 | expect(messageElement[0]).toBeHidden(); 179 | }); 180 | 181 | it('throws error when unsupported hide trigger is used', function () { 182 | helper.setOptions({hideTrigger: 'unsupported'}); 183 | expect(function () { 184 | createElem('
'); 185 | }).toThrow(); 186 | }); 187 | 188 | it('supports changing template via helper', function () { 189 | helper.setOptions({messageTemplate: '{{errorMessage}}'}); 190 | var form = createElem('
'); 191 | var input = form.find('input'); 192 | var messageElement = input.data('message-element'); 193 | 194 | expect(messageElement[0].tagName).toBe('A'); 195 | // expect(messageElement.is('a')).toBeTruthy(); 196 | }); 197 | 198 | it('supports changing template via input', function () { 199 | var form = createElem('
'); 200 | var input = form.find('input'); 201 | var messageElement = input.data('message-element'); 202 | expect(messageElement[0].tagName).toBe('A'); 203 | }); 204 | 205 | it('supports changing template via form', function () { 206 | var form = createElem('
'); 207 | var input = form.find('input'); 208 | var messageElement = input.data('message-element'); 209 | expect(messageElement[0].tagName).toBe('A'); 210 | }); 211 | 212 | it('will get first input settings, if form settings are also given', function () { 213 | var form = createElem('
'); 214 | var input = form.find('input'); 215 | var messageElement = input.data('message-element'); 216 | expect(messageElement[0].tagName).toBe('P'); 217 | }); 218 | 219 | it('supports parent class changing (for bootstrap) on error', function () { 220 | var form = createElem( 221 | '
' + 222 | '
' + 223 | '' + 224 | '
' + 225 | '
'); 226 | var formGroup = form.find('div'); 227 | var input = form.find('input'); 228 | var opts = helper.getOptions(); 229 | 230 | input.triggerHandler('blur'); 231 | $scope.$digest(); 232 | expect(formGroup.hasClass(opts.parentErrorClassName)).toBeTruthy(); 233 | 234 | $scope.test = 'one two three'; 235 | $scope.$digest(); 236 | expect(formGroup.hasClass(opts.parentErrorClassName)).toBeFalsy(); 237 | }); 238 | 239 | it('supports parent class changing (for bootstrap) on success', function () { 240 | var successClassName = 'has-success'; 241 | var opts = $scope.opts = { 242 | parentSuccessClassName: successClassName 243 | }; 244 | var form = createElem( 245 | '
' + 246 | '
' + 247 | '' + 248 | '
' + 249 | '
'); 250 | var formGroup = form.find('div'); 251 | var input = form.find('input'); 252 | 253 | expect(formGroup.hasClass(opts.parentSuccessClassName)).toBeFalsy(); 254 | 255 | input.triggerHandler('blur'); 256 | $scope.$digest(); 257 | expect(formGroup.hasClass(opts.parentSuccessClassName)).toBeFalsy(); 258 | 259 | $scope.test = 'one two three'; 260 | $scope.$digest(); 261 | input.triggerHandler('blur'); 262 | expect(formGroup.hasClass(opts.parentSuccessClassName)).toBeTruthy(); 263 | 264 | $scope.test = ''; 265 | input.triggerHandler('blur'); 266 | $scope.$digest(); 267 | expect(formGroup.hasClass(opts.parentSuccessClassName)).toBeFalsy(); 268 | }); 269 | 270 | it('should support custom error messages', function () { 271 | var form = createElem('
' + 272 | '' + 273 | '
'); 274 | var input = form.find('input'); 275 | var messageElement = input.data('message-element'); 276 | $scope.$digest(); 277 | input.triggerHandler('blur'); 278 | $scope.$digest(); 279 | expect(messageElement.text()).toBe('Must fill this one!'); 280 | }); 281 | 282 | }); 283 | -------------------------------------------------------------------------------- /dist/angular-validation-messages.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | angular.module('gg.vmsgs', []); 3 | ;(function () { 4 | 'use strict'; 5 | angular.module('gg.vmsgs') 6 | .directive('vmsgForm', ['$parse', function ($parse) { 7 | return { 8 | restrict: 'A', 9 | require: 'form', 10 | priority: -1, 11 | compile: function () { 12 | return { 13 | pre: function preLink(scope, elem, attrs) { 14 | 15 | scope.formOpts = $parse(attrs.vmsgForm || '')(scope); 16 | elem.attr('novalidate', 'novalidate'); 17 | scope.$watch(function () { 18 | return attrs.vmsgForm; 19 | }, function (opts) { 20 | scope.formOpts = $parse(opts || '')(scope); 21 | }); 22 | }, 23 | post: function postLink(scope, elem, attrs, formCtrl) { 24 | elem.bind('submit', function (e) { 25 | if (formCtrl.$invalid) { 26 | e.stopImmediatePropagation(); 27 | scope.$broadcast('submit'); 28 | } 29 | }); 30 | } 31 | }; 32 | }, 33 | controller: ['$scope', function ($scope) { 34 | this.getOptions = function () { 35 | return $scope.formOpts; 36 | }; 37 | }] 38 | }; 39 | }]); 40 | })(); 41 | 42 | ;/** 43 | * Created by Gabriel_Grinberg on 11/14/14. 44 | */ 45 | (function () { 46 | 'use strict'; 47 | angular.module('gg.vmsgs') 48 | .factory('ValidationMessagesHelper', ['$compile', function ($compile) { 49 | 50 | var defaultErrorKeys = ['required', 'email', 'url', 'minlength', 'maxlength', 'pattern', 'min', 'max']; 51 | 52 | var defaultOptions = { 53 | hideClassName: 'ng-hide', 54 | messageClassName: 'validation-message', 55 | messageTemplate: '', 56 | showTrigger: 'blur', 57 | hideTrigger: 'valid', 58 | dummy: false, 59 | 60 | //for overriding error messages 61 | errorMessages: {}, 62 | 63 | //to support boostrap styling 64 | parentContainerClassName: 'form-group', 65 | parentErrorClassName: 'has-error', 66 | parentSuccessClassName: '' 67 | }; 68 | 69 | return { 70 | setOptions: function (opts) { 71 | defaultOptions = angular.extend({}, defaultOptions, opts || {}); 72 | if (opts) { 73 | angular.forEach(opts, function (value, key) { 74 | if (key.toUpperCase() === 'ERRORMESSAGES') { 75 | angular.forEach(opts[key], function (eValue, eKey) { 76 | if (defaultErrorKeys.indexOf(eKey) === -1) { 77 | defaultErrorKeys.push(eKey); 78 | } 79 | }); 80 | } 81 | }); 82 | } 83 | }, 84 | getOptions: function (opts, formOpts) { 85 | return angular.extend({}, defaultOptions, formOpts || {}, opts || {}); 86 | }, 87 | showTriggers: ['blur', 'submit', 'keydown', 'keyup'], 88 | hideTriggers: ['valid', 'keydown', 'keyup'], 89 | 90 | //these also define the order of rendering 91 | errorKeys: defaultErrorKeys, 92 | errorMessages: { 93 | required: { 94 | default: 'This field is required', 95 | email: 'A valid e-mail address is required', 96 | number: 'A valid number is required', 97 | date: 'A valid date is required', 98 | week: 'A valid week is required', 99 | url: 'A valid url is required', 100 | month: 'A valid month is required' 101 | }, 102 | url: 'A valid url is required', 103 | email: 'A valid e-mail address is required', 104 | minlength: 'This field must be at least {minlength} chars', 105 | maxlength: 'This field must be less than {maxlength} chars', 106 | min: { 107 | default: 'This field must be higher than {min}', 108 | number: 'This number must be higher than {min}', 109 | date: 'This date must be after {min}', 110 | week: 'This week must be after {min}', 111 | month: 'This month must be after {min}' 112 | }, 113 | max: { 114 | default: 'This field must be lower than {max}', 115 | number: 'This number must be lower than {max}', 116 | date: 'This date must be before {max}', 117 | week: 'This week must be before {max}', 118 | month: 'This month must be before {max}' 119 | }, 120 | pattern: 'This field must match a specific pattern {pattern}' 121 | }, 122 | renderError: function (msg) { 123 | var args; 124 | args = arguments; 125 | if (args.length === 2 && args[1] !== null && typeof args[1] === 'object') { 126 | args = args[1]; 127 | } 128 | return msg.replace(/{([^}]*)}/g, function (match, key) { 129 | return (typeof args[key] !== 'undefined' ? args[key] : match); 130 | }); 131 | }, 132 | _getErrorMessage: function (errorKey, inputType, params, opts) { 133 | var errorMessageFromOpts = opts && opts.errorMessages && opts.errorMessages[errorKey]; 134 | var errorMessageObject = errorMessageFromOpts || this.errorMessages[errorKey]; 135 | 136 | if (errorMessageObject) { 137 | return this.renderError(errorMessageObject[inputType] || errorMessageObject.default || errorMessageObject, params); 138 | } else { 139 | throw 'Error message not supported for type ' + errorKey + ' and inputType ' + inputType; 140 | } 141 | }, 142 | getRenderParameters: function (elem) { 143 | var params = {}; 144 | ['minlength', 'maxlength', 'min', 'max', 'pattern'].forEach(function (attr) { 145 | var calculatedAttr = elem.attr(attr) || elem.attr('ng-' + attr); 146 | if (calculatedAttr) { 147 | params[attr] = calculatedAttr; 148 | } 149 | }); 150 | return params; 151 | }, 152 | getErrorMessage: function ($error, elem, opts) { 153 | var inputType = elem.attr('type'); 154 | var selectedError = ''; 155 | var renderParameters = this.getRenderParameters(elem); 156 | this.errorKeys.forEach(function (errorKey) { 157 | if (!selectedError && $error[errorKey]) { 158 | selectedError = errorKey; 159 | } 160 | }); 161 | return this._getErrorMessage(selectedError, inputType, renderParameters, opts); 162 | }, 163 | createMessageElement: function (scope, opts) { 164 | return $compile(opts.messageTemplate)(scope) 165 | .addClass(opts.messageClassName) 166 | .addClass(opts.hideClassName); 167 | } 168 | }; 169 | }]); 170 | })(); 171 | ;/** 172 | * Created by Gabriel_Grinberg on 11/15/14. 173 | */ 174 | (function () { 175 | 'use strict'; 176 | angular.module('gg.vmsgs') 177 | .directive('vmsg', ['ValidationMessagesHelper', '$parse', function (ValidationMessagesHelper, $parse) { 178 | return { 179 | require: ['ngModel', '^vmsgForm'], 180 | restrict: 'A', 181 | compile: function () { 182 | return function postLink(scope, elem, attrs, ctrls) { 183 | var localOpts = $parse(attrs.vmsg || '')(scope); 184 | var formOpts = ctrls[1].getOptions(); 185 | var opts = ValidationMessagesHelper.getOptions(localOpts, formOpts); 186 | var messageElem = ValidationMessagesHelper.createMessageElement(scope, opts); 187 | var ngModelCtrl = ctrls[0]; 188 | var elemParent = elem.parent(); 189 | 190 | var showStatusMessage = function () { 191 | if (ngModelCtrl.$invalid) { 192 | window.ngModelCtrl = ngModelCtrl; 193 | messageElem.removeClass(opts.hideClassName); 194 | if (elem.parent().hasClass(opts.parentContainerClassName)) { 195 | elemParent.addClass(opts.parentErrorClassName); 196 | } 197 | } else { 198 | if (elemParent.hasClass(opts.parentContainerClassName)) { 199 | elemParent.addClass(opts.parentSuccessClassName); 200 | } 201 | } 202 | }; 203 | 204 | var hideMessage = function () { 205 | messageElem.addClass(opts.hideClassName); 206 | if (elemParent.hasClass(opts.parentContainerClassName)) { 207 | elemParent.removeClass(opts.parentErrorClassName); 208 | elemParent.removeClass(opts.parentSuccessClassName); 209 | } 210 | }; 211 | 212 | elem.data('message-element', messageElem); 213 | elem.after(messageElem); 214 | 215 | //set up show message trigger 216 | switch (opts.showTrigger) { 217 | case 'blur': 218 | case 'keydown': 219 | case 'keyup': 220 | elem.on(opts.showTrigger, function () { 221 | showStatusMessage(); 222 | }); 223 | break; 224 | case 'submit': 225 | //we always show the errors when its submitted.. this option is for clarification only 226 | break; 227 | default: 228 | throw 'Show trigger "' + opts.showTrigger + '" is not supported'; 229 | } 230 | 231 | //we'll always show 232 | scope.$on('submit', function () { 233 | showStatusMessage(); 234 | }); 235 | 236 | //set up hide message trigger 237 | switch (opts.hideTrigger) { 238 | case 'valid': 239 | scope.$watch(function () { 240 | return ngModelCtrl.$valid; 241 | }, function (isValid, wasValid) { 242 | if (isValid !== wasValid) { 243 | hideMessage(); 244 | } 245 | }); 246 | break; 247 | case 'keydown': 248 | case 'keyup': 249 | elem.on(opts.hideTrigger, function () { 250 | hideMessage(); 251 | }); 252 | break; 253 | default: 254 | throw 'Unsupported hide trigger used'; 255 | } 256 | 257 | //each time the error changes, update the error message 258 | scope.$watch(function () { 259 | return ngModelCtrl.$error; 260 | }, function (newError) { 261 | if (ngModelCtrl.$invalid) { 262 | messageElem.find('msg').text(ValidationMessagesHelper.getErrorMessage(newError, elem, opts)); 263 | } 264 | }, true); 265 | 266 | }; 267 | } 268 | }; 269 | }]); 270 | })(); 271 | -------------------------------------------------------------------------------- /coverage/PhantomJS 1.9.8 (Mac OS X)/src/time-heatmap/time-directive.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for src/time-heatmap/time-directive.js 5 | 6 | 7 | 8 | 9 | 196 | 197 | 198 |
199 |

Code coverage report for src/time-heatmap/time-directive.js

200 |

201 | 202 | Statements: 100% (0 / 0)      203 | 204 | 205 | Branches: 100% (0 / 0)      206 | 207 | 208 | Functions: 100% (0 / 0)      209 | 210 | 211 | Lines: 100% (0 / 0)      212 | 213 | Ignored: none      214 |

215 |
All files » src/time-heatmap/ » time-directive.js
216 |
217 |
218 |

219 | 
220 | 
1 
 
221 | 222 |
223 | 226 | 227 | 228 | 229 | 230 | 321 | 322 | 323 | -------------------------------------------------------------------------------- /coverage/PhantomJS 1.9.8 (Mac OS X)/src/module.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for src/module.js 5 | 6 | 7 | 8 | 9 | 196 | 197 | 198 |
199 |

Code coverage report for src/module.js

200 |

201 | 202 | Statements: 100% (1 / 1)      203 | 204 | 205 | Branches: 100% (0 / 0)      206 | 207 | 208 | Functions: 100% (0 / 0)      209 | 210 | 211 | Lines: 100% (1 / 1)      212 | 213 | Ignored: none      214 |

215 |
All files » src/ » module.js
216 |
217 |
218 |

219 | 
226 | 
1 220 | 2 221 | 3  222 | 1 223 |  
'use strict';
224 | angular.module('gg.vmsgs', []);
225 |  
227 | 228 |
229 | 232 | 233 | 234 | 235 | 236 | 327 | 328 | 329 | -------------------------------------------------------------------------------- /coverage/PhantomJS 1.9.8 (Mac OS X)/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for All files 5 | 6 | 7 | 8 | 9 | 196 | 197 | 198 |
199 |

Code coverage report for All files

200 |

201 | 202 | Statements: 100% (92 / 92)      203 | 204 | 205 | Branches: 100% (56 / 56)      206 | 207 | 208 | Functions: 100% (35 / 35)      209 | 210 | 211 | Lines: 100% (92 / 92)      212 | 213 | Ignored: none      214 |

215 |
216 |
217 |
218 |
219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 |
FileStatementsBranchesFunctionsLines
src/100%(92 / 92)100%(56 / 56)100%(35 / 35)100%(92 / 92)
249 |
250 |
251 | 254 | 255 | 256 | 257 | 258 | 349 | 350 | 351 | -------------------------------------------------------------------------------- /coverage/PhantomJS 1.9.8 (Mac OS X)/src/directive.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for src/directive.js 5 | 6 | 7 | 8 | 9 | 196 | 197 | 198 |
199 |

Code coverage report for src/directive.js

200 |

201 | 202 | Statements: 100% (9 / 9)      203 | 204 | 205 | Branches: 100% (2 / 2)      206 | 207 | 208 | Functions: 100% (4 / 4)      209 | 210 | 211 | Lines: 100% (9 / 9)      212 | 213 | Ignored: none      214 |

215 |
All files » src/ » directive.js
216 |
217 |
218 |

219 | 
289 | 
1 220 | 2 221 | 3 222 | 4 223 | 5 224 | 6 225 | 7 226 | 8 227 | 9 228 | 10 229 | 11 230 | 12 231 | 13 232 | 14 233 | 15 234 | 16 235 | 17 236 | 18 237 | 19 238 | 20 239 | 21 240 | 22 241 | 23 242 | 241 243 |   244 | 1 245 | 2 246 |   247 |   248 |   249 |   250 | 1 251 | 2 252 | 1 253 | 1 254 | 1 255 |   256 |   257 |   258 |   259 |   260 |   261 | 1 262 |   263 |   264 |   265 |  
(function () {
266 |   'use strict';
267 |   function Yavd() {
268 |     return {
269 |       restrict: 'A',
270 |       require: 'form',
271 |       priority: -1,
272 |       link: function (scope, elem, attrs, formCtrl) {
273 |         elem.bind('submit', function (e){
274 |           if (formCtrl.$invalid) {
275 |             e.stopImmediatePropagation();
276 |             window.formCtrl = formCtrl;
277 |             debugger;
278 |           }
279 |         });
280 |       }
281 |     };
282 |   }
283 |  
284 |   angular.module('gg.yavd')
285 |     .directive('yavd', Yavd);
286 | })();
287 |  
288 |  
290 | 291 |
292 | 295 | 296 | 297 | 298 | 299 | 390 | 391 | 392 | -------------------------------------------------------------------------------- /coverage/PhantomJS 1.9.8 (Mac OS X)/src/heatmap/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for src/heatmap/ 5 | 6 | 7 | 8 | 9 | 196 | 197 | 198 |
199 |

Code coverage report for src/heatmap/

200 |

201 | 202 | Statements: 59.03% (85 / 144)      203 | 204 | 205 | Branches: 58.82% (40 / 68)      206 | 207 | 208 | Functions: 61.76% (21 / 34)      209 | 210 | 211 | Lines: 59.03% (85 / 144)      212 | 213 | Ignored: none      214 |

215 |
All files » src/heatmap/
216 |
217 |
218 |
219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 |
FileStatementsBranchesFunctionsLines
directive.js11.76%(2 / 17)0%(0 / 6)0%(0 / 7)11.76%(2 / 17)
service.js65.35%(83 / 127)64.52%(40 / 62)77.78%(21 / 27)65.35%(83 / 127)
262 |
263 |
264 | 267 | 268 | 269 | 270 | 271 | 362 | 363 | 364 | -------------------------------------------------------------------------------- /coverage/PhantomJS 1.9.8 (Mac OS X)/src/time-heatmap/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for src/time-heatmap/ 5 | 6 | 7 | 8 | 9 | 196 | 197 | 198 |
199 |

Code coverage report for src/time-heatmap/

200 |

201 | 202 | Statements: 68.42% (13 / 19)      203 | 204 | 205 | Branches: 70% (7 / 10)      206 | 207 | 208 | Functions: 80% (4 / 5)      209 | 210 | 211 | Lines: 68.42% (13 / 19)      212 | 213 | Ignored: none      214 |

215 |
All files » src/time-heatmap/
216 |
217 |
218 |
219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 |
FileStatementsBranchesFunctionsLines
time-directive.js100%(0 / 0)100%(0 / 0)100%(0 / 0)100%(0 / 0)
time-service.js68.42%(13 / 19)70%(7 / 10)80%(4 / 5)68.42%(13 / 19)
262 |
263 |
264 | 267 | 268 | 269 | 270 | 271 | 362 | 363 | 364 | -------------------------------------------------------------------------------- /coverage/PhantomJS 1.9.8 (Mac OS X)/src/form-directive.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for src/form-directive.js 5 | 6 | 7 | 8 | 9 | 196 | 197 | 198 |
199 |

Code coverage report for src/form-directive.js

200 |

201 | 202 | Statements: 100% (15 / 15)      203 | 204 | 205 | Branches: 100% (6 / 6)      206 | 207 | 208 | Functions: 100% (10 / 10)      209 | 210 | 211 | Lines: 100% (15 / 15)      212 | 213 | Ignored: none      214 |

215 |
All files » src/ » form-directive.js
216 |
217 |
218 |

219 | 
340 | 
1 220 | 2 221 | 3 222 | 4 223 | 5 224 | 6 225 | 7 226 | 8 227 | 9 228 | 10 229 | 11 230 | 12 231 | 13 232 | 14 233 | 15 234 | 16 235 | 17 236 | 18 237 | 19 238 | 20 239 | 21 240 | 22 241 | 23 242 | 24 243 | 25 244 | 26 245 | 27 246 | 28 247 | 29 248 | 30 249 | 31 250 | 32 251 | 33 252 | 34 253 | 35 254 | 36 255 | 37 256 | 38 257 | 39 258 | 40 259 | 411 260 |   261 | 1 262 | 20 263 |   264 |   265 |   266 |   267 | 26 268 |   269 |   270 | 24 271 | 24 272 | 112 273 |   274 | 22 275 |   276 |   277 |   278 | 22 279 | 4 280 | 3 281 | 3 282 |   283 |   284 |   285 |   286 |   287 |   288 | 26 289 | 18 290 |   291 |   292 |   293 |   294 |   295 | 1 296 |   297 |   298 |   299 |  
(function () {
300 |   'use strict';
301 |   function ValidationMessagesForm($parse) {
302 |     return {
303 |       restrict: 'A',
304 |       require: 'form',
305 |       priority: -1,
306 |       compile: function () {
307 |         return {
308 |           pre: function preLink(scope, elem, attrs) {
309 |  
310 |             scope.formOpts = $parse(attrs.vmsgForm || '')(scope);
311 |             scope.$watch(function () {
312 |               return attrs.vmsgForm;
313 |             }, function (opts){
314 |               scope.formOpts = $parse(opts || '')(scope);
315 |             });
316 |           },
317 |           post: function postLink(scope, elem, attrs, formCtrl) {
318 |             elem.bind('submit', function (e) {
319 |               if (formCtrl.$invalid) {
320 |                 e.stopImmediatePropagation();
321 |                 scope.$broadcast('submit');
322 |               }
323 |             });
324 |           }
325 |         };
326 |       },
327 |       controller: function ($scope) {
328 |         this.getOptions = function () {
329 |           return $scope.formOpts;
330 |         };
331 |       }
332 |     };
333 |   }
334 |  
335 |   angular.module('gg.vmsgs')
336 |     .directive('vmsgForm', ['$parse', ValidationMessagesForm]);
337 | })();
338 |  
339 |  
341 | 342 |
343 | 346 | 347 | 348 | 349 | 350 | 441 | 442 | 443 | -------------------------------------------------------------------------------- /coverage/PhantomJS 1.9.8 (Mac OS X)/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for src/ 5 | 6 | 7 | 8 | 9 | 196 | 197 | 198 |
199 |

Code coverage report for src/

200 |

201 | 202 | Statements: 100% (92 / 92)      203 | 204 | 205 | Branches: 100% (56 / 56)      206 | 207 | 208 | Functions: 100% (35 / 35)      209 | 210 | 211 | Lines: 100% (92 / 92)      212 | 213 | Ignored: none      214 |

215 |
All files » src/
216 |
217 |
218 |
219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 |
FileStatementsBranchesFunctionsLines
form-directive.js100%(15 / 15)100%(6 / 6)100%(10 / 10)100%(15 / 15)
helper.js100%(32 / 32)100%(31 / 31)100%(12 / 12)100%(32 / 32)
model-directive.js100%(44 / 44)100%(19 / 19)100%(13 / 13)100%(44 / 44)
module.js100%(1 / 1)100%(0 / 0)100%(0 / 0)100%(1 / 1)
288 |
289 |
290 | 293 | 294 | 295 | 296 | 297 | 388 | 389 | 390 | -------------------------------------------------------------------------------- /coverage/PhantomJS 1.9.8 (Mac OS X)/prettify.js: -------------------------------------------------------------------------------- 1 | window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); 2 | --------------------------------------------------------------------------------