├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── Gruntfile.js ├── LICENSE ├── README.md ├── bower.json ├── build.json ├── dist ├── macgyver.css ├── macgyver.js └── macgyver.min.js ├── docs ├── data.json ├── doc.js └── vendor.css ├── eslint.json ├── jsdoc.json ├── misc ├── grunt │ ├── bump.js │ ├── contrib-concat.js │ ├── contrib-connect.js │ ├── contrib-copy.js │ ├── contrib-stylus.js │ ├── contrib-uglify.js │ ├── contrib-watch.js │ ├── coveralls.js │ ├── eslint.js │ ├── jsdoc.js │ ├── karma.js │ ├── protractor.js │ ├── replace.js │ └── tasks.js ├── saucelabs │ ├── start_tunnel.sh │ └── teardown_tunnel.sh └── travis │ ├── before_build.sh │ ├── print_logs.sh │ ├── run.sh │ └── wait_for_browser_provider.sh ├── package.json ├── src ├── constants.js ├── controllers │ ├── affix.js │ ├── autocomplete.js │ └── tag_autocomplete.js ├── css │ ├── _variables.styl │ ├── common.styl │ ├── menu.styl │ ├── modal.styl │ ├── popover.styl │ ├── spinner.styl │ ├── tag_autocomplete.styl │ └── tooltip.styl ├── directives │ ├── affix.js │ ├── autocomplete.js │ ├── events │ │ ├── keydown.js │ │ ├── pause_typing.js │ │ └── window_resize.js │ ├── keys.js │ ├── menu.js │ ├── modal.js │ ├── placeholder.js │ ├── popover.js │ ├── scroll_spy.js │ ├── spinner.js │ ├── tag_autocomplete.js │ ├── time.js │ └── tooltip.js ├── filters │ ├── boolean.js │ ├── list.js │ ├── pluralize.js │ └── timestamp.js ├── main.js ├── services │ ├── modal.js │ ├── popover.js │ ├── scroll_spy.js │ └── time_util.js ├── template │ ├── menu.html │ └── tag_autocomplete.html └── util.js └── test ├── e2e ├── modal.html ├── modal.spec.js ├── time.html └── time.spec.js ├── karma.conf.js ├── protractor.conf.js ├── unit ├── affix.spec.js ├── autocomplete.spec.js ├── events.keydown.spec.js ├── events.pause_typing.spec.js ├── events.window_resize.spec.js ├── filters.boolean.spec.js ├── filters.list.spec.js ├── filters.pluralize.spec.js ├── filters.timestamp.spec.js ├── main.spec.js ├── menu.spec.js ├── modal.spec.js ├── placeholder.spec.js ├── popover.spec.js ├── scroll_spy.spec.js ├── services │ ├── popover.spec.js │ └── time_util.spec.js ├── spinner.spec.js ├── tag_autocomplete.spec.js ├── time.spec.js ├── tooltip.spec.js └── util.spec.js └── vendor └── browserTrigger.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Node packages 2 | node_modules 3 | 4 | # Bower vendor files 5 | bower_components 6 | 7 | # Build files 8 | build/ 9 | .gitmodules 10 | .grunt/ 11 | coverage/ 12 | out/ 13 | lib/ 14 | 15 | # Logs 16 | npm-debug.log 17 | 18 | # System file types 19 | .DS_Store 20 | .c9 21 | yarn.lock 22 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | build/ 2 | lib/ 3 | misc/ 4 | out/ 5 | src/ 6 | test/ 7 | .travis.yml 8 | build.json 9 | eslint.json 10 | Gruntfile.js 11 | init-project.sh 12 | jsdoc.json 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - 6.2 5 | 6 | cache: 7 | directories: 8 | - node_modules 9 | 10 | env: 11 | matrix: 12 | - JOB=unit 13 | - JOB=e2e BROWSER=chrome 14 | - JOB=e2e BROWSER=firefox VERSION=34 15 | global: 16 | - LOGS_DIR: /tmp/sauce-build/logs 17 | - SAUCE_USERNAME: macgyver-ci 18 | - SAUCE_ACCESS_KEY: 5aa3c19a-1374-439d-8777-cfd7521eb740 19 | - BROWSER_PROVIDER_READY_FILE: /tmp/browsersprovider-tunnel-ready 20 | before_script: 21 | - ./misc/travis/before_build.sh 22 | script: 23 | - ./misc/travis/run.sh 24 | after_script: 25 | - ./misc/saucelabs/teardown_tunnel.sh 26 | - ./misc/travis/print_logs.sh 27 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks); 3 | 4 | grunt.initConfig({ 5 | pkg: grunt.file.readJSON("package.json"), 6 | bower: grunt.file.readJSON("bower.json"), 7 | buildConf: grunt.file.readJSON("build.json") 8 | }); 9 | 10 | grunt.loadTasks("misc/grunt"); 11 | 12 | require('time-grunt')(grunt); 13 | 14 | grunt.registerTask("compile", [ 15 | "stylus", 16 | "concat:lib", 17 | "replace:src" 18 | ]); 19 | 20 | grunt.registerTask("deploy", "Build and copy to lib/", [ 21 | // bump, generate changelog 22 | "compile", 23 | "karma:build", 24 | "copy:dist", 25 | "uglify:dist", 26 | "doc" 27 | ]); 28 | 29 | grunt.registerTask("run", "Watch src and run test server", [ 30 | "compile", 31 | "eslint", 32 | "karma:unit", 33 | "doc", 34 | "connect:doc", 35 | "watch" 36 | ]); 37 | 38 | grunt.registerTask("test:ci", [ 39 | "eslint", 40 | "karma:travis", 41 | "coveralls" 42 | ]); 43 | 44 | grunt.registerTask("test:e2e", "Compile all source code, run a test server and run the end to end tests", [ 45 | "compile", 46 | "connect:e2e", 47 | "protractor:normal" 48 | ]); 49 | 50 | grunt.registerTask('doc', [ 51 | 'jsdoc:doc', 52 | 'copy:css', 53 | 'copy:doc' 54 | ]) 55 | }; 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 Adrian Lee 2 | http://angular-macgyver.github.io/MacGyver 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MacGyver 2 | 3 | Duct Tape and a Swiss Army Knife. Angular helpers for all your friends! 4 | 5 | [![Build Status](http://img.shields.io/travis/angular-macgyver/MacGyver/master.svg?style=flat-square)](https://travis-ci.org/angular-macgyver/MacGyver) 6 | [![Coverage Status](https://img.shields.io/coveralls/angular-macgyver/MacGyver/master.svg?style=flat-square)](https://coveralls.io/r/angular-macgyver/MacGyver?branch=master) 7 | [![License](http://img.shields.io/badge/license-MIT-green.svg?style=flat-square)](https://github.com/angular-macgyver/MacGyver/blob/master/LICENSE) 8 | [![Latest Release](http://img.shields.io/github/release/angular-macgyver/MacGyver.svg?style=flat-square)](https://github.com/angular-macgyver/MacGyver/releases/latest) 9 | 10 | [![Selenium Test Status](https://saucelabs.com/browser-matrix/macgyver-ci.svg)](https://saucelabs.com/u/macgyver-ci) 11 | 12 | ## Components ## 13 | 14 | ### Directives ### 15 | - Affix 16 | - Autocomplete 17 | - Canvas Spinner 18 | - Events 19 | - Menu 20 | - Modal 21 | - Placeholder 22 | - Popover 23 | - Scroll Spy 24 | - Spinner 25 | - Tag Autocomplete 26 | - Time Input 27 | - Tooltip 28 | 29 | ### Filters ### 30 | - Boolean 31 | - Pluralize 32 | - Timestamp 33 | 34 | ## 3rd party libraries dependencies ## 35 | - AngularJS (1.2.x+) 36 | 37 | ## Getting started 38 | - NPM: `npm install angular-macgyver --save` 39 | - Bower: `bower install angular-macgyver` 40 | - Yarn: `yarn add angular-macgyver` 41 | - Download from [Github](https://github.com/angular-macgyver/MacGyver/archive/master.zip) 42 | 43 | Once you have MacGyver in your project, just include "Mac" as a dependency in your Angular application and you’re good to go. 44 | 45 | ```javascript 46 | angular.module("myModule", ["Mac"]); 47 | ``` 48 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MacGyver", 3 | "version": "1.0.0", 4 | "main": ["dist/macgyver.css", "dist/macgyver.js"], 5 | "ignore": [ 6 | "build", 7 | "lib", 8 | "misc", 9 | "out", 10 | "src", 11 | "test", 12 | ".travis.yml", 13 | "build.json", 14 | "eslint.json", 15 | "Gruntfile.js", 16 | "init-project.sh", 17 | "jsdoc.json" 18 | ], 19 | "devDependencies": { 20 | "angular-mocks": "~1.4.0" 21 | }, 22 | "dependencies": { 23 | "angular": "~1.4.0", 24 | "angular-animate": "~1.4.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /build.json: -------------------------------------------------------------------------------- 1 | { 2 | "full": [ 3 | "src/main.js", 4 | "src/**/*.js" 5 | ], 6 | "css": { 7 | "core": [ 8 | "src/css/*.styl", 9 | "!src/css/_*.styl" 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /dist/macgyver.css: -------------------------------------------------------------------------------- 1 | .mac-date-time.ng-invalid-date,.mac-date-time.ng-invalid-time{background:#ffe2e2;border:solid 1px #dea9a9}.close,.close-modal{width:18px;height:18px;border-radius:9px;display:block;background:#bdc0c7;position:relative;opacity:1;}.close:after,.close-modal:after{content:'';display:block;width:2px;height:10px;background:#fff;position:absolute;top:4px;left:8px;transform:rotate(45deg)}.close:before,.close-modal:before{content:'';display:block;width:2px;height:10px;background:#fff;position:absolute;top:4px;left:8px;transform:rotate(-45deg)}.close:hover,.close-modal:hover{background:#828590}.hide{display:none}.affix{position:fixed;}.affix-bottom{position:absolute} 2 | .mac-menu{position:absolute;top:0;left:0;min-width:200px;background:#fff;box-shadow:0 3px 6px rgba(0,0,0,0.2);border:1px solid #d1d3d8;box-sizing:border-box;max-height:325px;overflow-y:auto;opacity:0;visibility:hidden;transition:opacity .1s ease-out,visibility .1s ease-out;z-index:4;}.mac-menu.visible{visibility:visible;opacity:1}.mac-menu ul{list-style:none;margin:0;padding-left:0;}.mac-menu ul .mac-menu-item{display:block;padding:5px 10px;transition:background .1s ease-out;color:#000;cursor:pointer;}.mac-menu ul .mac-menu-item.active{background:#e0e8fb;transition:0;text-decoration:none} 3 | .mac-modal-overlay{background-color:rgba(245,246,250,0.75);position:fixed;top:0;left:0;right:0;bottom:0;opacity:0;display:none;overflow-y:auto;z-index:3;}.mac-modal-overlay.ng-animate{display:block;transition:.2s ease-out all}.mac-modal-overlay.visible{opacity:1;display:block;}.mac-modal-overlay.visible .mac-modal{top:50%}.mac-modal{position:absolute;top:0;left:50%;width:500px;margin-left:-251px;background:#fff;border:1px solid #d1d3d8;box-shadow:0 4px 10px rgba(0,0,0,0.15);transition:top .2s ease-out;border-radius:3px;transform:rotateX(0) rotateY(0);}.mac-modal:after{content:"";display:block;position:absolute;bottom:-20px;height:20px;width:1px}.mac-close-modal{position:absolute;top:-5px;right:-5px;width:18px;height:18px;border-radius:9px;display:block;background:#bdc0c7;cursor:pointer;}.mac-close-modal:before,.mac-close-modal:after{content:'';display:block;width:2px;height:10px;background:#fff;position:absolute;top:4px;left:8px}.mac-close-modal:before{transform:rotate(-45deg)}.mac-close-modal:after{transform:rotate(45deg)}.mac-modal-content{padding:20px} 4 | .mac-popover{position:absolute;top:0;left:0;background:#fff;box-shadow:0 3px 6px rgba(0,0,0,0.2);border:1px solid #d1d3d8;box-sizing:border-box;transform:scale3d(0,0,0);opacity:0;transform-origin:left top;visibility:hidden;border-radius:3px;z-index:2;}.mac-popover.fixed{position:fixed}.mac-popover.visible{opacity:1;visibility:visible;transform:scale3d(1,1,1);transition:transform .1s ease-out,opacity .1s ease-in,visibility .1s ease-out}.mac-popover.ng-leave-active{opacity:0;transform:scale3d(0,0,0);visibility:hidden}.mac-popover .tip{display:block;width:0;height:0;border-bottom:9px solid #d1d3d8;border-left:9px solid rgba(255,255,255,0);border-right:9px solid rgba(255,255,255,0);position:absolute;top:-9px;left:15px;}.mac-popover .tip:after{content:'';display:block;width:0;height:0;border-bottom:8px solid #fff;border-left:8px solid rgba(255,255,255,0);border-right:8px solid rgba(255,255,255,0);position:absolute;top:1px;left:-8px}.mac-popover.above{transform-origin:left bottom;}.mac-popover.above .tip{top:auto;bottom:-9px;border-top:9px solid #d1d3d8;border-bottom:0;}.mac-popover.above .tip:after{top:auto;bottom:1px;border-top:8px solid #fff;border-bottom:0}.mac-popover.above.right{transform-origin:right bottom;}.mac-popover.above.right .tip{left:auto;right:15px}.mac-popover.below.right{transform-origin:right top;}.mac-popover.below.right .tip{left:auto;right:15px}.mac-popover.middle .tip{top:50%;margin-top:-9px;border-top:9px solid rgba(255,255,255,0);border-bottom:9px solid rgba(255,255,255,0);}.mac-popover.middle .tip:after{border-top:8px solid rgba(255,255,255,0);border-bottom:8px solid rgba(255,255,255,0);top:-8px}.mac-popover.middle.right{transform-origin:left center;}.mac-popover.middle.right .tip{border-left:0;border-right:9px solid #d1d3d8;left:-9px;}.mac-popover.middle.right .tip:after{border-left:0;border-right:8px solid #fff;left:1px}.mac-popover.middle.left{transform-origin:right center;}.mac-popover.middle.left .tip{border-right:0;border-left:9px solid #d1d3d8;right:-9px;left:auto;}.mac-popover.middle.left .tip:after{border-right:0;border-left:8px solid #fff;right:1px;left:auto}.mac-popover .popover-header{background:#eaecf1;display:none;}.mac-popover .popover-header .title{font-size:13px;font-weight:bold;line-height:33px;padding-left:10px;padding-top:1px;user-select:none;overflow:ellipsis;margin-right:40px}.mac-popover .popover-header .close{float:right;margin:7px 10px 0 0;display:none}.mac-popover .popover-content{max-height:415px;overflow-y:auto;overflow-x:hidden;position:relative}.mac-popover .popover-footer{display:none}.mac-popover.footer{padding-bottom:42px;}.mac-popover.footer .popover-footer{display:block;position:absolute;bottom:0;left:0;right:0;padding:10px;box-shadow:0 -1px 5px rgba(0,0,0,0.15)}.mac-popover.header .popover-header{display:block}.mac-popover.header.below .tip:after{border-bottom:8px solid #eaecf1}.mac-popover.header.middle.left .tip:after{border-left:8px solid #eaecf1}.mac-popover.header.middle.right .tip:after{border-right:8px solid #eaecf1} 5 | .mac-spinner,.mac-cspinner{display:inline-block;position:relative;}.mac-spinner.block,.mac-cspinner.block{display:block;margin:0 auto}@-moz-keyframes fade{0%{opacity:1}100%{opacity:.02}}@-webkit-keyframes fade{0%{opacity:1}100%{opacity:.02}}@-o-keyframes fade{0%{opacity:1}100%{opacity:.02}}@keyframes fade{0%{opacity:1}100%{opacity:.02}} 6 | .mac-tag-autocomplete{border:1px solid #aaa;background:#fff}.mac-tag-list{margin:0;padding-left:0}.mac-tag{margin:3px;position:relative;display:inline-block;list-style:none;font-size:13px;border:1px solid #bfc9e1;border-radius:3px;color:#000;}.mac-tag .tag-label{padding:3px 20px 3px 5px}.mac-tag .mac-tag-close{position:absolute;right:6px;color:#9da6b7;cursor:pointer;top:50%;margin-top:-9.5px}.mac-tag .mac-tag-input{padding:3px;margin:0;border:none;box-shadow:none;width:100%;border-radius:3px} 7 | .mac-tooltip{background:rgba(47,48,53,0.75);text-align:center;color:#fff;padding:5px 7px;position:absolute;top:0;left:0;font-size:12px;opacity:0;visibility:hidden;transition:opacity .1s ease-out,visibility .1s ease-out,margin .1s ease-out;border-radius:3px;z-index:7;}.mac-tooltip.visible{visibility:visible;opacity:1;margin-top:0}.mac-tooltip:after{content:'';display:block;width:0;height:0;position:absolute}.mac-tooltip.top:after{border-top:6px solid rgba(47,48,53,0.75);border-left:6px solid transparent;border-right:6px solid transparent;bottom:-6px;left:50%;margin-left:-6px}.mac-tooltip.bottom:after{border-bottom:6px solid rgba(47,48,53,0.75);border-left:6px solid transparent;border-right:6px solid transparent;top:-6px;left:50%;margin-left:-6px}.mac-tooltip.left:after{border-left:6px solid rgba(47,48,53,0.75);border-top:6px solid transparent;border-bottom:6px solid transparent;right:-6px;top:50%;margin-top:-6px}.mac-tooltip.right:after{border-right:6px solid rgba(47,48,53,0.75);border-top:6px solid transparent;border-bottom:6px solid transparent;left:-6px;top:50%;margin-top:-6px} -------------------------------------------------------------------------------- /docs/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "data":[{"name": "United States", "id": "123"},{"name": "United Kingdom", "id": "234"},{"name": "United Arab Emirates", "id": "345"}] 3 | } 4 | -------------------------------------------------------------------------------- /docs/doc.js: -------------------------------------------------------------------------------- 1 | /*global document */ 2 | (function() { 3 | angular.module("MacDemo", ['Mac', 'ngAnimate']).directive("code", function() { 4 | return { 5 | restrict: "E", 6 | terminal: true 7 | }; 8 | }) 9 | .controller("modalController", ["$scope", "modal", function($scope, modal) {}]) 10 | .controller("ExampleController", [ 11 | "$scope", "$window", "keys", function($scope, $window, keys) { 12 | var code, key; 13 | $scope.keys = []; 14 | for (key in keys) { 15 | code = keys[key]; 16 | $scope.keys.push({ 17 | key: key, 18 | code: code 19 | }); 20 | } 21 | 22 | $scope.selectedOptions = [ 23 | { 24 | value: 1, 25 | text: "text1" 26 | }, { 27 | value: 2, 28 | text: "text2" 29 | }, { 30 | value: 3, 31 | text: "text3" 32 | } 33 | ]; 34 | $scope.selectedOptionValue = "1"; 35 | 36 | // Menu section 37 | $scope.menuItems = [ 38 | { 39 | label: "Page 1", 40 | key: "Page 1" 41 | }, { 42 | label: "Page 2", 43 | key: "Page 2" 44 | }, { 45 | label: "Page 3", 46 | key: "Page 3" 47 | }, { 48 | label: "Page 4", 49 | key: "Page 4" 50 | } 51 | ]; 52 | $scope.selectingMenuItem = function(index) { 53 | $scope.selectedItem = $scope.menuItems[index].label; 54 | }; 55 | 56 | // Autocomplete section 57 | // Used in autocomplete to transform data 58 | $scope.onSuccess = function(data) { 59 | return data.data; 60 | }; 61 | 62 | $scope.autocompleteQuery = ""; 63 | 64 | // Url to remotely fetch content 65 | $scope.autocompleteUrl = "data.json"; 66 | 67 | // Selected tags in tag autocomplete 68 | $scope.tagAutocompleteSelected = []; 69 | $scope.tagAutocompleteDisabledSelected = []; 70 | $scope.tagAutocompleteEvents = []; 71 | $scope.tagAutocompletePlaceholder = "Hello"; 72 | $scope.tagAutocompleteModel = ""; 73 | $scope.tagAutocompleteOnSelected = function(item) { 74 | return { 75 | key: item 76 | }; 77 | }; 78 | 79 | $scope.tagAutocompleteOnBlur = function(event, item) { 80 | if (!item) { 81 | return; 82 | } 83 | $scope.tagAutocompleteEvents.push({ 84 | key: item 85 | }); 86 | $scope.tagAutocompleteModel = ""; 87 | return $scope.tagAutocompleteModel; 88 | }; 89 | 90 | $scope.tagAutocompleteOnKeyup = function(event, item) { 91 | console.debug("You just typed something"); 92 | }; 93 | 94 | $scope.extraTagInputs = [ 95 | { 96 | name: "United States", 97 | id: "123" 98 | }, { 99 | name: "United Kingdom", 100 | id: "234" 101 | }, { 102 | name: "United Arab Emirates", 103 | id: "345" 104 | } 105 | ]; 106 | $scope.selected = [ 107 | { 108 | name: "United States", 109 | id: "123" 110 | } 111 | ]; 112 | 113 | $scope.startDate = "01/01/2013"; 114 | $scope.minDate = "07/01/2012"; 115 | $scope.startTime = "04:42 PM"; 116 | $scope.fiveMinAgo = Math.round(Date.now() / 1000) - 5 * 60; 117 | $scope.oneDayAgo = Math.round(Date.now() / 1000) - 24 * 60 * 60; 118 | $scope.threeDaysAgo = Math.round(Date.now() / 1000) - 72 * 60 * 60; 119 | 120 | $scope.afterPausing = function($event) { 121 | $scope.pauseTypingModel = angular.element($event.target).val(); 122 | }; 123 | 124 | $scope.windowResizing = function($event) { 125 | $scope.windowWidth = angular.element($event.target).width(); 126 | }; 127 | } 128 | ]); 129 | 130 | window.prettyPrint && prettyPrint(); 131 | 132 | var source = document.getElementsByClassName('prettyprint source linenums'); 133 | var i = 0; 134 | var lineNumber = 0; 135 | var lineId; 136 | var lines; 137 | var totalLines; 138 | var anchorHash; 139 | 140 | if (source && source[0]) { 141 | anchorHash = document.location.hash.substring(1); 142 | lines = source[0].getElementsByTagName('li'); 143 | totalLines = lines.length; 144 | 145 | for (; i < totalLines; i++) { 146 | lineNumber++; 147 | lineId = 'line' + lineNumber; 148 | lines[i].id = lineId; 149 | if (lineId === anchorHash) { 150 | lines[i].className += ' selected'; 151 | } 152 | } 153 | } 154 | })(); 155 | -------------------------------------------------------------------------------- /docs/vendor.css: -------------------------------------------------------------------------------- 1 | .docs-example { 2 | position: relative; 3 | margin: 15px 0; 4 | padding: 39px 19px 14px; 5 | background-color: #fff; 6 | border: 1px solid #ddd; 7 | border-radius: 4px; 8 | min-height: 25px; 9 | } 10 | .docs-example:after { 11 | content: 'Example'; 12 | position: absolute; 13 | top: -1px; 14 | left: -1px; 15 | padding: 3px 7px; 16 | font-size: 12px; 17 | font-weight: bold; 18 | background-color: #f5f5f5; 19 | border: 1px solid #ddd; 20 | border-radius: 4px 0; 21 | color: #9da0a4; 22 | } 23 | 24 | .mac-tag-autocomplete { 25 | width: 100%; 26 | } 27 | 28 | .date-time input { 29 | width: 120px; 30 | } 31 | 32 | /* Tomorrow Theme */ 33 | /* Original theme - https://github.com/chriskempson/tomorrow-theme */ 34 | /* Pretty printing styles. Used with prettify.js. */ 35 | /* SPAN elements with the classes below are added by prettyprint. */ 36 | /* plain text */ 37 | .pln { 38 | color: #4d4d4c; } 39 | 40 | @media screen { 41 | /* string content */ 42 | .str { 43 | color: #718c00; } 44 | 45 | /* a keyword */ 46 | .kwd { 47 | color: #8959a8; } 48 | 49 | /* a comment */ 50 | .com { 51 | color: #8e908c; } 52 | 53 | /* a type name */ 54 | .typ { 55 | color: #4271ae; } 56 | 57 | /* a literal value */ 58 | .lit { 59 | color: #f5871f; } 60 | 61 | /* punctuation */ 62 | .pun { 63 | color: #4d4d4c; } 64 | 65 | /* lisp open bracket */ 66 | .opn { 67 | color: #4d4d4c; } 68 | 69 | /* lisp close bracket */ 70 | .clo { 71 | color: #4d4d4c; } 72 | 73 | /* a markup tag name */ 74 | .tag { 75 | color: #c82829; } 76 | 77 | /* a markup attribute name */ 78 | .atn { 79 | color: #f5871f; } 80 | 81 | /* a markup attribute value */ 82 | .atv { 83 | color: #3e999f; } 84 | 85 | /* a declaration */ 86 | .dec { 87 | color: #f5871f; } 88 | 89 | /* a variable name */ 90 | .var { 91 | color: #c82829; } 92 | 93 | /* a function name */ 94 | .fun { 95 | color: #4271ae; } } 96 | /* Use higher contrast and text-weight for printable form. */ 97 | @media print, projection { 98 | .str { 99 | color: #060; } 100 | 101 | .kwd { 102 | color: #006; 103 | font-weight: bold; } 104 | 105 | .com { 106 | color: #600; 107 | font-style: italic; } 108 | 109 | .typ { 110 | color: #404; 111 | font-weight: bold; } 112 | 113 | .lit { 114 | color: #044; } 115 | 116 | .pun, .opn, .clo { 117 | color: #440; } 118 | 119 | .tag { 120 | color: #006; 121 | font-weight: bold; } 122 | 123 | .atn { 124 | color: #404; } 125 | 126 | .atv { 127 | color: #060; } } 128 | /* Style */ 129 | /* 130 | pre.prettyprint { 131 | background: white; 132 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 133 | font-size: 12px; 134 | line-height: 1.5; 135 | border: 1px solid #ccc; 136 | padding: 10px; } 137 | */ 138 | 139 | /* Specify class=linenums on a pre to get line numbering */ 140 | ol.linenums { 141 | margin-top: 0; 142 | margin-bottom: 0; } 143 | 144 | /* IE indents via margin-left */ 145 | li.L0, 146 | li.L1, 147 | li.L2, 148 | li.L3, 149 | li.L4, 150 | li.L5, 151 | li.L6, 152 | li.L7, 153 | li.L8, 154 | li.L9 { 155 | /* */ } 156 | 157 | /* Alternate shading for lines */ 158 | li.L1, 159 | li.L3, 160 | li.L5, 161 | li.L7, 162 | li.L9 { 163 | /* */ } 164 | -------------------------------------------------------------------------------- /eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true 4 | }, 5 | "globals": { 6 | "angular": true 7 | }, 8 | "extends": "eslint:recommended", 9 | "rules": { 10 | "no-console": 0 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "plugins/markdown", 4 | "node_modules/macgyver-jsdoc/plugins/ngdoc" 5 | ], 6 | "opts": { 7 | "readme": "./README.md", 8 | "recurse": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /misc/grunt/bump.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | /** 3 | * bump section 4 | * Update package and bower version 5 | */ 6 | grunt.config("bump", { 7 | options: { 8 | files: ["bower.json", "package.json"], 9 | updateConfigs: ["bower", "pkg"], 10 | commit: false, 11 | commitMessage: "chore(build): Build v%VERSION%", 12 | tagMessage: "Build v%VERSION%", 13 | push: false, 14 | prereleaseName: 'rc' 15 | } 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /misc/grunt/contrib-concat.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | /** 3 | * Concat section 4 | * lib - Full MacGyver package 5 | */ 6 | grunt.config("concat", { 7 | options: { 8 | banner: "/**\n * MacGyver v<%= pkg.version %>\n * @link <%= pkg.homepage %>\n * @license <%= pkg.license[0].type %>\n */\n(function(window, angular, undefined) {\n", 9 | footer: "\n})(window, window.angular);" 10 | }, 11 | lib: { 12 | dest: "lib/macgyver.js", 13 | src: "<%= buildConf.full %>" 14 | } 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /misc/grunt/contrib-connect.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | /** 3 | * Connect section 4 | * Creates a temporary server for display documentation 5 | */ 6 | grunt.config("connect", { 7 | doc: { 8 | options: { 9 | port: 9001, 10 | hostname: "0.0.0.0", 11 | base: "out" 12 | } 13 | }, 14 | e2e: { 15 | options: { 16 | port: 9001, 17 | base: "", 18 | hostname: "0.0.0.0" 19 | } 20 | } 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /misc/grunt/contrib-copy.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | /** 3 | * Copy section 4 | */ 5 | grunt.config("copy", { 6 | css: { 7 | files: [ 8 | { 9 | src: "lib/macgyver.css", 10 | dest: "out/styles/macgyver.css" 11 | } 12 | ] 13 | }, 14 | doc: { 15 | files: [ 16 | { 17 | src: "lib/macgyver.js", 18 | dest: "out/scripts/macgyver.js" 19 | }, 20 | { 21 | src: "docs/doc.js", 22 | dest: "out/scripts/doc.js" 23 | }, 24 | { 25 | src: "docs/vendor.css", 26 | dest: "out/styles/vendor.css" 27 | }, 28 | { 29 | src: "docs/data.json", 30 | dest: "out/data.json" 31 | } 32 | ] 33 | }, 34 | dist: { 35 | files: [ 36 | { 37 | expand: true, 38 | cwd: "lib/", 39 | src: ["*"], 40 | dest: "dist/" 41 | } 42 | ] 43 | } 44 | }); 45 | }; 46 | -------------------------------------------------------------------------------- /misc/grunt/contrib-stylus.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | /** 3 | * Stylus section 4 | * Adding nib to all stylus 5 | */ 6 | grunt.config("stylus", { 7 | options: { 8 | use: ["nib"], 9 | paths: ["src/css/"], 10 | "import": ["_variables"] 11 | }, 12 | dev: { 13 | files: [ 14 | { 15 | "lib/macgyver.css": "<%= buildConf.css.core %>" 16 | } 17 | ] 18 | } 19 | }); 20 | }; 21 | -------------------------------------------------------------------------------- /misc/grunt/contrib-uglify.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | grunt.config("uglify", { 3 | options: { 4 | report: "min", 5 | preserveComments: false 6 | }, 7 | dist: { 8 | files: [ 9 | { 10 | src: "lib/macgyver.js", 11 | dest: "dist/macgyver.min.js" 12 | } 13 | ] 14 | } 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /misc/grunt/contrib-watch.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | /** 3 | * watch section 4 | * Watch all js and css changes 5 | */ 6 | 7 | grunt.config("watch", { 8 | options: { 9 | livereload: true 10 | }, 11 | js: { 12 | files: ["src/**/*.js"], 13 | tasks: [ 14 | "eslint:src", 15 | "karma:unit:run", 16 | "concat:lib", 17 | "replace:src", 18 | "copy:doc" 19 | ] 20 | }, 21 | test: { 22 | files: ["test/**/*.spec.js"], 23 | tasks: ["eslint:test", "eslint:e2e", "karma:unit:run"] 24 | }, 25 | css: { 26 | files: ["src/css/*.styl"], 27 | tasks: ["stylus", "copy:css"] 28 | } 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /misc/grunt/coveralls.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | grunt.config("coveralls", { 3 | src: { 4 | src: "coverage/lcov.info", 5 | force: false 6 | } 7 | }); 8 | }; 9 | -------------------------------------------------------------------------------- /misc/grunt/eslint.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | /** 3 | * eslint section 4 | */ 5 | grunt.config('eslint', { 6 | options: { 7 | configFile: 'eslint.json' 8 | }, 9 | src: { 10 | files: { 11 | src: ['src/**/*.js'] 12 | } 13 | }, 14 | test: { 15 | options: { 16 | envs: ['jasmine'], 17 | globals: ['module', 'inject', 'browserTrigger'] 18 | }, 19 | files: { 20 | src: ['test/unit/**/*.js'] 21 | } 22 | }, 23 | e2e: { 24 | options: { 25 | envs: ['protractor', 'jasmine'] 26 | }, 27 | files: { 28 | src: ['test/e2e/*.js'] 29 | } 30 | } 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /misc/grunt/jsdoc.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | grunt.config('jsdoc', { 3 | doc: { 4 | src: ['src/'], 5 | options: { 6 | destination: 'out', 7 | configure: 'jsdoc.json', 8 | private: false, 9 | template: "node_modules/macgyver-jsdoc/template" 10 | } 11 | } 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /misc/grunt/karma.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | /** 3 | * karma section 4 | * Testing framework 5 | */ 6 | grunt.config("karma", { 7 | options: { 8 | configFile: "test/karma.conf.js", 9 | ngHtml2JsPreprocessor: { 10 | stripPrefix: "src/" 11 | } 12 | }, 13 | unit: { 14 | background: true 15 | }, 16 | travis: { 17 | browsers: ["SL_Chrome", "SL_Firefox"], 18 | reporters: ["dots", "coverage", "saucelabs"], 19 | singleRun: true, 20 | preprocessors: { 21 | "src/**/*.js": ["coverage"], 22 | "**/*.html": ["ng-html2js"] 23 | }, 24 | coverageReporter: { 25 | type: "lcov", 26 | dir: "coverage/", 27 | subdir: "." 28 | } 29 | }, 30 | build: { 31 | singleRun: true, 32 | options: { 33 | files: [ 34 | "node_modules/angular/angular.js", 35 | "src/template/*.html", 36 | "lib/macgyver.js", 37 | "node_modules/angular-mocks/angular-mocks.js", 38 | "test/vendor/browserTrigger.js", 39 | "test/unit/*.spec.js" 40 | ] 41 | } 42 | } 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /misc/grunt/protractor.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | grunt.config("protractor", { 3 | normal: "test/protractor.conf.js" 4 | }); 5 | }; 6 | -------------------------------------------------------------------------------- /misc/grunt/replace.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | /** 3 | * Replace section 4 | * replace placeholder with contents 5 | */ 6 | grunt.config("replace", { 7 | src: { 8 | options: { 9 | pattern: /templateUrl: ["'](.+)["']/g, 10 | replace: function (match) { 11 | var compiledHtml, filePath; 12 | filePath = require("path").join("src/", match[1]); 13 | compiledHtml = grunt.file.read(filePath); 14 | compiledHtml = compiledHtml.replace(/"/g, "\\\""); 15 | compiledHtml = compiledHtml.replace(/\n/g, ""); 16 | return "template: \"" + (compiledHtml.trim()) + "\""; 17 | } 18 | }, 19 | files: [ 20 | { 21 | expand: true, 22 | flatten: false, 23 | src: ["lib/*.js", "!lib/macgyver.min.js"], 24 | ext: ".js" 25 | } 26 | ] 27 | } 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /misc/grunt/tasks.js: -------------------------------------------------------------------------------- 1 | var child = require("child_process"); 2 | 3 | module.exports = function(grunt) { 4 | /** 5 | * @name replace 6 | * @description 7 | * Replace placeholder with other values and content 8 | */ 9 | grunt.registerMultiTask("replace", "Replace placeholder with contents", function() { 10 | var options = this.options({ 11 | separator: "", 12 | replace: "", 13 | pattern: null 14 | }); 15 | 16 | var parse = function(code) { 17 | var templateUrlRegex = options.pattern; 18 | var updatedCode = code; 19 | var match; 20 | while (match = templateUrlRegex.exec(code)) { 21 | var replacement; 22 | if (grunt.util._(options.replace).isFunction()) { 23 | replacement = options.replace(match); 24 | } else { 25 | replacement = options.replace; 26 | } 27 | updatedCode = updatedCode.replace(match[0], replacement); 28 | } 29 | return updatedCode; 30 | }; 31 | 32 | this.files.forEach(function(file) { 33 | var src = file.src.filter(function(filepath) { 34 | var exists; 35 | if (!(exists = grunt.file.exists(filepath))) { 36 | grunt.log.warn("Source file '" + filepath + "' not found"); 37 | } 38 | return exists; 39 | }).map(function(filepath) { 40 | return parse(grunt.file.read(filepath)); 41 | }).join(grunt.util.normalizelf(options.separator)); 42 | 43 | grunt.file.write(file.dest, src); 44 | grunt.log.writeln("Replace placeholder with contents in '" + file.dest + "' successfully"); 45 | }); 46 | }); 47 | 48 | /** 49 | * @name protractor 50 | * @description 51 | * To run protractor. Following codes are taken from AngularJS, see: 52 | * https://github.com/angular/angular.js/blob/master/lib/grunt/utils.js#L155 53 | */ 54 | grunt.registerMultiTask("protractor", "Run Protractor integration tests", function() { 55 | var done = this.async(); 56 | 57 | var args = ["node_modules/protractor/bin/protractor", this.data]; 58 | 59 | p = child.spawn("node", args); 60 | p.stdout.pipe(process.stdout); 61 | p.stderr.pipe(process.stderr); 62 | p.on("exit", function(code) { 63 | if (code !== 0) { 64 | grunt.fail.warn("Protractor test(s) failed. Exit code: " + code); 65 | } 66 | done(); 67 | }); 68 | }); 69 | }; 70 | -------------------------------------------------------------------------------- /misc/saucelabs/start_tunnel.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Setup and start Sauce Connect for your TravisCI build 4 | # This script requires your .travis.yml to include the following two private env variables: 5 | # SAUCE_USERNAME 6 | # SAUCE_ACCESS_KEY 7 | # Follow the steps at https://saucelabs.com/opensource/travis to set that up. 8 | # 9 | # Curl and run this script as part of your .travis.yml before_script section: 10 | # before_script: 11 | # - curl https://gist.github.com/santiycr/5139565/raw/sauce_connect_setup.sh | bash 12 | SC_VERSION="4.3.16" 13 | CONNECT_URL="https://saucelabs.com/downloads/sc-$SC_VERSION-linux.tar.gz" 14 | CONNECT_DIR="/tmp/sauce-connect-$RANDOM" 15 | CONNECT_DOWNLOAD="sc-$SC_VERSION-linux.tar.gz" 16 | 17 | CONNECT_LOG="$LOGS_DIR/sauce-connect" 18 | CONNECT_STDOUT="$LOGS_DIR/sauce-connect.stdout" 19 | CONNECT_STDERR="$LOGS_DIR/sauce-connect.stderr" 20 | 21 | # Get Connect and start it 22 | mkdir -p $CONNECT_DIR 23 | cd $CONNECT_DIR 24 | curl $CONNECT_URL -o $CONNECT_DOWNLOAD 2> /dev/null 1> /dev/null 25 | mkdir sauce-connect 26 | tar --extract --file=$CONNECT_DOWNLOAD --strip-components=1 --directory=sauce-connect > /dev/null 27 | rm $CONNECT_DOWNLOAD 28 | 29 | SAUCE_ACCESS_KEY=`echo $SAUCE_ACCESS_KEY` 30 | 31 | 32 | ARGS="" 33 | 34 | # Set tunnel-id only on Travis, to make local testing easier. 35 | if [ ! -z "$TRAVIS_JOB_NUMBER" ]; then 36 | ARGS="$ARGS --tunnel-identifier $TRAVIS_JOB_NUMBER" 37 | fi 38 | if [ ! -z "$BROWSER_PROVIDER_READY_FILE" ]; then 39 | ARGS="$ARGS --readyfile $BROWSER_PROVIDER_READY_FILE" 40 | fi 41 | 42 | echo "Starting Sauce Connect in the background, logging into:" 43 | echo " $CONNECT_LOG" 44 | echo " $CONNECT_STDOUT" 45 | echo " $CONNECT_STDERR" 46 | sauce-connect/bin/sc -u $SAUCE_USERNAME -k $SAUCE_ACCESS_KEY $ARGS \ 47 | --logfile $CONNECT_LOG 2> $CONNECT_STDERR 1> $CONNECT_STDOUT & 48 | -------------------------------------------------------------------------------- /misc/saucelabs/teardown_tunnel.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o pipefail 4 | 5 | 6 | echo "Shutting down Sauce Connect tunnel" 7 | 8 | killall sc 9 | 10 | while [[ -n `ps -ef | grep "sauce-connect-" | grep -v "grep"` ]]; do 11 | printf "." 12 | sleep .5 13 | done 14 | 15 | echo "" 16 | echo "Sauce Connect tunnel has been shut down" 17 | -------------------------------------------------------------------------------- /misc/travis/before_build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | mkdir -p $LOGS_DIR 6 | 7 | ./misc/saucelabs/start_tunnel.sh 8 | 9 | npm install -g grunt-cli 10 | 11 | echo "wait_for_browser_provider" 12 | ./misc/travis/wait_for_browser_provider.sh 13 | -------------------------------------------------------------------------------- /misc/travis/print_logs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | LOG_FILES=$LOGS_DIR/* 4 | 5 | for FILE in $LOG_FILES; do 6 | echo -e "\n\n\n" 7 | echo "================================================================================" 8 | echo " $FILE" 9 | echo "================================================================================" 10 | cat $FILE 11 | done 12 | -------------------------------------------------------------------------------- /misc/travis/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | if [ $JOB = "unit" ]; then 6 | grunt test:ci 7 | elif [ $JOB = "e2e" ]; then 8 | grunt test:e2e 9 | else 10 | echo "Unknown job type. Use either JOB=unit or JOB=e2e" 11 | fi 12 | -------------------------------------------------------------------------------- /misc/travis/wait_for_browser_provider.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | # Wait for Connect to be ready before exiting 5 | # Time out if we wait for more than 2 minutes, so that we can print logs. 6 | let "counter=0" 7 | 8 | while [ ! -f $BROWSER_PROVIDER_READY_FILE ]; do 9 | let "counter++" 10 | if [ $counter -gt 240 ]; then 11 | echo "Timed out after 2 minutes waiting for browser provider ready file" 12 | # We must manually print logs here because travis will not run 13 | # after_script commands if the failure occurs before the script 14 | # phase. 15 | ./misc/travis/print_logs.sh 16 | exit 5 17 | fi 18 | sleep .5 19 | done 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Adrian Lee ", 3 | "name": "angular-macgyver", 4 | "description": "A library of shared directives", 5 | "version": "1.0.0", 6 | "homepage": "http://angular-macgyver.github.io/MacGyver", 7 | "repository": { 8 | "type": "git", 9 | "url": "git@github.com:angular-macgyver/MacGyver.git" 10 | }, 11 | "engines": { 12 | "node": ">=5.3.0" 13 | }, 14 | "license": "MIT", 15 | "dependencies": { 16 | "angular": "~1.4.0", 17 | "angular-animate": "~1.4.0" 18 | }, 19 | "devDependencies": { 20 | "angular-mocks": "~1.4.0", 21 | "electron-prebuilt": "^1.1.1", 22 | "grunt": "^1.0.1", 23 | "grunt-bump": "^0.8.0", 24 | "grunt-contrib-concat": "^1.0.0", 25 | "grunt-contrib-connect": "^1.0.1", 26 | "grunt-contrib-copy": "^1.0.0", 27 | "grunt-contrib-stylus": "^1.2.0", 28 | "grunt-contrib-uglify": "^2.0.0", 29 | "grunt-contrib-watch": "^1.0.0", 30 | "grunt-coveralls": "^1.0.0", 31 | "grunt-eslint": "^19.0.0", 32 | "grunt-jsdoc": "^2.0.0", 33 | "grunt-karma": "^2.0.0", 34 | "jsdoc": "^3.4.0", 35 | "karma": "^1.3.0", 36 | "karma-coverage": "^1.0.0", 37 | "karma-electron-launcher": "^0.1.0", 38 | "karma-jasmine": "^1.0.0", 39 | "karma-ng-html2js-preprocessor": "^1.0.0", 40 | "karma-sauce-launcher": "^1.0.0", 41 | "macgyver-jsdoc": "github:angular-macgyver/macgyver-jsdoc", 42 | "matchdep": "1.0.1", 43 | "protractor": "4.0.5", 44 | "time-grunt": "1.4.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | /* jshint multistr: true */ 2 | 3 | angular.module('Mac'). 4 | /** 5 | * @ngdoc constant 6 | * @name macTooltipDefaults 7 | * @description 8 | * Default values for mac-tooltip 9 | */ 10 | constant('macTooltipDefaults', { 11 | direction: 'top', 12 | trigger: 'hover', 13 | inside: false, 14 | class: 'visible' 15 | }). 16 | 17 | /** 18 | * @ngdoc constant 19 | * @name macTimeDefaults 20 | * @description 21 | * Default values for mac-time 22 | */ 23 | constant('macTimeDefaults', { 24 | default: '12:00 AM', 25 | placeholder: '--:--' 26 | }). 27 | 28 | /** 29 | * @ngdoc constant 30 | * @name scrollSpyDefaults 31 | * @description 32 | * Default values for mac-scroll-spy 33 | */ 34 | constant('scrollSpyDefaults', { 35 | offset: 0, 36 | highlightClass: 'active' 37 | }). 38 | 39 | /** 40 | * @ngdoc constant 41 | * @name macPopoverDefaults 42 | * @description 43 | * Default values for mac-popover 44 | */ 45 | constant('macPopoverDefaults', { 46 | trigger: { 47 | offsetY: 0, 48 | offsetX: 0, 49 | trigger: 'click', 50 | container: null 51 | }, 52 | element: { 53 | footer: false, 54 | header: false, 55 | title: '', 56 | direction: 'above left', 57 | refreshOn: '' 58 | }, 59 | template: "
\ 60 |
\ 61 |
\ 62 |
{{macPopoverTitle}}
\ 63 |
\ 64 |
\ 65 |
" 66 | }). 67 | 68 | /** 69 | * @ngdoc constant 70 | * @name macModalDefaults 71 | * @description 72 | * Default values for mac-modal 73 | */ 74 | constant('macModalDefaults', { 75 | keyboard: false, 76 | overlayClose: false, 77 | resize: false, 78 | position: true, 79 | open: angular.noop, 80 | topOffset: 20, 81 | attributes: {}, 82 | beforeShow: angular.noop, 83 | afterShow: angular.noop, 84 | beforeHide: angular.noop, 85 | afterHide: angular.noop, 86 | template: "
\ 87 |
\ 88 | \ 89 |
\ 90 |
\ 91 |
" 92 | }). 93 | 94 | /** 95 | * @ngdoc constant 96 | * @name macAffixDefaults 97 | * @description 98 | * Default values for mac-affix 99 | */ 100 | constant('macAffixDefaults', { 101 | top: 0, 102 | bottom: 0, 103 | disabled: false, 104 | classes: "affix affix-top affix-bottom" 105 | }); 106 | -------------------------------------------------------------------------------- /src/controllers/affix.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Controller for affix directive 3 | */ 4 | 5 | /** 6 | * @param {Element} $element Affix directive element 7 | * @param {$document} $document 8 | * @param {$window} $window 9 | * @param {Object} defaults Affix default values 10 | * @constructor 11 | */ 12 | function MacAffixController ($element, $document, $window, defaults) { 13 | this.$document = $document; 14 | this.defaults = defaults; 15 | 16 | this.$element = $element; 17 | 18 | this.offset = { 19 | top: defaults.top, 20 | bottom: defaults.bottom 21 | }; 22 | 23 | this.windowEl = angular.element($window); 24 | 25 | this.disabled = defaults.disabled; 26 | 27 | this.lastAffix = null; 28 | this.unpin = null; 29 | this.pinnedOffset = null; 30 | } 31 | 32 | /** 33 | * Update to or bottom offset. This function make sure the value is an integer 34 | * or use default values 35 | * @param {String} key Offset key 36 | * @param {String|Integer} value Update value 37 | * @param {Boolean} useDefault 38 | */ 39 | MacAffixController.prototype.updateOffset = function (key, value, useDefault) { 40 | // Don't do anything if changing invalid key 41 | if (key !== 'top' && key !== 'bottom') { 42 | return; 43 | } 44 | 45 | if (!!useDefault && value === null) { 46 | value = this.defaults[key]; 47 | } 48 | 49 | if (value !== null && !isNaN(+value)) { 50 | this.offset[key] = +value; 51 | } 52 | } 53 | 54 | MacAffixController.prototype.scrollEvent = function () { 55 | // Check if element is visible 56 | if (this.$element[0].offsetHeight <= 0 && this.$element[0].offsetWidth <= 0) { 57 | return; 58 | } 59 | 60 | var scrollTop = this.windowEl.scrollTop(); 61 | var scrollHeight = this.$document.height(); 62 | var elementHeight = this.$element.outerHeight(); 63 | var affix; 64 | 65 | if (this.unpin !== null && scrollTop <= this.unpin) { 66 | affix = false; 67 | } else if (this.offset.bottom !== null && scrollTop > scrollHeight - elementHeight - this.offset.bottom) { 68 | affix = 'bottom'; 69 | } else if (this.offset.top !== null && scrollTop <= this.offset.top) { 70 | affix = 'top'; 71 | } else { 72 | affix = false; 73 | } 74 | 75 | if (affix === this.lastAffix) return; 76 | if (this.unpin) { 77 | this.$element.css('top', ''); 78 | } 79 | 80 | this.lastAffix = affix; 81 | 82 | if (affix === 'bottom') { 83 | if (this.pinnedOffset !== null) { 84 | this.unpin = this.pinnedOffset; 85 | } 86 | 87 | this.$element 88 | .removeClass(this.defaults.classes) 89 | .addClass('affix'); 90 | this.pinnedOffset = this.$document.height() - this.$element.outerHeight() - this.offset.bottom; 91 | this.unpin = this.pinnedOffset; 92 | 93 | } else { 94 | this.unpin = null; 95 | } 96 | 97 | this.$element 98 | .removeClass(this.defaults.classes) 99 | .addClass('affix' + (affix ? '-' + affix : '')); 100 | 101 | // Look into merging this with the move if block 102 | if (affix === 'bottom') { 103 | var curOffset = this.$element.offset(); 104 | this.$element.css('top', this.unpin - curOffset.top); 105 | } 106 | 107 | return true; 108 | } 109 | 110 | MacAffixController.prototype.setDisabled = function (newValue) { 111 | this.disabled = newValue || this.defaults.disabled; 112 | 113 | if (this.disabled) { 114 | this.reset(); 115 | } else { 116 | this.scrollEvent(); 117 | } 118 | 119 | return this.disabled; 120 | } 121 | 122 | MacAffixController.prototype.reset = function () { 123 | // clear all styles and reset affix element 124 | this.lastAffix = null; 125 | this.unpin = null; 126 | 127 | this.$element 128 | .css('top', '') 129 | .removeClass(this.defaults.classes); 130 | } 131 | 132 | angular.module('Mac') 133 | .controller('MacAffixController', [ 134 | '$element', 135 | '$document', 136 | '$window', 137 | 'macAffixDefaults', 138 | MacAffixController 139 | ]); 140 | -------------------------------------------------------------------------------- /src/controllers/tag_autocomplete.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Controller for tag autocomplete directive 3 | */ 4 | 5 | /** 6 | * @param {$scope} $scope 7 | * @constructor 8 | */ 9 | function MacTagAutocompleteController ($scope, $element, $attrs, $parse, $timeout, keys) { 10 | this.$scope = $scope; 11 | this.$element = $element; 12 | this.$attrs = $attrs; 13 | 14 | this.$parse = $parse; 15 | this.$timeout = $timeout; 16 | this.keys = keys; 17 | 18 | this.textInput = ''; 19 | 20 | this.labelKey = this.labelKey != undefined ? this.labelKey : 'name'; 21 | this.labelGetter = $parse(this.labelKey); 22 | } 23 | 24 | /** 25 | * Callback function on autocomplete keydown. The function especially 26 | * handles 2 cases, 27 | * - pressing BACKSPACE: Remove the last selected item 28 | * - pressing ENTER: If autocomplete is disabled, take the current input 29 | * and tokenify the value 30 | * Invoke macAutocompleteOnKeydown afterwards 31 | * 32 | * @param {Event} $event 33 | * @return {boolean} true 34 | */ 35 | MacTagAutocompleteController.prototype.onKeyDown = function ($event) { 36 | var stroke = $event.which || $event.keyCode; 37 | switch(stroke) { 38 | case this.keys.BACKSPACE: 39 | if (!this.$scope.textInput && angular.isArray(this.selected)) { 40 | this.selected.pop(); 41 | } 42 | break; 43 | case this.keys.ENTER: 44 | // Used when autocomplete is not needed 45 | if (this.textInput && this.disabled) { 46 | this.onSelect(this.textInput); 47 | } 48 | break; 49 | } 50 | 51 | this.onKeydownFn({ 52 | $event: $event, 53 | value: this.textInput 54 | }); 55 | 56 | return true; 57 | } 58 | 59 | /** 60 | * Callback function when macAutocomplete made a successful xhr request 61 | * Default to `data.data` if function doesn't exist 62 | * 63 | * @param {Object} data 64 | * @return {Object} 65 | */ 66 | MacTagAutocompleteController.prototype.onSuccess = function (data, status, headers) { 67 | var result = data.data; 68 | 69 | if (this.$attrs['macTagAutocompleteOnSuccess']) { 70 | result = this.onSuccessFn({ 71 | data: data, 72 | status: status, 73 | headers: headers 74 | }); 75 | } 76 | return result; 77 | } 78 | 79 | /** 80 | * Callback function when user select an item from menu or pressed enter 81 | * User can specify macTagAutocompleteOnEnter function to modify item before 82 | * pushing into `selected` array 83 | * This function will also clear the text in autocomplete 84 | * 85 | * @param {Object} item 86 | */ 87 | MacTagAutocompleteController.prototype.onSelect = function (item) { 88 | if (this.$attrs['macTagAutocompleteOnEnter']) { 89 | item = this.onEnterFn({item: item}); 90 | } 91 | 92 | if (item && angular.isArray(this.selected)) { 93 | this.selected.push(item); 94 | } 95 | 96 | // NOTE: $timeout is added to allow user to access the model before 97 | // clearing value in autocomplete 98 | var ctrl = this; 99 | this.$timeout(function () { 100 | ctrl.textInput = ''; 101 | }, 0); 102 | } 103 | 104 | /** 105 | * If a label attr is specified, convert the tag object into string 106 | * for display 107 | * 108 | * @param {Object} tag 109 | * @return {string} 110 | */ 111 | MacTagAutocompleteController.prototype.getTagLabel = function (tag) { 112 | return this.labelKey ? this.labelGetter(tag) : tag; 113 | }; 114 | 115 | angular.module('Mac') 116 | .controller('MacTagAutocompleteController', [ 117 | '$scope', 118 | '$element', 119 | '$attrs', 120 | '$parse', 121 | '$timeout', 122 | 'keys', 123 | MacTagAutocompleteController]); 124 | -------------------------------------------------------------------------------- /src/css/_variables.styl: -------------------------------------------------------------------------------- 1 | // Z-index 2 | // --------------------------- 3 | $popoverZindex = 2 4 | $modalZindex = 3 5 | $menuZindex = 4 6 | $tooltipZindex = 7 7 | -------------------------------------------------------------------------------- /src/css/common.styl: -------------------------------------------------------------------------------- 1 | .mac-date-time 2 | &.ng-invalid-date 3 | &.ng-invalid-time 4 | background #ffe2e2 5 | border solid 1px #dea9a9 6 | 7 | .close, .close-modal 8 | width 18px 9 | height 18px 10 | border-radius 9px 11 | display block 12 | background #bdc0c7 13 | position relative 14 | opacity 1 15 | 16 | &:after 17 | content '' 18 | display block 19 | width 2px 20 | height 10px 21 | background #FFF 22 | position absolute 23 | top 4px 24 | left 8px 25 | transform rotate(45deg) 26 | &:before 27 | content '' 28 | display block 29 | width 2px 30 | height 10px 31 | background #FFF 32 | position absolute 33 | top 4px 34 | left 8px 35 | transform rotate(-45deg) 36 | 37 | &:hover 38 | background #828590 39 | 40 | .hide 41 | display none 42 | 43 | .affix 44 | position fixed 45 | 46 | &-bottom 47 | position absolute 48 | -------------------------------------------------------------------------------- /src/css/menu.styl: -------------------------------------------------------------------------------- 1 | .mac-menu 2 | position absolute 3 | top 0 4 | left 0 5 | min-width 200px 6 | background #FFF 7 | box-shadow 0 3px 6px rgba(0,0,0,0.2) 8 | border 1px solid #d1d3d8 9 | box-sizing border-box 10 | max-height 325px 11 | overflow-y auto 12 | opacity 0 13 | visibility hidden 14 | transition opacity 0.1s ease-out, visibility 0.1s ease-out 15 | z-index $menuZindex 16 | 17 | &.visible 18 | visibility visible 19 | opacity 1 20 | 21 | ul 22 | list-style none 23 | margin 0px 24 | padding-left 0px 25 | 26 | .mac-menu-item 27 | display block 28 | padding 5px 10px 29 | transition background 0.1s ease-out 30 | color #000 31 | cursor pointer 32 | 33 | &.active 34 | background #e0e8fb 35 | transition 0 36 | text-decoration none 37 | -------------------------------------------------------------------------------- /src/css/modal.styl: -------------------------------------------------------------------------------- 1 | .mac-modal-overlay 2 | background-color rgba(245,246,250,0.75) 3 | position fixed 4 | top 0 5 | left 0 6 | right 0 7 | bottom 0 8 | opacity 0 9 | display none 10 | overflow-y auto 11 | z-index $modalZindex 12 | 13 | &.ng-animate 14 | display block 15 | transition 0.2s ease-out all 16 | 17 | &.visible 18 | opacity 1.0 19 | display block 20 | 21 | .mac-modal 22 | top 50% 23 | 24 | .mac-modal 25 | position absolute 26 | top 0 27 | left 50% 28 | width 500px 29 | margin-left -251px 30 | background #FFF 31 | border 1px solid #d1d3d8 32 | box-shadow 0 4px 10px rgba(0,0,0,0.15) 33 | transition top 0.2s ease-out 34 | border-radius 3px 35 | transform rotateX(0) rotateY(0) 36 | 37 | &:after 38 | content "" 39 | display block 40 | position absolute 41 | bottom -20px 42 | height 20px 43 | width 1px 44 | 45 | .mac-close-modal 46 | position absolute 47 | top -5px 48 | right -5px 49 | width 18px 50 | height 18px 51 | border-radius 9px 52 | display block 53 | background #bdc0c7 54 | cursor pointer 55 | 56 | &:before, &:after 57 | content '' 58 | display block 59 | width 2px 60 | height 10px 61 | background #fff 62 | position absolute 63 | top 4px 64 | left 8px 65 | 66 | &:before 67 | transform rotate(-45deg) 68 | 69 | &:after 70 | transform rotate(45deg) 71 | 72 | .mac-modal-content 73 | padding 20px 74 | -------------------------------------------------------------------------------- /src/css/popover.styl: -------------------------------------------------------------------------------- 1 | .mac-popover 2 | position absolute 3 | top 0 4 | left 0 5 | background #FFF 6 | box-shadow 0 3px 6px rgba(0,0,0,0.2) 7 | border 1px solid #d1d3d8 8 | box-sizing border-box 9 | transform scale3d(0,0,0) 10 | opacity 0 11 | transform-origin left top 12 | visibility hidden 13 | border-radius 3px 14 | z-index $popoverZindex 15 | 16 | &.fixed 17 | position fixed 18 | 19 | &.visible 20 | opacity 1 21 | visibility visible 22 | transform scale3d(1,1,1) 23 | transition transform .1s ease-out, opacity .1s ease-in, visibility .1s ease-out 24 | 25 | &.ng-leave-active 26 | opacity 0 27 | transform scale3d(0,0,0) 28 | visibility hidden 29 | 30 | .tip 31 | display block 32 | width 0 33 | height 0 34 | border-bottom 9px solid #d1d3d8 35 | //-Transparent doesn't work in FireFox, so we use RGBA for now 36 | border-left 9px solid rgba(255,255,255,0) 37 | border-right 9px solid rgba(255,255,255,0) 38 | position absolute 39 | top -9px 40 | left 15px 41 | 42 | &:after 43 | content '' 44 | display block 45 | width 0 46 | height 0 47 | border-bottom 8px solid #FFF 48 | border-left 8px solid rgba(255,255,255,0) 49 | border-right 8px solid rgba(255,255,255,0) 50 | position absolute 51 | top 1px 52 | left -8px 53 | 54 | 55 | &.above 56 | transform-origin left bottom 57 | .tip 58 | top auto 59 | bottom -9px 60 | border-top 9px solid #d1d3d8 61 | border-bottom 0 62 | &:after 63 | top auto 64 | bottom 1px 65 | border-top 8px solid #FFF 66 | border-bottom 0 67 | &.right 68 | transform-origin right bottom 69 | .tip 70 | left auto 71 | right 15px 72 | 73 | &.below 74 | &.right 75 | transform-origin right top 76 | .tip 77 | left auto 78 | right 15px 79 | 80 | &.middle 81 | .tip 82 | top 50% 83 | margin-top -9px 84 | border-top 9px solid rgba(255,255,255,0) 85 | border-bottom 9px solid rgba(255,255,255,0) 86 | 87 | &:after 88 | border-top 8px solid rgba(255,255,255,0) 89 | border-bottom 8px solid rgba(255,255,255,0) 90 | top -8px 91 | 92 | &.right 93 | transform-origin left center 94 | .tip 95 | border-left 0 96 | border-right 9px solid #d1d3d8 97 | left -9px 98 | 99 | &:after 100 | border-left 0 101 | border-right 8px solid #FFF 102 | left 1px 103 | 104 | &.left 105 | transform-origin right center 106 | .tip 107 | border-right 0 108 | border-left 9px solid #d1d3d8 109 | right -9px 110 | left auto 111 | 112 | &:after 113 | border-right 0 114 | border-left 8px solid #FFF 115 | right 1px 116 | left auto 117 | 118 | .popover-header 119 | background #eaecf1 120 | display none 121 | 122 | .title 123 | font-size 13px 124 | font-weight bold 125 | line-height 33px 126 | padding-left 10px 127 | padding-top 1px 128 | user-select none 129 | overflow ellipsis 130 | margin-right 40px 131 | .close 132 | float right 133 | margin 7px 10px 0 0 134 | display none 135 | 136 | .popover-content 137 | max-height 415px 138 | overflow-y auto 139 | overflow-x hidden 140 | position relative 141 | 142 | .popover-footer 143 | display none 144 | 145 | &.footer 146 | padding-bottom 42px 147 | 148 | .popover-footer 149 | display block 150 | position absolute 151 | bottom 0 152 | left 0 153 | right 0 154 | padding 10px 155 | box-shadow 0 -1px 5px rgba(0,0,0,0.15) 156 | 157 | &.header 158 | .popover-header 159 | display block 160 | 161 | &.below 162 | .tip:after 163 | border-bottom 8px solid #eaecf1 164 | 165 | &.middle 166 | &.left 167 | .tip:after 168 | border-left 8px solid #eaecf1 169 | 170 | &.right 171 | .tip:after 172 | border-right 8px solid #eaecf1 173 | -------------------------------------------------------------------------------- /src/css/spinner.styl: -------------------------------------------------------------------------------- 1 | .mac-spinner, .mac-cspinner 2 | display inline-block 3 | position relative 4 | 5 | &.block 6 | display block 7 | margin 0 auto 8 | 9 | @keyframes fade 10 | 0% 11 | opacity 1 12 | 100% 13 | opacity 0.02 14 | -------------------------------------------------------------------------------- /src/css/tag_autocomplete.styl: -------------------------------------------------------------------------------- 1 | .mac-tag-autocomplete 2 | border 1px solid #aaaaaa 3 | background #ffffff 4 | 5 | .mac-tag-list 6 | margin 0px 7 | padding-left 0px 8 | 9 | .mac-tag 10 | margin 3px 11 | position relative 12 | display inline-block 13 | list-style none 14 | font-size 13px 15 | border 1px solid #bfc9e1 16 | border-radius 3px 17 | color #000 18 | 19 | .tag-label 20 | padding 3px 20px 3px 5px 21 | 22 | .mac-tag-close 23 | position absolute 24 | right 6px 25 | color #9da6b7 26 | cursor pointer 27 | top 50% 28 | margin-top -9.5px 29 | 30 | .mac-tag-input 31 | padding 3px 32 | margin 0px 33 | border none 34 | box-shadow none 35 | width 100% 36 | border-radius 3px 37 | -------------------------------------------------------------------------------- /src/css/tooltip.styl: -------------------------------------------------------------------------------- 1 | .mac-tooltip 2 | background rgba(47,48,53,0.75) 3 | text-align center 4 | color #FFF 5 | padding 5px 7px 6 | position absolute 7 | top 0 8 | left 0 9 | font-size 12px 10 | opacity 0 11 | visibility hidden 12 | transition opacity 0.1s ease-out, visibility 0.1s ease-out, margin 0.1s ease-out 13 | border-radius 3px 14 | z-index $tooltipZindex 15 | 16 | &.visible 17 | visibility visible 18 | opacity 1.0 19 | margin-top 0 20 | 21 | &:after 22 | content '' 23 | display block 24 | width 0 25 | height 0 26 | position absolute 27 | 28 | &.top 29 | &:after 30 | border-top 6px solid rgba(47,48,53,0.75) 31 | border-left 6px solid transparent 32 | border-right 6px solid transparent 33 | bottom -6px 34 | left 50% 35 | margin-left -6px 36 | 37 | &.bottom 38 | &:after 39 | border-bottom 6px solid rgba(47,48,53,0.75) 40 | border-left 6px solid transparent 41 | border-right 6px solid transparent 42 | top -6px 43 | left 50% 44 | margin-left -6px 45 | 46 | &.left 47 | &:after 48 | border-left 6px solid rgba(47,48,53,0.75) 49 | border-top 6px solid transparent 50 | border-bottom 6px solid transparent 51 | right -6px 52 | top 50% 53 | margin-top -6px 54 | 55 | &.right 56 | &:after 57 | border-right 6px solid rgba(47,48,53,0.75) 58 | border-top 6px solid transparent 59 | border-bottom 6px solid transparent 60 | left -6px 61 | top 50% 62 | margin-top -6px 63 | -------------------------------------------------------------------------------- /src/directives/affix.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc directive 3 | * @name macAffix 4 | * 5 | * @restrict EAC 6 | * 7 | * @description 8 | * Fix the component at a certain position 9 | * 10 | * @param {Expr} mac-affix-disabled To unpin element (default false) 11 | * @param {Expr} mac-affix-top Top offset (default 0) 12 | * @param {Expr} mac-affix-bottom Bottom offset (default 0) 13 | * @param {Event} refresh-mac-affix To update the position of affixed element 14 | * 15 | * @example 16 |
Nav content
17 | */ 18 | 19 | angular.module('Mac').directive('macAffix', ['$window', function($window) { 20 | return { 21 | require: 'macAffix', 22 | bindToController: true, 23 | controllerAs: 'macAffix', 24 | controller: 'MacAffixController', 25 | link: function($scope, element, attrs, macAffixCtrl) { 26 | var scrollEventWrapper = function () { 27 | macAffixCtrl.scrollEvent(); 28 | } 29 | var windowEl = angular.element($window); 30 | 31 | if (attrs.macAffixTop !== null) { 32 | macAffixCtrl.updateOffset('top', $scope.$eval(attrs.macAffixTop), true); 33 | $scope.$watch(attrs.macAffixTop, function(value) { 34 | macAffixCtrl.updateOffset('top', value); 35 | }); 36 | } 37 | 38 | if (attrs.macAffixBottom !== null) { 39 | macAffixCtrl.updateOffset('bottom', $scope.$eval(attrs.macAffixBottom), true); 40 | $scope.$watch(attrs.macAffixBottom, function(value) { 41 | macAffixCtrl.updateOffset('bottom', value); 42 | }); 43 | } 44 | 45 | if (attrs.macAffixDisabled) { 46 | macAffixCtrl.setDisabled($scope.$eval(attrs.macAffixDisabled)); 47 | 48 | $scope.$watch(attrs.macAffixDisabled, function (value) { 49 | if (value === null || value === macAffixCtrl.disabled) return; 50 | 51 | macAffixCtrl.setDisabled(value); 52 | 53 | var action = value ? 'unbind' : 'bind'; 54 | windowEl[action]('scroll', scrollEventWrapper); 55 | }); 56 | } 57 | 58 | if (!macAffixCtrl.disabled) { 59 | windowEl.bind('scroll', scrollEventWrapper); 60 | } 61 | 62 | $scope.$on('refresh-mac-affix', function () { 63 | macAffixCtrl.scrollEvent(); 64 | }); 65 | 66 | $scope.$on('$destroy', function () { 67 | windowEl.unbind('scroll', scrollEventWrapper); 68 | }); 69 | } 70 | }; 71 | }]); 72 | -------------------------------------------------------------------------------- /src/directives/autocomplete.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc directive 3 | * @name macAutocomplete 4 | * @description 5 | * A directive for providing suggestions while typing into the field 6 | * 7 | * Autocomplete allows for custom html templating in the dropdown and some properties are exposed on the local scope on each template instance, including: 8 | * 9 | * | Variable | Type | Details | 10 | * |-----------|---------|-----------------------------------------------------------------------------| 11 | * | `$index` | Number | iterator offset of the repeated element (0..length-1) | 12 | * | `$first` | Boolean | true if the repeated element is first in the iterator. | 13 | * | `$middle` | Boolean | true if the repeated element is between the first and last in the iterator. | 14 | * | `$last` | Boolean | true if the repeated element is last in the iterator. | 15 | * | `$even` | Boolean | true if the iterator position `$index` is even (otherwise false). | 16 | * | `$odd` | Boolean | true if the iterator position `$index` is odd (otherwise false). | 17 | * | `item` | Object | item object with `value` and `label` if label-key is set | 18 | * 19 | * To use custom templating 20 | * 21 | * ``` 22 | * 23 | * {{item.label}} 24 | * 25 | * ``` 26 | * 27 | * Template default to `{{item.label}}` if not defined 28 | * 29 | * @param {String} ng-model Assignable angular expression to data-bind to (required) 30 | * @param {String} mac-placeholder Placeholder text 31 | * @param {Expression} mac-autocomplete-source Data to use. 32 | * Source support multiple types: 33 | * - Array: An array can be used for local data and there are two supported formats: 34 | * - An array of strings: ["Item1", "Item2"] 35 | * - An array of objects with mac-autocomplete-label key: [{name:"Item1"}, {name:"Item2"}] 36 | * - String: Using a string as the source is the same as passing the variable into mac-autocomplete-url 37 | * - Function: A callback when querying for data. The callback receive two arguments: 38 | * - {String} Value currently in the text input 39 | * - {Function} A response callback which expects a single argument, data to user. The data will be 40 | * populated on the menu and the menu will adjust accordingly 41 | * @param {Boolean} mac-autocomplete-disabled Boolean value if autocomplete should be disabled 42 | * @param {Function} mac-autocomplete-on-select Function called when user select on an item 43 | * - `selected` - {Object} The item selected 44 | * @param {Function} mac-autocomplete-on-success function called on success ajax request 45 | * - `data` - {Object} Data returned from the request 46 | * - `status` - {Number} The status code of the response 47 | * - `header` - {Object} Header of the response 48 | * @param {Function} mac-autocomplete-on-error Function called on ajax request error 49 | * - `data` - {Object} Data returned from the request 50 | * - `status` - {Number} The status code of the response 51 | * - `header` - {Object} Header of the response 52 | * @param {String} mac-autocomplete-label The label to display to the users (default "name") 53 | * @param {String} mac-autocomplete-query The query parameter on GET command (default "q") 54 | * @param {Integer} mac-autocomplete-delay Delay on fetching autocomplete data after keyup (default 800) 55 | * 56 | * @param {Expr} mac-menu-class Classes for mac-menu used by mac-autocomplete. For more info, check [ngClass](http://docs.angularjs.org/api/ng/directive/ngClass) 57 | * 58 | * @example 59 | Basic setup 60 | 61 | 68 | 69 | 76 | * 77 | * @example 78 | Example with autocomplete using source 79 | 80 | 85 | 86 | 91 | * 92 | */ 93 | 94 | angular.module('Mac') 95 | .directive('macAutocomplete', [ 96 | function () { 97 | return { 98 | restrict: 'EA', 99 | template: '', 100 | transclude: true, 101 | replace: true, 102 | require: ['ngModel', 'macAutocomplete'], 103 | bindToController: true, 104 | controllerAs: 'macAutocomplete', 105 | controller: 'MacAutocompleteController', 106 | scope: { 107 | onSelect: '&macAutocompleteOnSelect', 108 | onSuccess: '&macAutocompleteOnSuccess', 109 | onError: '&macAutocompleteOnError', 110 | source: '=macAutocompleteSource', 111 | disabled: '=?macAutocompleteDisabled', 112 | queryKey: '@macAutocompleteQuery', 113 | delay: '@macAutocompleteDelay', 114 | class: '=macMenuClass' 115 | }, 116 | link: function ($scope, element, attrs, ctrls, transclude) { 117 | var ngModelCtrl = ctrls[0], 118 | macAutocompleteCtrl = ctrls[1]; 119 | 120 | macAutocompleteCtrl.initializeMenu(ngModelCtrl, transclude); 121 | ngModelCtrl.$parsers.push(function(value) { 122 | return macAutocompleteCtrl.parser(value); 123 | }); 124 | element.bind('keydown', function (event) { 125 | return macAutocompleteCtrl.keydownHandler(event); 126 | }); 127 | } 128 | }; 129 | } 130 | ]); 131 | -------------------------------------------------------------------------------- /src/directives/events/keydown.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc directive 3 | * @name macKeydownEnter 4 | * 5 | * @description 6 | * macKeydownEnter allows you to specify custom behavior when pressing 7 | * enter in an input 8 | * 9 | * @param {expression} macKeydownEnter To evaluate on hitting enter 10 | * 11 | * @example 12 | 13 | 14 |
Key pressed: {{eventKeydown}}
15 |
16 | 17 | */ 18 | 19 | /** 20 | * @ngdoc directive 21 | * @name macKeydownEscape 22 | * 23 | * @description 24 | * macKeydownEscape allows you to specify custom behavior when pressing 25 | * escape in an input 26 | * 27 | * @param {expression} macKeydownEscape To evaluate on hitting escape 28 | * 29 | * @example 30 | 31 | 32 |
Key pressed: {{eventKeydown}}
33 |
34 | 35 | */ 36 | 37 | /** 38 | * @ngdoc directive 39 | * @name macKeydownSpace 40 | * 41 | * @description 42 | * macKeydownSpace allows you to specify custom behavior when pressing 43 | * space in an input 44 | * 45 | * @param {expression} macKeydownSpace To evaluate on hitting space 46 | * 47 | * @example 48 | 49 | 50 |
Key pressed: {{eventKeydown}}
51 |
52 | 53 | */ 54 | 55 | /** 56 | * @ngdoc directive 57 | * @name macKeydownLeft 58 | * 59 | * @description 60 | * macKeydownLeft allows you to specify custom behavior when pressing 61 | * left in an input 62 | * 63 | * @param {expression} macKeydownLeft To evaluate on hitting left 64 | * 65 | * @example 66 | 67 | 68 |
Key pressed: {{eventKeydown}}
69 |
70 | 71 | */ 72 | 73 | /** 74 | * @ngdoc directive 75 | * @name macKeydownUp 76 | * 77 | * @description 78 | * macKeydownUp allows you to specify custom behavior when pressing 79 | * up in an input 80 | * 81 | * @param {expression} macKeydownUp To evaluate on hitting up 82 | * 83 | * @example 84 | 85 | 86 |
Key pressed: {{eventKeydown}}
87 |
88 | 89 | */ 90 | 91 | /** 92 | * @ngdoc directive 93 | * @name macKeydownRight 94 | * 95 | * @description 96 | * macKeydownRight allows you to specify custom behavior when pressing 97 | * right in an input 98 | * 99 | * @param {expression} macKeydownRight To evaluate on hitting right 100 | * 101 | * @example 102 | 103 | 104 |
Key pressed: {{eventKeydown}}
105 |
106 | 107 | */ 108 | 109 | /** 110 | * @ngdoc directive 111 | * @name macKeydownDown 112 | * 113 | * @description 114 | * macKeydownDown allows you to specify custom behavior when pressing 115 | * down in an input 116 | * 117 | * @param {expression} macKeydownDown To evaluate on hitting down 118 | * 119 | * @example 120 | 121 | 122 |
Key pressed: {{eventKeydown}}
123 |
124 | 125 | */ 126 | 127 | /** 128 | * 129 | * A directive for handling certain keys on keydown event 130 | * Currently MacGyver supports enter, escape, space, left, up, right and down 131 | * 132 | * @param {Expression} mac-keydown-enter Expression 133 | * @param {Expression} mac-keydown-escape Expression to evaluate on hitting escape 134 | * @param {Expression} mac-keydown-space Expression to evaluate on hitting space 135 | * @param {Expression} mac-keydown-left Expression to evaluate on hitting left 136 | * @param {Expression} mac-keydown-up Expression to evaluate on hitting up 137 | * @param {Expression} mac-keydown-right Expression to evaluate on hitting right 138 | * @param {Expression} mac-keydown-down Expression to evaluate on hitting down 139 | * @private 140 | */ 141 | function keydownFactory (key) { 142 | var name = 'macKeydown' + key; 143 | angular.module('Mac').directive(name, ['$parse', 'keys', function($parse, keys) { 144 | return { 145 | restrict: 'A', 146 | link: function($scope, element, attributes) { 147 | var expr = $parse(attributes[name]); 148 | element.bind('keydown', function($event) { 149 | if ($event.which === keys[key.toUpperCase()]) { 150 | $event.preventDefault(); 151 | $scope.$apply(function() { 152 | expr($scope, {$event: $event}); 153 | }); 154 | } 155 | }); 156 | } 157 | } 158 | }]); 159 | } 160 | 161 | var keydownKeys = ['Enter', 'Escape', 'Space', 'Left', 'Up', 'Right', 'Down']; 162 | keydownKeys.forEach(keydownFactory); 163 | -------------------------------------------------------------------------------- /src/directives/events/pause_typing.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc directive 3 | * @name macPauseTyping 4 | * 5 | * @description 6 | * macPauseTyping directive allow user to specify custom behavior after user stops typing for more than (delay) milliseconds 7 | * 8 | * @param {expression} mac-pause-typing Expression to evaluate after delay 9 | * @param {expression} mac-pause-typing-delay Delay value to evaluate expression (default 800) 10 | * 11 | * @example 12 | 13 | 14 |
value: {{pauseTypingModel}}
15 |
16 | 17 | */ 18 | 19 | angular.module('Mac').directive('macPauseTyping', ['$parse', '$timeout', function($parse, $timeout) { 20 | return { 21 | restrict: 'A', 22 | link: function($scope, element, attrs) { 23 | var expr = $parse(attrs.macPauseTyping), 24 | delay = $scope.$eval(attrs.macPauseTypingDelay) || 800, 25 | keyupTimer; 26 | 27 | element.bind('keyup', function($event) { 28 | if(keyupTimer) { 29 | $timeout.cancel(keyupTimer); 30 | } 31 | 32 | keyupTimer = $timeout(function() { 33 | expr($scope, {$event: $event}); 34 | }, delay); 35 | }); 36 | } 37 | }; 38 | }]); 39 | -------------------------------------------------------------------------------- /src/directives/events/window_resize.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc directive 3 | * @name macWindowResize 4 | * 5 | * @description 6 | * Binding custom behavior on window resize event 7 | * 8 | * @param {expression} mac-window-resize Expression to evaluate on window resize 9 | * 10 | * @example` 11 | 12 |
13 | Current width: {{windowWidth}} 14 |
15 |
16 |
17 | Current width: {{windowWidth}} 18 |
19 | */ 20 | 21 | angular.module('Mac').directive('macWindowResize', ['$parse', '$window', function($parse, $window) { 22 | return { 23 | restrict: 'A', 24 | link: function ($scope, element, attrs) { 25 | var callbackFn = $parse(attrs.macWindowResize), 26 | windowEl = angular.element($window); 27 | 28 | var handler = function($event) { 29 | $scope.$apply(function() { 30 | callbackFn($scope, {$event: $event}); 31 | }); 32 | return true; 33 | }; 34 | 35 | windowEl.bind('resize', handler); 36 | 37 | $scope.$on('$destroy', function() { 38 | windowEl.unbind('resize', handler); 39 | }); 40 | } 41 | }; 42 | }]); 43 | -------------------------------------------------------------------------------- /src/directives/keys.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc constant 3 | * @name keys 4 | * @description 5 | * MacGyver comes with character code enum for easy reference and better 6 | * readability. 7 | * 8 | * | | | | | 9 | * | --- | --- | --- | --- | 10 | * | **CANCEL** - 3 | **FOUR** - 52 | **U** - 85 | **F7** - 118 | 11 | * | **HELP** - 6 | **FIVE** - 53 | **V** - 86 | **F8** - 119 | 12 | * | **BACKSPACE** - 8 | **SIX** - 54 | **W** - 87 | **F9** - 120 | 13 | * | **TAB** - 9 | **SEVEN** - 55 | **X** - 88 | **F10** - 121 | 14 | * | **CLEAR** - 12 | **EIGHT** - 56 | **Y** - 89 | **F11** - 122 | 15 | * | **ENTER** - 13 | **NINE** - 57 | **Z** - 90 | **F12** - 123 | 16 | * | **RETURN** - 13 | **SEMICOLON** - 59 | **CONTEXT_MENU** - 93 | **F13** - 124 | 17 | * | **SHIFT** - 16 | **EQUALS** - 61 | **NUMPAD0** - 96 | **F14** - 125 | 18 | * | **CONTROL** - 17 | **COMMAND** - 91 | **NUMPAD1** - 97 | **F15** - 126 | 19 | * | **ALT** - 18 | **A** - 65 | **NUMPAD2** - 98 | **F16** - 127 | 20 | * | **PAUSE** - 19 | **B** - 66 | **NUMPAD3** - 99 | **F17** - 128 | 21 | * | **CAPS_LOCK** - 20 | **C** - 67 | **NUMPAD4** - 100 | **F18** - 129 | 22 | * | **ESCAPE** - 27 | **D** - 68 | **NUMPAD5** - 101 | **F19** - 130 | 23 | * | **SPACE** - 32 | **E** - 69 | **NUMPAD6** - 102 | **F20** - 131 | 24 | * | **PAGE_UP** - 33 | **F** - 70 | **NUMPAD7** - 103 | **F21** - 132 | 25 | * | **PAGE_DOWN** - 34 | **G** - 71 | **NUMPAD8** - 104 | **F22** - 133 | 26 | * | **END** - 35 | **H** - 72 | **NUMPAD9** - 105 | **F23** - 134 | 27 | * | **HOME** - 36 | **I** - 73 | **MULTIPLY** - 106 | **F24** - 135 | 28 | * | **LEFT** - 37 | **J** - 74 | **ADD** - 107 | **NUM_LOCK** - 144 | 29 | * | **UP** - 38 | **K** - 75 | **SEPARATOR** - 108 | **SCROLL_LOCK** - 145 | 30 | * | **RIGHT** - 39 | **L** - 76 | **SUBTRACT** - 109 | **COMMA** - 188 | 31 | * | **DOWN** - 40 | **M** - 77 | **DECIMAL** - 110 | **PERIOD** - 190 | 32 | * | **PRINT_SCREEN** - 44 | **N** - 78 | **DIVIDE** - 111 | **SLASH** - 191 | 33 | * | **INSERT** - 45 | **O** - 79 | **F1** - 112 | **BACK_QUOTE** - 192 | 34 | * | **DELETE** - 46 | **P** - 80 | **F2** - 113 | **OPEN_BRACKET** - 219 | 35 | * | **ZERO** - 48 | **Q** - 81 | **F3** - 114 | **BACK_SLASH** - 220 | 36 | * | **ONE** - 49 | **R** - 82 | **F4** - 115 | **CLOSE_BRACKET** - 221 | 37 | * | **TWO** - 50 | **S** - 83 | **F5** - 116 | **QUOTE** - 222 | 38 | * | **THREE** - 51 | **T** - 84 | **F6** - 117 | **META** - 224 | 39 | */ 40 | angular.module('Mac').constant('keys', { 41 | CANCEL: 3, 42 | HELP: 6, 43 | BACKSPACE: 8, 44 | TAB: 9, 45 | CLEAR: 12, 46 | ENTER: 13, 47 | RETURN: 13, 48 | SHIFT: 16, 49 | CONTROL: 17, 50 | ALT: 18, 51 | PAUSE: 19, 52 | CAPS_LOCK: 20, 53 | ESCAPE: 27, 54 | SPACE: 32, 55 | PAGE_UP: 33, 56 | PAGE_DOWN: 34, 57 | END: 35, 58 | HOME: 36, 59 | LEFT: 37, 60 | UP: 38, 61 | RIGHT: 39, 62 | DOWN: 40, 63 | PRINT_SCREEN: 44, 64 | INSERT: 45, 65 | DELETE: 46, 66 | ZERO: 48, 67 | ONE: 49, 68 | TWO: 50, 69 | THREE: 51, 70 | FOUR: 52, 71 | FIVE: 53, 72 | SIX: 54, 73 | SEVEN: 55, 74 | EIGHT: 56, 75 | NINE: 57, 76 | SEMICOLON: 59, 77 | EQUALS: 61, 78 | COMMAND: 91, 79 | A: 65, 80 | B: 66, 81 | C: 67, 82 | D: 68, 83 | E: 69, 84 | F: 70, 85 | G: 71, 86 | H: 72, 87 | I: 73, 88 | J: 74, 89 | K: 75, 90 | L: 76, 91 | M: 77, 92 | N: 78, 93 | O: 79, 94 | P: 80, 95 | Q: 81, 96 | R: 82, 97 | S: 83, 98 | T: 84, 99 | U: 85, 100 | V: 86, 101 | W: 87, 102 | X: 88, 103 | Y: 89, 104 | Z: 90, 105 | CONTEXT_MENU: 93, 106 | NUMPAD0: 96, 107 | NUMPAD1: 97, 108 | NUMPAD2: 98, 109 | NUMPAD3: 99, 110 | NUMPAD4: 100, 111 | NUMPAD5: 101, 112 | NUMPAD6: 102, 113 | NUMPAD7: 103, 114 | NUMPAD8: 104, 115 | NUMPAD9: 105, 116 | MULTIPLY: 106, 117 | ADD: 107, 118 | SEPARATOR: 108, 119 | SUBTRACT: 109, 120 | DECIMAL: 110, 121 | DIVIDE: 111, 122 | F1: 112, 123 | F2: 113, 124 | F3: 114, 125 | F4: 115, 126 | F5: 116, 127 | F6: 117, 128 | F7: 118, 129 | F8: 119, 130 | F9: 120, 131 | F10: 121, 132 | F11: 122, 133 | F12: 123, 134 | F13: 124, 135 | F14: 125, 136 | F15: 126, 137 | F16: 127, 138 | F17: 128, 139 | F18: 129, 140 | F19: 130, 141 | F20: 131, 142 | F21: 132, 143 | F22: 133, 144 | F23: 134, 145 | F24: 135, 146 | NUM_LOCK: 144, 147 | SCROLL_LOCK: 145, 148 | COMMA: 188, 149 | PERIOD: 190, 150 | SLASH: 191, 151 | BACK_QUOTE: 192, 152 | OPEN_BRACKET: 219, 153 | BACK_SLASH: 220, 154 | CLOSE_BRACKET: 221, 155 | QUOTE: 222, 156 | META: 224 157 | }); 158 | -------------------------------------------------------------------------------- /src/directives/menu.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc directive 3 | * @name macMenu 4 | * @description 5 | * A directive for creating a menu with multiple items 6 | * 7 | * Menu allows for custom html templating for each item. 8 | * 9 | * Since macMenu is using ngRepeat, some ngRepeat properties along with `item` are exposed on the local scope of each template instance, including: 10 | * 11 | * | Variable | Type | Details | 12 | * |-----------|---------|-----------------------------------------------------------------------------| 13 | * | `$index` | Number | iterator offset of the repeated element (0..length-1) | 14 | * | `$first` | Boolean | true if the repeated element is first in the iterator. | 15 | * | `$middle` | Boolean | true if the repeated element is between the first and last in the iterator. | 16 | * | `$last` | Boolean | true if the repeated element is last in the iterator. | 17 | * | `$even` | Boolean | true if the iterator position `$index` is even (otherwise false). | 18 | * | `$odd` | Boolean | true if the iterator position `$index` is odd (otherwise false). | 19 | * | `item` | Object | item object | 20 | * 21 | * To use custom templating 22 | * ```html 23 | * 24 | * {{item.label}} 25 | * 26 | * ``` 27 | * 28 | * Template default to `{{item.label}}` if not defined 29 | * 30 | * @param {Expression} mac-menu-items List of items to display in the menu 31 | * Each item should have a `label` key as display text 32 | * @param {Function} mac-menu-select Callback on select 33 | * - `index` - {Integer} Item index 34 | * @param {Object} mac-menu-style Styles apply to the menu 35 | * @param {Expression} mac-menu-index Index of selected item 36 | * 37 | * @example 38 | 39 | 40 |
Current selected item: {{selectedItem}}
41 |
42 | 43 | */ 44 | 45 | angular.module('Mac').directive('macMenu', function () { 46 | return { 47 | restrict: 'EA', 48 | replace: true, 49 | templateUrl: 'template/menu.html', 50 | transclude: true, 51 | scope: { 52 | items: '=macMenuItems', 53 | style: '=macMenuStyle', 54 | select: '&macMenuSelect', 55 | pIndex: '=macMenuIndex' 56 | }, 57 | link: function ($scope, element, attrs) { 58 | $scope.selectItem = function (index) { 59 | $scope.select({index: index}); 60 | }; 61 | 62 | $scope.setIndex = function (index) { 63 | $scope.index = index; 64 | 65 | if (attrs.macMenuIndex) { 66 | $scope.pIndex = parseInt(index); 67 | } 68 | }; 69 | 70 | // NOTE: sync local index with user index 71 | if (attrs.macMenuIndex) { 72 | $scope.$watch('pIndex', function (value) { 73 | $scope.index = parseInt(value); 74 | }); 75 | } 76 | 77 | $scope.$watchCollection('items', function (items) { 78 | if (items.length) { 79 | attrs.$addClass('visible'); 80 | } else { 81 | attrs.$removeClass('visible'); 82 | } 83 | }); 84 | } 85 | }; 86 | }). 87 | 88 | // INFO: Used internally by mac-menu 89 | // TODO(adrian): Look into removing this 90 | directive('macMenuTransclude', ['$compile', function ($compile) { 91 | return { 92 | link: function ($scope, element, attrs, ctrls, transclude) { 93 | transclude($scope, function (clone) { 94 | element.empty(); 95 | if (!clone.length) { 96 | // default item template 97 | clone = $compile('{{item.label}}')($scope); 98 | } 99 | element.append(clone); 100 | }); 101 | } 102 | }; 103 | }]); 104 | -------------------------------------------------------------------------------- /src/directives/modal.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc directive 3 | * @name macModal 4 | * @description 5 | * Element directive to define the modal dialog. Modal content is transcluded into a 6 | * modal template 7 | * 8 | * @param {Boolean} mac-modal-keyboard Allow closing modal with keyboard (default false) 9 | * @param {Boolean} mac-modal-overlay-close Allow closing modal when clicking on overlay (default false) 10 | * @param {Boolean} mac-modal-resize Allow modal to resize on window resize event (default false) 11 | * @param {Integer} mac-modal-topOffset Top offset when the modal is larger than window height (default 20) 12 | * @param {Expr} mac-modal-open Callback when the modal is opened 13 | * @param {Expr} mac-modal-before-show Callback before showing the modal 14 | * @param {Expr} mac-modal-after-show Callback when modal is visible with CSS transitions completed 15 | * @param {Expr} mac-modal-before-hide Callback before hiding the modal 16 | * @param {Expr} mac-modal-after-hide Callback when modal is hidden from the user with CSS transitions completed 17 | * @param {Boolean} mac-modal-position Calculate size and position with JS (default true) 18 | * 19 | * @example 20 | 21 | 22 |
23 |

Just another modal

24 |
25 |
26 | 27 |
28 | 29 |
30 |

Just another modal

31 |
32 |
33 | 34 | */ 35 | angular.module('Mac').directive('macModal', [ 36 | '$parse', 37 | 'modal', 38 | 'util', 39 | function($parse, modal, util) { 40 | return { 41 | restrict: 'E', 42 | template: modal.defaults.template, 43 | replace: true, 44 | transclude: true, 45 | link: function($scope, element, attrs, controller, transclude) { 46 | transclude($scope, function (clone) { 47 | angular.element(element[0].querySelector('.mac-modal-content-wrapper')).replaceWith(clone); 48 | }); 49 | 50 | var opts = util.extendAttributes('macModal', modal.defaults, attrs); 51 | 52 | if (opts.overlayClose) { 53 | element.on('click', function ($event) { 54 | if (angular.element($event.target).hasClass('mac-modal-overlay')) { 55 | $scope.$apply(function () { 56 | modal.hide(); 57 | }); 58 | } 59 | }); 60 | } 61 | 62 | var callbacks = ['beforeShow', 'afterShow', 'beforeHide', 'afterHide', 'open']; 63 | callbacks.forEach(function (callback) { 64 | var key = 'macModal' + util.capitalize(callback); 65 | opts[callback] = $parse(attrs[key]) || angular.noop; 66 | }); 67 | 68 | var registerModal = function (id) { 69 | if (!id) return; 70 | 71 | modal.register(id, element, opts); 72 | // NOTE: Remove from modal service when mac-modal directive is removed 73 | // from DOM 74 | $scope.$on('$destroy', function () { 75 | if (modal.opened && modal.opened.id == id) { 76 | modal.hide(); 77 | } 78 | 79 | modal.unregister(id); 80 | }); 81 | } 82 | 83 | if (attrs.id) { 84 | registerModal(attrs.id); 85 | } else { 86 | attrs.$observe('macModal', function (id) { 87 | registerModal(id); 88 | }); 89 | } 90 | } 91 | }; 92 | } 93 | ]) 94 | .directive('macModal', [ 95 | '$parse', 96 | 'modal', 97 | function ($parse, modal) { 98 | return { 99 | restrict: 'A', 100 | link: function ($scope, element, attrs) { 101 | if (!attrs.macModal) { 102 | return; 103 | } 104 | 105 | element.bind('click', function () { 106 | $scope.$apply(function () { 107 | var data = $parse(attrs.macModalData)($scope) || {}; 108 | modal.show(attrs.macModal, { 109 | data: data, 110 | scope: $scope 111 | }); 112 | }); 113 | 114 | return true; 115 | }); 116 | } 117 | }; 118 | } 119 | ]) 120 | .directive('macModalClose', [ 121 | 'modal', 122 | function (modal) { 123 | return { 124 | restrict: 'A', 125 | link: function($scope, element) { 126 | element.bind('click', function () { 127 | $scope.$apply(function () { 128 | modal.hide(); 129 | }); 130 | }); 131 | } 132 | }; 133 | } 134 | ]); 135 | -------------------------------------------------------------------------------- /src/directives/placeholder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc directive 3 | * @name macPlaceholder 4 | * @description 5 | * Dynamically fill out the placeholder text of input 6 | * 7 | * @param {String} mac-placeholder Variable that contains the placeholder text 8 | * 9 | * @example 10 | 11 | 12 | 13 | 14 | */ 15 | 16 | angular.module('Mac').directive('macPlaceholder', function() { 17 | return { 18 | restrict: 'A', 19 | link: function($scope, element, attrs) { 20 | $scope.$watch(attrs.macPlaceholder, function(value) { 21 | attrs.$set('placeholder', value); 22 | }); 23 | } 24 | }; 25 | }); 26 | -------------------------------------------------------------------------------- /src/directives/popover.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc directive 3 | * @name macPopover Trigger 4 | * @description 5 | * Mac popover trigger directive 6 | * 7 | * @param {String} mac-popover ID of the popover to show 8 | * @param {Integer} mac-popover-offset-x Extra x offset (default 0) 9 | * @param {Integer} mac-popover-offset-y Extra y offset (default 0) 10 | * @param {String} mac-popover-container Container for popover 11 | * - Attribute does not exist: document body 12 | * - Attribute without value: Parent element of the popover 13 | * - Attribute with scope variable: Use as container if it is an DOM element 14 | * @param {String} mac-popover-trigger Trigger option, click | hover | focus (default click) 15 | * - click: Popover only opens when user click on trigger 16 | * - hover: Popover shows when user hover on trigger 17 | * - focus: Popover shows when focus on input element 18 | * 19 | * @example 20 | 21 | 29 | Open a popover 30 | Open a popover 31 | Open a popover 32 | Open a popover 33 | Open a popover 34 | Open a popover 35 | 36 | Above left 37 | */ 38 | 39 | angular.module('Mac').directive('macPopover', [ 40 | '$timeout', 41 | 'macPopoverDefaults', 42 | 'popover', 43 | 'util', 44 | function ($timeout, defaults, popover, util) { 45 | return { 46 | restrict: 'A', 47 | link: function ($scope, element, attrs) { 48 | var options, delayId, closeDelayId, unobserve; 49 | 50 | options = util.extendAttributes('macPopover', defaults.trigger, attrs); 51 | 52 | /** 53 | * Clearing show and/or hide delays 54 | */ 55 | function clearDelays () { 56 | if (delayId) { 57 | $timeout.cancel(delayId); 58 | delayId = null; 59 | } 60 | if (closeDelayId) { 61 | $timeout.cancel(closeDelayId); 62 | closeDelayId = null; 63 | } 64 | } 65 | 66 | /** 67 | * Check if popover should be shown, and show popover with service 68 | * @param {string} id 69 | * @param {Number} delay (default 0) 70 | */ 71 | function show (id, delay) { 72 | delay = delay || 0; 73 | 74 | clearDelays(); 75 | delayId = $timeout(function () { 76 | var last = popover.last(); 77 | 78 | // Close the last popover 79 | // If the trigger is the same, `show` acts as a toggle 80 | if (last) { 81 | popover.hide(); 82 | if (element[0] === last.element[0]) { 83 | return true; 84 | } 85 | } 86 | 87 | // Add current scope to option for compiling popover later 88 | options.scope = $scope; 89 | popover.show(id, element, options); 90 | }, delay); 91 | } 92 | 93 | /** 94 | * Hide popover 95 | * @param {Element} element 96 | * @param {Number} delay (default 0) 97 | */ 98 | function hide (element, delay) { 99 | delay = delay || 0; 100 | 101 | clearDelays(); 102 | closeDelayId = $timeout(function () { 103 | popover.hide(element); 104 | }, delay); 105 | } 106 | 107 | // NOTE: Only bind once 108 | unobserve = attrs.$observe('macPopover', function (id) { 109 | var showEvent, hideEvent; 110 | 111 | if (!id) return; 112 | 113 | if (options.trigger === 'click') { 114 | element.bind('click', function () { 115 | show(id, 0); 116 | }); 117 | } else { 118 | showEvent = options.trigger === 'focus' ? 'focusin' : 'mouseenter'; 119 | hideEvent = options.trigger === 'focus' ? 'focusout' : 'mouseleave'; 120 | 121 | element.bind(showEvent, function () { 122 | show(id, 400); 123 | }); 124 | element.bind(hideEvent, function () { 125 | hide(element, 500); 126 | }); 127 | 128 | unobserve(); 129 | } 130 | }); 131 | 132 | // Hide popover before trigger gets destroyed 133 | $scope.$on('$destroy', function () { 134 | hide(element, 0); 135 | }); 136 | } 137 | }; 138 | } 139 | ]). 140 | 141 | /** 142 | * @ngdoc directive 143 | * @name macPopover Element 144 | * @description 145 | * Element directive to define popover 146 | * 147 | * @param {String} id Modal id 148 | * @param {Bool} mac-popover-footer Show footer or not 149 | * @param {Bool} mac-popover-header Show header or not 150 | * @param {String} mac-popover-title Popover title 151 | * @param {String} mac-popover-direction Popover direction (default "above left") 152 | * @param {String} mac-popover-refresh-on Event to update popover size and position 153 | * - above, below or middle - Place the popover above, below or center align the trigger element 154 | * - left or right - Place tip on the left or right of the popover 155 | * 156 | * @example 157 | 158 | 166 | Open a popover 167 | Open a popover 168 | Open a popover 169 | Open a popover 170 | Open a popover 171 | Open a popover 172 | 173 | Open a popover 174 | */ 175 | directive('macPopover', [ 176 | 'macPopoverDefaults', 177 | 'popover', 178 | 'util', 179 | function (defaults, popover, util) { 180 | return { 181 | restrict: 'E', 182 | compile: function (element, attrs) { 183 | var opts; 184 | if (!attrs.id) { 185 | throw Error('macPopover: Missing id'); 186 | } 187 | 188 | opts = util.extendAttributes('macPopover', defaults.element, attrs); 189 | angular.extend(opts, {template: element.html()}); 190 | 191 | // link function 192 | return function ($scope, element, attrs) { 193 | var unobserve = attrs.$observe('id', function (value) { 194 | var commentEl; 195 | 196 | // Register the popover with popover service 197 | popover.register(value, opts); 198 | 199 | // Replace original element with comment once element is cached 200 | commentEl = document.createComment('macPopover: ' + value); 201 | element.replaceWith(commentEl); 202 | 203 | unobserve(); 204 | }); 205 | }; 206 | } 207 | }; 208 | } 209 | ]). 210 | 211 | /** 212 | * An internal directive to compile popover template 213 | * @private 214 | */ 215 | directive('macPopoverFillContent', ['$compile', function ($compile) { 216 | return { 217 | restrict: 'A', 218 | link: function ($scope, element) { 219 | element.html($scope.macPopoverTemplate); 220 | $compile(element.contents())($scope); 221 | } 222 | }; 223 | }]); 224 | -------------------------------------------------------------------------------- /src/directives/scroll_spy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc directive 3 | * @name macScrollSpy 4 | * @description 5 | * Element to spy scroll event on 6 | * 7 | * @param {Integer} mac-scroll-spy-offset Top offset when calculating scroll position 8 | * @example 9 | 10 | */ 11 | angular.module('Mac').directive('macScrollSpy', [ 12 | '$window', 13 | 'scrollSpy', 14 | 'scrollSpyDefaults', 15 | 'util', 16 | function ($window, scrollSpy, defaults, util) { 17 | return { 18 | link: function ($scope, element, attrs) { 19 | var options, spyElement; 20 | 21 | options = util.extendAttributes('macScrollSpy', defaults, attrs); 22 | 23 | // NOTE: Look into using something other than $window 24 | spyElement = element[0].tagName == 'BODY' ? angular.element($window) : element; 25 | 26 | spyElement.bind('scroll', function () { 27 | var scrollTop, scrollHeight, maxScroll, i, anchor; 28 | 29 | // NOTE: exit immediately if no items are registered 30 | if (scrollSpy.registered.length == 0) { 31 | return true; 32 | } 33 | 34 | scrollTop = spyElement.scrollTop() + options.offset; 35 | scrollHeight = spyElement[0].scrollHeight || element[0].scrollHeight; 36 | maxScroll = scrollHeight - spyElement.height(); 37 | 38 | // Select the last anchor when scrollTop is over maxScroll 39 | if (scrollTop >= maxScroll) { 40 | return scrollSpy.setActive(scrollSpy.last()); 41 | } 42 | 43 | for (i = 0; i < scrollSpy.registered.length; i++) { 44 | anchor = scrollSpy.registered[i]; 45 | if (scrollTop >= anchor.top && 46 | (!scrollSpy.registered[i + 1] || scrollTop <= scrollSpy.registered[i + 1].top)) { 47 | $scope.$apply(function () { 48 | scrollSpy.setActive(anchor); 49 | }); 50 | return true; 51 | } 52 | } 53 | }); 54 | } 55 | } 56 | }]). 57 | 58 | /** 59 | * @ngdoc directive 60 | * @name macScrollSpyAnchor 61 | * @description 62 | * Section in the spied element 63 | * @param {String} id Id to identify anchor 64 | * @param {String} mac-scroll-spy-anchor ID to identify anchor (use either element or this attribute) 65 | * @param {Event} refresh-scroll-spy To refresh the top offset of all scroll anchors 66 | * @example 67 | 68 | */ 69 | directive('macScrollSpyAnchor', ['scrollSpy', function (scrollSpy) { 70 | return { 71 | link: function ($scope, element, attrs) { 72 | var id = attrs.id || attrs.macScrollSpyAnchor; 73 | 74 | if (!id) { 75 | throw new Error('Missing scroll spy anchor id'); 76 | } 77 | 78 | var anchor = scrollSpy.register(id, element); 79 | 80 | $scope.$on('$destroy', function () { 81 | scrollSpy.unregister(id); 82 | }); 83 | 84 | // Re-register anchor to update position/offset 85 | $scope.$on('refresh-scroll-spy', function () { 86 | if (anchor) { 87 | scrollSpy.updateOffset(anchor); 88 | } 89 | }); 90 | } 91 | } 92 | }]). 93 | 94 | /** 95 | * @ngdoc directive 96 | * @name macScrollSpyTarget 97 | * @description 98 | * Element to highlight when anchor scroll into view 99 | * @param {String} mac-scroll-spy-target Name of the anchor 100 | * @param {String} mac-scroll-spy-target-class Class to apply for highlighting (default active) 101 | * 102 | * @example 103 | 107 | */ 108 | directive('macScrollSpyTarget', ['scrollSpy', 'scrollSpyDefaults', function (scrollSpy, defaults) { 109 | return { 110 | link: function ($scope, element, attrs) { 111 | var target = attrs.macScrollSpyTarget; 112 | var highlightClass = attrs.macScrollSpyTargetClass || defaults.highlightClass; 113 | 114 | if (!target) { 115 | throw new Error('Missing scroll spy target name'); 116 | } 117 | 118 | var callback = function (active) { 119 | element.toggleClass(highlightClass, target == active.id); 120 | } 121 | 122 | // update target class if target is re-rendered 123 | if (scrollSpy.active) { 124 | callback(scrollSpy.active); 125 | } 126 | 127 | scrollSpy.addListener(callback); 128 | $scope.$on('$destroy', function () { 129 | scrollSpy.removeListener(callback); 130 | }); 131 | } 132 | }; 133 | }]); 134 | -------------------------------------------------------------------------------- /src/directives/spinner.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc directive 3 | * @name macSpinner 4 | * @description 5 | * A directive for generating spinner 6 | * 7 | * @param {Integer} mac-spinner-size The size of the spinner (default 16) 8 | * @param {Integer} mac-spinner-z-index The z-index (default inherit) 9 | * @param {String} mac-spinner-color Color of all the bars (default #2f3035) 10 | * 11 | * @example 12 | Basic setup 13 | 14 | 15 | 16 | 17 | */ 18 | 19 | angular.module('Mac').directive('macSpinner', ['util', function (util) { 20 | var updateBars = function (bars, propertyName, value) { 21 | var i, property; 22 | if (angular.isObject(propertyName)) { 23 | for (property in propertyName) { 24 | updateBars(bars, property, propertyName[property]); 25 | } 26 | return; 27 | } 28 | 29 | for (i = 0; i < bars.length; i++) { 30 | bars[i].style[propertyName] = value; 31 | } 32 | }, 33 | setSpinnerSize = function (element, bars, size) { 34 | if (!size) { 35 | return; 36 | } 37 | 38 | updateBars(bars, { 39 | height: size * 0.32 + 'px', 40 | left: size * 0.445 + 'px', 41 | top: size * 0.37 + 'px', 42 | width: size * 0.13 + 'px', 43 | borderRadius: size * 0.32 * 2 + 'px', 44 | position: 'absolute' 45 | }); 46 | 47 | if (!isNaN(+size) && angular.isNumber(+size)) { 48 | size = size + 'px'; 49 | } 50 | 51 | element.css({ 52 | height: size, 53 | width: size 54 | }); 55 | }, 56 | defaults = { 57 | size: 16, 58 | zIndex: 'inherit', 59 | color: '#2f3035' 60 | }; 61 | 62 | return { 63 | restrict: 'E', 64 | replace: true, 65 | template: '
', 66 | 67 | compile: function (element) { 68 | var i, bars = [], 69 | animateCss = util.getCssVendorName(element[0], 'animation'), 70 | transformCss = util.getCssVendorName(element[0], 'transform'), 71 | delay, degree, styl, bar; 72 | 73 | for (i = 0; i < 10; i++) { 74 | delay = i * 0.1 - 1 + !i; 75 | degree = i * 36; 76 | styl = {}; 77 | bar = angular.element('
'); 78 | // Cache each bar for css updates 79 | bars.push(bar[0]); 80 | 81 | styl[animateCss] = 'fade 1s linear infinite ' + delay + 's'; 82 | styl[transformCss] = 'rotate(' + degree + 'deg) translate(0, 130%)'; 83 | bar.css(styl); 84 | 85 | element.append(bar); 86 | } 87 | 88 | return function ($scope, element, attrs) { 89 | if (attrs.macSpinnerSize) { 90 | attrs.$observe('macSpinnerSize', function (value) { 91 | setSpinnerSize(element, bars, value); 92 | }); 93 | } else { 94 | setSpinnerSize(element, bars, defaults.size); 95 | } 96 | 97 | attrs.$observe('macSpinnerZIndex', function (value) { 98 | if (value) { 99 | element.css('z-index', value); 100 | } 101 | }); 102 | 103 | if (attrs.macSpinnerColor) { 104 | attrs.$observe('macSpinnerColor', function (value) { 105 | if (value) { 106 | updateBars(bars, 'background', value); 107 | } 108 | }); 109 | } else { 110 | updateBars(bars, 'background', defaults.color) 111 | } 112 | }; 113 | } 114 | }; 115 | }]); 116 | -------------------------------------------------------------------------------- /src/directives/tag_autocomplete.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc directive 3 | * @name macTagAutocomplete 4 | * @description 5 | * A directive for generating tag input with autocomplete support on text input. 6 | * Tag autocomplete has priority 800 7 | * 8 | * @param {String} mac-tag-autocomplete-source Data to use. 9 | * Source support multiple types: 10 | * - Array: An array can be used for local data and there are two supported formats: 11 | * - An array of strings: ["Item1", "Item2"] 12 | * - An array of objects with mac-autocomplete-label key: [{name:"Item1"}, {name:"Item2"}] 13 | * - String: Using a string as the source is the same as passing the variable into mac-autocomplete-url 14 | * - Function: A callback when querying for data. The callback receive two arguments: 15 | * - {String} Value currently in the text input 16 | * - {Function} A response callback which expects a single argument, data to user. The data will be 17 | * populated on the menu and the menu will adjust accordingly 18 | * @param {String} mac-tag-autocomplete-value The value to be sent back upon selection (default "id") 19 | * @param {String} mac-tag-autocomplete-label The label to display to the users (default "name") 20 | * @param {Expr} mac-tag-autocomplete-model Model for autocomplete 21 | * @param {Array} mac-tag-autocomplete-selected The list of elements selected by the user (required) 22 | * @param {String} mac-tag-autocomplete-query The query parameter on GET command (defualt "q") 23 | * @param {Integer} mac-tag-autocomplete-delay Time delayed on fetching autocomplete data after keyup (default 800) 24 | * @param {String} mac-tag-autocomplete-placeholder Placeholder text of the text input (default "") 25 | * @param {Boolean} mac-tag-autocomplete-disabled If autocomplete is enabled or disabled (default false) 26 | * @param {Function} mac-tag-autocomplete-on-success Function called on successful ajax request (Proxy attribute for macAutocomplete) 27 | * - `data` - {Object} Data returned from the request 28 | * - `status` - {Number} The status code of the response 29 | * - `header` - {Object} Header of the response 30 | * @param {Expr} mac-tag-autocomplete-on-enter When autocomplete is disabled, this function is called on enter, Should return either string, object or boolean. If false, item is not added 31 | * - `item` - {String} User input 32 | * @param {Event} mac-tag-autocomplete-clear-input $broadcast message; clears text input when received 33 | * 34 | * @param {expression} mac-tag-autocomplete-blur Callback function on blur 35 | * - `$event` - {Event} Event object 36 | * - `ctrl` - {MacTagAutocompleteController} Tag autocomplete element controller 37 | * - `value` - {String} Text input 38 | * @param {expression} mac-tag-autocomplete-focus Callback function on focus 39 | * - `$event` - {Event} Event object 40 | * - `ctrl` - {MacTagAutocompleteController} Tag autocomplete element controller 41 | * - `value` - {String} Text input 42 | * @param {expression} mac-tag-autocomplete-keyup Callback function on keyup 43 | * - `$event` - {Event} Event object 44 | * - `ctrl` - {MacTagAutocompleteController} Tag autocomplete element controller 45 | * - `value` - {String} Text input 46 | * @param {expression} mac-tag-autocomplete-keydown Callback function on keydown 47 | * - `$event` - {Event} Event object 48 | * - `ctrl` - {MacTagAutocompleteController} Tag autocomplete element controller 49 | * - `value` - {String} Text input 50 | * @param {expression} mac-tag-autocomplete-keypress Callback function on keypress 51 | * - `$event` - {Event} Event object 52 | * - `ctrl` - {MacTagAutocompleteController} Tag autocomplete element controller 53 | * - `value` - {String} Text input 54 | * 55 | * @example 56 | Basic example 57 | 58 | 67 | 68 | 77 | * 78 | * @example 79 | Example with autocomplete disabled 80 | 81 | 88 | 89 | 96 | */ 97 | 98 | angular.module('Mac').directive('macTagAutocomplete', [ 99 | function () { 100 | return { 101 | restrict: 'E', 102 | templateUrl: 'template/tag_autocomplete.html', 103 | replace: true, 104 | priority: 800, 105 | scope: { 106 | source: '=macTagAutocompleteSource', 107 | placeholder: '=macTagAutocompletePlaceholder', 108 | selected: '=macTagAutocompleteSelected', 109 | disabled: '=macTagAutocompleteDisabled', 110 | model: '=macTagAutocompleteModel', 111 | onSuccessFn: '&macTagAutocompleteOnSuccess', 112 | onEnterFn: '&macTagAutocompleteOnEnter', 113 | onKeydownFn: '&macTagAutocompleteOnKeydown', 114 | labelKey: '@macTagAutocompleteLabel' 115 | }, 116 | controller: 'MacTagAutocompleteController', 117 | controllerAs: 'macTagAutocomplete', 118 | bindToController: true, 119 | 120 | compile: function (element, attrs) { 121 | var labelKey = attrs.macTagAutocompleteLabel != undefined ? 122 | attrs.macTagAutocompleteLabel : 'name'; 123 | 124 | var delay = +attrs.macTagAutocompleteDelay; 125 | delay = isNaN(delay) ? 800 : delay; 126 | 127 | var textInput = angular.element(element[0].querySelector('.mac-autocomplete')); 128 | textInput.attr({ 129 | 'mac-autocomplete-label': labelKey, 130 | 'mac-autocomplete-query': attrs.macTagAutocompleteQuery || 'q', 131 | 'mac-autocomplete-delay': delay 132 | }); 133 | 134 | return function ($scope, element, attrs, ctrl) { 135 | // NOTE: Proxy is created to prevent tag autocomplete from breaking 136 | // when user did not specify model 137 | if (attrs.macTagAutocompleteModel) { 138 | $scope.$watch('macTagAutocomplete.textInput', function (value) { $scope.model = value;}); 139 | $scope.$watch('macTagAutocomplete.model', function (value) { ctrl.textInput = value;}); 140 | } 141 | 142 | element.bind('click', function () { 143 | var textInputDOM = element[0].querySelector('.mac-autocomplete'); 144 | textInputDOM.focus(); 145 | }); 146 | 147 | $scope.$on('mac-tag-autocomplete-clear-input', function () { 148 | ctrl.textInput = ''; 149 | }); 150 | }; 151 | } 152 | } 153 | } 154 | ]); 155 | 156 | function macAutocompleteEventFactory (key) { 157 | var name = 'macTagAutocomplete' + key; 158 | var eventName = key.toLowerCase(); 159 | 160 | angular.module('Mac').directive(name, [ 161 | '$parse', 162 | function ($parse) { 163 | return { 164 | restrict: 'A', 165 | priority: 700, 166 | require: 'macTagAutocomplete', 167 | link: function ($scope, element, attrs, ctrl) { 168 | var input = angular.element(element[0].querySelector('.mac-autocomplete')); 169 | var expr = $parse(attrs[name]); 170 | 171 | if (!input) return; 172 | 173 | input.bind(eventName, function($event) { 174 | $scope.$apply(function() { 175 | expr($scope, { 176 | $event: $event, 177 | ctrl: ctrl, 178 | value: ctrl.textInput 179 | }); 180 | }); 181 | }); 182 | } 183 | } 184 | } 185 | ]); 186 | } 187 | 188 | var macAutocompleteEvents = ['Blur', 'Focus', 'Keyup', 'Keydown', 'Keypress']; 189 | macAutocompleteEvents.forEach(macAutocompleteEventFactory); 190 | -------------------------------------------------------------------------------- /src/directives/time.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc directive 3 | * @name macTime 4 | * @description 5 | * A directive for creating a time input field. Time input can use any `ng-` attributes support by text input type. 6 | * 7 | * @param {String} ng-model Assignable angular expression to data-bind to 8 | * Clearing model by setting it to null or '' will set model back to default value 9 | * @param {String} name Property name of the form under which the control is published 10 | * @param {String} required Adds `required` validation error key if the value is not entered. 11 | * @param {String} ng-required Adds `required` attribute and `required` validation constraint to 12 | * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of 13 | * `required` when you want to data-bind to the `required` attribute. 14 | * @param {String} ng-pattern Sets `pattern` validation error key if the value does not match the 15 | * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for 16 | * patterns defined as scope expressions. 17 | * @param {String} ng-change Angular expression to be executed when input changes due to user interaction with the input element. 18 | * @param {String} ng-disabled Enable or disable time input 19 | * 20 | * @param {String} mac-time-default If model is undefined, use this as the starting value (default 12:00 PM) 21 | * 22 | * @example 23 | Basic setup 24 | 25 | 26 | 27 | 28 | */ 29 | 30 | angular.module('Mac').directive('macTime', [ 31 | 'keys', 32 | 'util', 33 | 'macTimeDefaults', 34 | 'macTimeUtil', 35 | function (keys, util, defaults, timeUtil) { 36 | return { 37 | restrict: 'E', 38 | require: 'ngModel', 39 | replace: true, 40 | template: '', 41 | link: function ($scope, element, attrs, ngModelCtrl) { 42 | var opts, time, timeValidator, whitelistKeys; 43 | 44 | opts = util.extendAttributes('macTime', defaults, attrs); 45 | 46 | whitelistKeys = [keys.UP, 47 | keys.DOWN, 48 | keys.LEFT, 49 | keys.RIGHT, 50 | keys.A, 51 | keys.P 52 | ]; 53 | 54 | // Set default placeholder 55 | if (!attrs.placeholder) { 56 | attrs.$set('placeholder', opts.placeholder); 57 | } 58 | 59 | // Validation 60 | timeValidator = function (value) { 61 | if (!value || util.validateTime(value)) { 62 | ngModelCtrl.$setValidity('time', true); 63 | return value; 64 | } else { 65 | ngModelCtrl.$setValidity('time', false); 66 | return undefined; 67 | } 68 | }; 69 | 70 | ngModelCtrl.$formatters.push(timeValidator); 71 | ngModelCtrl.$parsers.push(timeValidator); 72 | 73 | time = timeUtil.initializeTime(opts); 74 | 75 | element.on('blur', function () { 76 | $scope.$apply(function () { 77 | timeUtil.updateInput(time, ngModelCtrl); 78 | }); 79 | }); 80 | 81 | /** 82 | * Note: The initial click into the input will not update the time because the 83 | * model is empty. The selection by default should be hour 84 | */ 85 | element.on('click', function () { 86 | $scope.$apply(function() { 87 | var isModelSet = !!ngModelCtrl.$modelValue; 88 | 89 | timeUtil.updateTime(time, ngModelCtrl); 90 | timeUtil.updateInput(time, ngModelCtrl); 91 | 92 | // After the initial view update, selectionStart is set to the end. 93 | // This is not the desired behavior as it should select hour by default 94 | if (!isModelSet) { 95 | timeUtil.selectHours(element); 96 | return; 97 | } 98 | 99 | switch (timeUtil.getSelection(element)) { 100 | case 'hour': 101 | timeUtil.selectHours(element); 102 | break; 103 | case 'minute': 104 | timeUtil.selectMinutes(element); 105 | break; 106 | case 'meridian': 107 | timeUtil.selectMeridian(element); 108 | break; 109 | } 110 | }); 111 | 112 | return true; 113 | }); 114 | 115 | element.on('keydown', function (event) { 116 | var key = event.which; 117 | 118 | if (whitelistKeys.indexOf(key) === -1) { 119 | return true; 120 | } 121 | 122 | event.preventDefault(); 123 | 124 | $scope.$apply(function () { 125 | var change, selection = timeUtil.getSelection(element), meridian; 126 | 127 | if (key === keys.UP || key === keys.DOWN) { 128 | change = key === keys.UP ? 1 : -1; 129 | 130 | switch (selection) { 131 | case 'hour': 132 | timeUtil.incrementHour(time, change); 133 | timeUtil.selectHours(element); 134 | break; 135 | case 'minute': 136 | timeUtil.incrementMinute(time, change); 137 | timeUtil.selectMinutes(element); 138 | break; 139 | case 'meridian': 140 | timeUtil.toggleMeridian(time); 141 | timeUtil.selectMeridian(element); 142 | break; 143 | } 144 | 145 | timeUtil.updateInput(time, ngModelCtrl); 146 | 147 | } else if (key === keys.LEFT) { 148 | timeUtil.selectPreviousSection(element); 149 | timeUtil.updateInput(time, ngModelCtrl); 150 | 151 | } else if (key === keys.RIGHT) { 152 | timeUtil.selectNextSection(element); 153 | timeUtil.updateInput(time, ngModelCtrl); 154 | 155 | } else if ((key === keys.A || key === keys.P) && selection === 'meridian') { 156 | meridian = key === keys.A ? 'AM' : 'PM'; 157 | timeUtil.setMeridian(time, meridian); 158 | 159 | timeUtil.updateInput(time, ngModelCtrl); 160 | timeUtil.selectMeridian(element); 161 | } 162 | }); 163 | }); 164 | 165 | element.on('keyup', function (event) { 166 | var key = event.which; 167 | 168 | if (!((keys.NUMPAD0 <= key && key <= keys.NUMPAD9) || (keys.ZERO <= key && key <= keys.NINE))) { 169 | event.preventDefault(); 170 | } 171 | 172 | $scope.$apply(function () { 173 | timeUtil.updateTime(time, ngModelCtrl); 174 | }); 175 | }); 176 | } 177 | }; 178 | } 179 | ]); 180 | -------------------------------------------------------------------------------- /src/directives/tooltip.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc directive 3 | * @name macTooltip 4 | * @description 5 | * Tooltip directive 6 | * 7 | * @param {String} mac-tooltip Text to show in tooltip 8 | * @param {String} mac-tooltip-direction Direction of tooltip (default 'top') 9 | * @param {String} mac-tooltip-trigger How tooltip is triggered (default 'hover') 10 | * @param {Boolean} mac-tooltip-inside Should the tooltip be appended inside element (default false) 11 | * @param {Expr} mac-tooltip-disabled Disable and enable tooltip 12 | * 13 | * @example 14 | 15 | 21 | 22 | Tooltip on bottom 23 | */ 24 | 25 | /** 26 | * NOTE: This directive does not use $animate to append and remove DOM element or 27 | * add and remove classes in order to optimize showing tooltips by eliminating 28 | * the need for firing a $digest cycle. 29 | */ 30 | 31 | angular.module('Mac').directive('macTooltip', [ 32 | '$timeout', 33 | 'macTooltipDefaults', 34 | 'util', 35 | function ($timeout, defaults, util) { 36 | return { 37 | restrict: 'A', 38 | link: function ($scope, element, attrs) { 39 | var tooltip, text, disabled, unobserve, closeDelay, opts; 40 | 41 | opts = util.extendAttributes('macTooltip', defaults, attrs); 42 | 43 | function showTip () { 44 | var container, offset, elementSize, tooltipSize, messageEl; 45 | 46 | if (disabled || !text || tooltip) { 47 | return true; 48 | } 49 | 50 | container = opts.inside ? element : angular.element(document.body); 51 | 52 | // Check if the tooltip still exists, remove if it does 53 | removeTip(0); 54 | 55 | messageEl = angular.element('
'); 56 | messageEl.text(text); 57 | 58 | tooltip = angular.element('
').addClass('mac-tooltip ' + opts.direction); 59 | tooltip.append(messageEl); 60 | 61 | container.append(tooltip); 62 | 63 | // Only get element offset when not adding tooltip within the element 64 | offset = opts.inside ? {top: 0, left: 0} : element.offset(); 65 | 66 | // Get element height and width 67 | elementSize = { 68 | width: element.outerWidth(), 69 | height: element.outerHeight() 70 | }; 71 | 72 | // Get tooltip width and height 73 | tooltipSize = { 74 | width: tooltip.outerWidth(), 75 | height: tooltip.outerHeight() 76 | }; 77 | 78 | // Adjust offset based on direction 79 | switch (opts.direction) { 80 | case 'bottom': 81 | case 'top': 82 | offset.left += elementSize.width / 2 - tooltipSize.width / 2; 83 | break; 84 | case 'left': 85 | case 'right': 86 | offset.top += elementSize.height / 2 - tooltipSize.height / 2; 87 | break; 88 | } 89 | 90 | if (opts.direction == 'bottom') { 91 | offset.top += elementSize.height; 92 | } else if (opts.direction == 'top') { 93 | offset.top -= tooltipSize.height; 94 | } else if (opts.direction == 'left') { 95 | offset.left -= tooltipSize.width; 96 | } else if (opts.direction == 'right') { 97 | offset.left += elementSize.width; 98 | } 99 | 100 | // Set the offset 101 | angular.forEach(offset, function (value, key) { 102 | if (!isNaN(+value) && angular.isNumber(+value)) { 103 | value = value + "px"; 104 | } 105 | 106 | tooltip.css(key, value); 107 | }); 108 | 109 | tooltip.addClass(opts.class); 110 | return true; 111 | } 112 | 113 | function removeTip (delay) { 114 | delay = delay === undefined ? 100 : delay; 115 | 116 | if (tooltip && !closeDelay) { 117 | tooltip.removeClass(opts.class); 118 | 119 | closeDelay = $timeout(function () { 120 | if (tooltip) { 121 | tooltip.remove(); 122 | } 123 | tooltip = null; 124 | closeDelay = null; 125 | }, delay, false); 126 | } 127 | 128 | return true; 129 | } 130 | 131 | function toggle () { return tooltip ? removeTip() : showTip(); } 132 | 133 | // Calling unobserve in the callback to simulate observeOnce 134 | unobserve = attrs.$observe('macTooltip', function (value) { 135 | if (value === undefined) { 136 | return; 137 | } 138 | 139 | text = value; 140 | 141 | if (opts.trigger !== 'hover' && opts.trigger !== 'click') { 142 | throw new Error('macTooltip: Invalid trigger'); 143 | } 144 | 145 | if (opts.trigger === 'click') { 146 | element.bind('click', toggle); 147 | } else if (opts.trigger === 'hover') { 148 | element.bind('mouseenter', showTip); 149 | element.bind('mouseleave click', function () { 150 | removeTip(); 151 | }); 152 | } 153 | 154 | unobserve(); 155 | }); 156 | 157 | if (attrs.macTooltipDisabled) { 158 | $scope.$watch(attrs.macTooltipDisabled, function (value) { 159 | disabled = !!value; 160 | }); 161 | } 162 | 163 | $scope.$on('$destroy', function () { 164 | removeTip(0); 165 | }); 166 | } 167 | }; 168 | } 169 | ]); 170 | -------------------------------------------------------------------------------- /src/filters/boolean.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @name booleanFactory 3 | * @description 4 | * A boolean factory that creates a function that returns certain strings 5 | * based on the `boolean` variable 6 | * @param {string} trueDefault Default string for true 7 | * @param {string} falseDefault Default string for false 8 | * @returns {Function} 9 | * @private 10 | */ 11 | function booleanFactory(trueDefault, falseDefault) { 12 | return function() { 13 | return function(boolean, trueString, falseString) { 14 | trueString = trueString || trueDefault; 15 | falseString = falseString || falseDefault; 16 | return boolean ? trueString : falseString; 17 | }; 18 | }; 19 | } 20 | 21 | /** 22 | * @ngdoc filter 23 | * @name boolean 24 | * @description 25 | * Print out string based on passed in value 26 | * 27 | * @param {*} boolean Value to check 28 | * @param {string} trueString String to print when boolean is truthy 29 | * @param {string} falseString String to print when boolean is falsy 30 | * @returns {string} Either trueString or falseString based on boolean 31 | * 32 | * @example 33 | 34 | */ 35 | var booleanFilter = booleanFactory('true', 'false'); 36 | 37 | /** 38 | * @ngdoc filter 39 | * @name true 40 | * @description 41 | * Print out string when boolean is truthy 42 | * 43 | * @param {*} boolean Value to check 44 | * @param {string} trueString String to print when boolean is truthy 45 | * @returns {string} 46 | * 47 | * @example 48 | 49 | */ 50 | var trueFilter = booleanFactory('true', ''); 51 | 52 | /** 53 | * @ngdoc filter 54 | * @name false 55 | * @description 56 | * Print out string when boolean is falsy 57 | * 58 | * @param {*} boolean Value to check 59 | * @param {string} falseString String to print when boolean is falsy 60 | * @returns {string} 61 | * 62 | * @example 63 | 64 | */ 65 | var falseFilter = booleanFactory('', 'false'); 66 | 67 | angular.module('Mac') 68 | .filter('boolean', booleanFilter) 69 | .filter('true', trueFilter) 70 | .filter('false', falseFilter); 71 | -------------------------------------------------------------------------------- /src/filters/list.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc filter 3 | * @name list 4 | * @description 5 | * List filter. Use for converting arrays into a string 6 | * 7 | * @param {Array} list Array of items 8 | * @param {String} separator String to separate each element of the array (default ,) 9 | * @returns {String} Formatted string 10 | * 11 | * @example 12 | {{['item1', 'item2', 'item3'] | list}} 13 | */ 14 | 15 | angular.module('Mac').filter('list', function() { 16 | return function(list, separator) { 17 | if (!separator) { 18 | separator = ', '; 19 | } 20 | 21 | if (!angular.isArray(list)) return list; 22 | 23 | return list.join(separator); 24 | }; 25 | }); 26 | -------------------------------------------------------------------------------- /src/filters/pluralize.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc filter 3 | * @name pluralize 4 | * @description 5 | * Pluralizes the given string. It's a simple proxy to the pluralize function on util. 6 | * 7 | * @param {String} string Noun to pluralize 8 | * @param {Integer} count The numer of objects 9 | * @param {Boolean} includeCount To include the number in formatted string 10 | * @returns {String} Formatted plural 11 | * 12 | * @example 13 | 14 |
15 |
Single
16 |
{{"person" | pluralize: 1}}
17 |
Multiple
18 |
{{"person" | pluralize: 20}}
19 |
20 |
21 | {{dog | pluralize:10:true}} 22 | */ 23 | 24 | angular.module('Mac').filter('pluralize', ['util', function(util) { 25 | return function(string, count, includeCount) { 26 | // Default includeCount to true 27 | if (includeCount === undefined) { 28 | includeCount = true; 29 | } 30 | 31 | return util.pluralize(string, count, includeCount); 32 | }; 33 | }]); 34 | -------------------------------------------------------------------------------- /src/filters/timestamp.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc filter 3 | * @name timestamp 4 | * @description 5 | * Takes in a unix timestamp and turns it into a human-readable relative time string, like "5 6 | * minutes ago" or "just now". 7 | * 8 | * @param {integer} time The time to format 9 | * @returns {String} Formatted string 10 | * 11 | * @example 12 | 13 |
14 |
Now - 5 minutes
15 |
{{ fiveMinAgo | timestamp}}
16 |
Now - 1 day
17 |
{{ oneDayAgo | timestamp}}
18 |
Now - 3 days
19 |
{{ threeDaysAgo | timestamp}}
20 |
21 |
22 | {{yesterday | timestamp}} 23 | */ 24 | 25 | angular.module('Mac').filter('timestamp', ['util', function(util) { 26 | function _createTimestamp(count, noun) { 27 | noun = util.pluralize(noun, count); 28 | return count + ' ' + noun + ' ' + 'ago'; 29 | } 30 | 31 | return function(time) { 32 | var parsedTime = parseInt(time), 33 | currentTime = Math.round(Date.now() / 1000), 34 | timeDiff; 35 | 36 | if (isNaN(parsedTime)) return time; 37 | 38 | timeDiff = currentTime - parsedTime; 39 | 40 | if (timeDiff < 45) { 41 | return 'just now'; 42 | } else if (timeDiff < 120) { 43 | return 'about a minute ago'; 44 | } else { 45 | if (timeDiff < 60) return timeDiff + ' seconds ago'; 46 | 47 | timeDiff /= 60; 48 | if (timeDiff < 60) 49 | return _createTimestamp(Math.floor(timeDiff), 'min'); 50 | 51 | timeDiff /= 60; 52 | if (timeDiff < 24) 53 | return _createTimestamp(Math.floor(timeDiff), 'hour'); 54 | 55 | timeDiff /= 24; 56 | if (timeDiff < 7) 57 | return _createTimestamp(Math.floor(timeDiff), 'day'); 58 | 59 | if (timeDiff < 31) 60 | return _createTimestamp(Math.floor(timeDiff/7), 'week'); 61 | 62 | if (timeDiff < 365) 63 | return _createTimestamp(Math.floor(timeDiff/31), 'month'); 64 | 65 | return _createTimestamp(Math.floor(timeDiff/365), 'year'); 66 | } 67 | }; 68 | }]); 69 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | angular.module("Mac", ["Mac.Util"]); 2 | 3 | /** 4 | * @ngdoc function 5 | * @name angular.element 6 | * @module ng 7 | * @kind function 8 | * 9 | * @description 10 | * Angular comes with jqLite, a tiny, API-compatible subset of jQuery. However, its 11 | * functionality is very limited and MacGyver extends jqLite to make sure MacGyver 12 | * components work properly. 13 | * 14 | * Most of the code in this file are based on jQuery and modified a little bit to work 15 | * with MacGyver. 16 | * 17 | * Real jQuery will continue to take precedence over jqLite and all functions MacGyver extends. 18 | * 19 | * MacGyver adds the following methods: 20 | * - [height()](http://api.jquery.com/height/) - Does not support set 21 | * - [width()](http://api.jquery.com/width/) - Does not support set 22 | * - [outerHeight()](http://api.jquery.com/outerHeight/) - Does not support set 23 | * - [outerWidth()](http://api.jquery.com/outerWidth/) - Does not support set 24 | * - [offset()](http://api.jquery.com/offset/) 25 | * - [position()](http://api.jquery.com/position/) 26 | * - [scrollTop()](http://api.jquery.com/scrollTop/) 27 | */ 28 | 29 | var cssExpand = ["Top", "Right", "Bottom", "Left"], 30 | core_pnum = /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source, 31 | rnumnonpx = new RegExp("^(" + core_pnum + ")(?!px)[a-z%]+$", "i"); 32 | 33 | function getStyles(element) { 34 | return window.getComputedStyle(element, null); 35 | } 36 | 37 | function isWindow(obj) { 38 | return obj && obj.document && obj.location && obj.alert && obj.setInterval; 39 | } 40 | 41 | function isScope(obj) { 42 | return obj && (obj.$evalAsync != null) && (obj.$watch != null); 43 | } 44 | 45 | // HACK: Add isScope to AngularJS global scope 46 | angular.isScope = isScope; 47 | 48 | function getWindow(element) { 49 | if (isWindow(element)) { 50 | return element; 51 | } else { 52 | return element.nodeType === 9 && element.defaultView; 53 | } 54 | } 55 | 56 | function augmentWidthOrHeight(element, name, extra, isBorderBox, styles) { 57 | var i, start, val; 58 | if (extra === (isBorderBox ? "border" : "content")) { 59 | return 0; 60 | } 61 | val = 0; 62 | start = name === "Width" ? 1 : 0; 63 | for (i = start; i <= 3; i += 2) { 64 | if (extra === "margin") { 65 | val += parseFloat(styles["" + extra + cssExpand[i]] || 0); 66 | } 67 | if (isBorderBox) { 68 | if (extra === "content") { 69 | val -= parseFloat(styles["padding" + cssExpand[i]] || 0); 70 | } 71 | if (extra !== "margin") { 72 | val -= parseFloat(styles["border" + cssExpand[i]] || 0); 73 | } 74 | } else { 75 | val += parseFloat(styles["padding" + cssExpand[i]] || 0); 76 | if (extra !== "padding") { 77 | val += parseFloat(styles["border" + cssExpand + "Width"] || 0); 78 | } 79 | } 80 | } 81 | return val; 82 | } 83 | 84 | function getWidthOrHeight(type, prefix, element) { 85 | return function(margin) { 86 | var defaultExtra, doc, extra, isBorderBox, name, styles, value, valueIsBorderBox; 87 | 88 | switch (prefix) { 89 | case 'inner': 90 | defaultExtra = 'padding'; 91 | break; 92 | case 'outer': 93 | defaultExtra = ''; 94 | break; 95 | default: 96 | defaultExtra = 'content'; 97 | } 98 | extra = defaultExtra || (margin === true ? "margin" : "border"); 99 | 100 | if (isWindow(element)) { 101 | return element.document.documentElement["client" + type]; 102 | } 103 | 104 | if (element.nodeType === 9) { 105 | doc = element.documentElement; 106 | return Math.max(element.body["scroll" + type], doc["scroll" + type], element.body["offset" + type], doc["offset" + type], doc["client" + type]); 107 | } 108 | 109 | valueIsBorderBox = true; 110 | styles = getStyles(element); 111 | name = type.toLowerCase(); 112 | value = type === "Height" ? element.offsetHeight : element.offsetWidth; 113 | isBorderBox = element.style.boxSizing === "border-box"; 114 | 115 | if (value <= 0 || value === null) { 116 | value = styles[name]; 117 | if (value < 0 || value === null) { 118 | value = element.style[name]; 119 | } 120 | if (rnumnonpx.test(value)) { 121 | return value; 122 | } 123 | valueIsBorderBox = isBorderBox; 124 | value = parseFloat(value) || 0; 125 | } 126 | return value + augmentWidthOrHeight(element, type, extra || (isBorderBox ? "border" : "content"), valueIsBorderBox, styles); 127 | }; 128 | } 129 | 130 | function getOffsetParent(element) { 131 | var parent = element.parentNode; 132 | while (parent && parent.style['position'] === 'static') { 133 | parent = parent.parentNode; 134 | } 135 | 136 | return parent || document.documentElement; 137 | } 138 | 139 | var jqLiteExtend = { 140 | height: function(element) { 141 | return getWidthOrHeight("Height", "", element)(); 142 | }, 143 | width: function(element) { 144 | return getWidthOrHeight("Width", "", element)(); 145 | }, 146 | outerHeight: function(element, margin) { 147 | return getWidthOrHeight("Height", "outer", element)(margin); 148 | }, 149 | outerWidth: function(element, margin) { 150 | return getWidthOrHeight("Width", "outer", element)(margin); 151 | }, 152 | offset: function(element) { 153 | var rect, doc, win, docElem; 154 | 155 | // Support: IE<=11+ 156 | // Running getBoundingClientRect on a 157 | // disconnected node in IE throws an error 158 | if (!element.getClientRects().length) { 159 | return { top: 0, left: 0 }; 160 | } 161 | 162 | rect = element.getBoundingClientRect(); 163 | 164 | if (rect.width || rect.height) { 165 | doc = element.ownerDocument; 166 | win = getWindow(doc); 167 | docElem = doc.documentElement; 168 | 169 | return { 170 | top: rect.top + win.pageYOffset - docElem.clientTop, 171 | left: rect.left + win.pageXOffset - docElem.clientLeft 172 | }; 173 | } 174 | 175 | return rect; 176 | }, 177 | position: function (element) { 178 | var offsetParent, offset, parentOffset = { 179 | top: 0, 180 | left: 0 181 | }; 182 | 183 | if (element.style['position'] === 'fixed') { 184 | offset = element.getBoundingClientRect(); 185 | } else { 186 | offsetParent = getOffsetParent(element); 187 | 188 | offset = jqLiteExtend.offset(element); 189 | if (offsetParent.nodeName !== 'HTML') { 190 | parentOffset = jqLiteExtend.offset(offsetParent); 191 | } 192 | 193 | parentOffset.top += offsetParent['scrollTop']; 194 | parentOffset.left += offsetParent['scrollLeft']; 195 | } 196 | 197 | return { 198 | top: offset.top - parentOffset.top - element.style['marginTop'], 199 | left: offset.left - parentOffset.left - element.style['marginLeft'] 200 | } 201 | }, 202 | scrollTop: function(element, value) { 203 | var win = getWindow(element); 204 | if (value == null) { 205 | if (win) { 206 | return win["pageYOffset"]; 207 | } else { 208 | return element["scrollTop"]; 209 | } 210 | } 211 | if (win) { 212 | return win.scrollTo(window.pageYOffset, value); 213 | } else { 214 | return element["scrollTop"] = value; 215 | } 216 | }, 217 | scrollLeft: function(element, value) { 218 | var win = getWindow(element); 219 | if (value == null) { 220 | if (win) { 221 | return win["pageXOffset"]; 222 | } else { 223 | return element["scrollLeft"]; 224 | } 225 | } 226 | if (win) { 227 | return win.scrollTo(window.pageXOffset, value); 228 | } else { 229 | return element["scrollLeft"] = value; 230 | } 231 | } 232 | }; 233 | 234 | (function () { 235 | var jqLite = angular.element; 236 | if ((window.jQuery != null) && (angular.element.prototype.offset != null)) { 237 | return; 238 | } 239 | return angular.forEach(jqLiteExtend, function(fn, name) { 240 | return jqLite.prototype[name] = function(arg1, arg2) { 241 | if (this.length) { 242 | return fn(this[0], arg1, arg2); 243 | } 244 | }; 245 | }); 246 | })(); 247 | -------------------------------------------------------------------------------- /src/services/scroll_spy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc service 3 | * @name scrollSpy 4 | * @description 5 | * There are multiple components used by scrollspy 6 | * - Scrollspy service is used to keep track of all and active anchors 7 | * - Multiple directives including: 8 | * - mac-scroll-spy - Element to spy scroll event 9 | * - mac-scroll-spy-anchor - Section in element spying on 10 | * - mac-scroll-spy-target - Element to highlight, most likely a nav item 11 | * 12 | * @param {Function} register Register an anchor with the service 13 | * - {String} id ID of the anchor 14 | * - {Element} element Element to spy on 15 | * 16 | * @param {Function} unregister Remove anchor from service 17 | * - {String} id ID of the anchor 18 | * 19 | * @param {Function} setActive Set active anchor and fire all listeners 20 | * - {Object} anchor Anchor object 21 | * 22 | * @param {Function} addListener Add listener when active is set 23 | * - {Function} fn Callback function 24 | * 25 | * @param {Function} removeListener Remove listener 26 | * - {Function} fn Callback function 27 | */ 28 | angular.module('Mac').service('scrollSpy', [ 29 | function() { 30 | return { 31 | registered: [], 32 | active: {}, 33 | listeners: [], 34 | 35 | register: function(id, element) { 36 | var anchor = { 37 | id: id, 38 | element: element, 39 | top: element.offset().top 40 | }; 41 | this.registered.push(anchor); 42 | this.sort(); 43 | return anchor; 44 | }, 45 | 46 | updateOffset: function(anchor) { 47 | anchor.top = anchor.element.offset().top; 48 | this.sort(); 49 | }, 50 | 51 | sort: function() { 52 | this.registered.sort(function(a, b) { 53 | if (a.top > b.top) { 54 | return 1; 55 | } else if (a.top < b.top) { 56 | return -1; 57 | } 58 | return 0; 59 | }); 60 | }, 61 | 62 | unregister: function(id) { 63 | var index = -1, i; 64 | for (i = 0; i < this.registered.length; i++) { 65 | if (this.registered[i].id === id) { 66 | index = i; 67 | break; 68 | } 69 | } 70 | 71 | if (index !== -1) { 72 | this.registered.splice(index, 1); 73 | } 74 | }, 75 | 76 | last: function() { 77 | return this.registered[this.registered.length - 1]; 78 | }, 79 | 80 | setActive: function(anchor) { 81 | var i; 82 | if (this.active.id === anchor.id) { 83 | return; 84 | } 85 | this.active = anchor; 86 | for (i = 0; i < this.listeners.length; i++) { 87 | this.listeners[i](anchor); 88 | } 89 | }, 90 | 91 | addListener: function(fn) { 92 | return this.listeners.push(fn); 93 | }, 94 | 95 | removeListener: function(fn) { 96 | var index = this.listeners.indexOf(fn); 97 | if (index !== -1) { 98 | this.listeners.splice(index, 1); 99 | } 100 | } 101 | }; 102 | } 103 | ]); 104 | -------------------------------------------------------------------------------- /src/services/time_util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc service 3 | * @name timeUtil 4 | * @description 5 | * All utility functions for MacTime 6 | */ 7 | angular.module('Mac').factory('macTimeUtil', [ 8 | '$filter', 9 | '$timeout', 10 | 'macTimeDefaults', 11 | 'util', 12 | function($filter, $timeout, defaults, util) { 13 | /** 14 | * @ngdoc method 15 | * @name timeUtil#initializeTime 16 | * @description 17 | * Generate Date object based on options 18 | * @param {Object} options 19 | * @returns {Date} 20 | */ 21 | function initializeTime (options) { 22 | var currentDate = new Date().toDateString(), time; 23 | 24 | time = new Date(currentDate + ' ' + options.default); 25 | 26 | if (isNaN(time.getTime())) { 27 | time = new Date(currentDate + ' ' + defaults.default); 28 | } 29 | 30 | return time; 31 | } 32 | 33 | /** 34 | * @ngdoc method 35 | * @name timeUtil#getSelection 36 | * @description 37 | * Get element cursor section 38 | * @param {Element} element 39 | * @returns {String} 40 | */ 41 | function getSelection (element) { 42 | var start = element[0].selectionStart; 43 | 44 | if (0 <= start && start < 3){ 45 | return 'hour'; 46 | } else if (3 <= start && start < 6) { 47 | return 'minute'; 48 | } else if (6 <= start && start < 9) { 49 | return 'meridian'; 50 | } 51 | } 52 | 53 | /** 54 | * A wrapper for setSelectionRange with a timeout 0 55 | * @param {Element} element 56 | * @param {Number} start 57 | * @param {Number} end 58 | */ 59 | function selectRange (element, start, end) { 60 | $timeout(function () { 61 | element[0].setSelectionRange(start, end); 62 | }, 0, false); 63 | } 64 | 65 | /** 66 | * Select hour block 67 | * @param {Element} element 68 | */ 69 | function selectHours (element) { 70 | selectRange(element, 0, 2); 71 | } 72 | 73 | /** 74 | * Select minute block 75 | * @param {Element} element 76 | */ 77 | function selectMinutes (element) { 78 | selectRange(element, 3, 5); 79 | } 80 | 81 | /** 82 | * Select meridian block (AM/PM) 83 | * @param {Element} element 84 | */ 85 | function selectMeridian (element) { 86 | selectRange(element, 6, 8); 87 | } 88 | 89 | /** 90 | * Select/highlight next block 91 | * hour -> minute 92 | * minute -> meridian 93 | * meridian -> meridian (no next block) 94 | * @param {Element} element 95 | */ 96 | function selectNextSection (element) { 97 | switch (getSelection(element)) { 98 | case 'hour': 99 | selectMinutes(element); 100 | break; 101 | case 'minute': 102 | case 'meridian': 103 | selectMeridian(element); 104 | break; 105 | } 106 | } 107 | 108 | /** 109 | * Select/highlight previous block 110 | * hour -> hour (no previous block) 111 | * minute -> hour 112 | * meridian -> minute 113 | * @param {Element} element 114 | */ 115 | function selectPreviousSection (element) { 116 | switch (getSelection(element)) { 117 | case 'hour': 118 | case 'minute': 119 | selectHours(element); 120 | break; 121 | case 'meridian': 122 | selectMinutes(element); 123 | break; 124 | } 125 | } 126 | 127 | /** 128 | * Toggle time hour based on meridian value 129 | * @param {Date} time 130 | * @param {String} meridian 131 | */ 132 | function setMeridian (time, meridian) { 133 | var hours = time.getHours(); 134 | 135 | if (hours >= 12 && meridian === 'AM') { 136 | hours -= 12; 137 | } else if (hours < 12 && meridian === 'PM') { 138 | hours += 12; 139 | } 140 | 141 | time.setHours(hours); 142 | } 143 | 144 | /** 145 | * Toggle time hour 146 | * @param {Date} time 147 | */ 148 | function toggleMeridian (time) { 149 | var hours = time.getHours(); 150 | time.setHours((hours + 12) % 24); 151 | } 152 | 153 | /** 154 | * Change hour, wrapper for setHours 155 | * @param {Date} time 156 | * @param {Number} change 157 | */ 158 | function incrementHour (time, change) { 159 | time.setHours(time.getHours() + change); 160 | } 161 | 162 | /** 163 | * Change minute, wrapper for setMinutes 164 | * @param {Date} time 165 | * @param {Number} change 166 | */ 167 | function incrementMinute (time, change) { 168 | time.setMinutes(time.getMinutes() + change); 169 | } 170 | 171 | /** 172 | * Update input view value with ngModelController 173 | * @param {Date} time 174 | * @param {ngController} controller 175 | */ 176 | function updateInput (time, controller) { 177 | var displayTime = $filter('date')(time.getTime(), 'hh:mm a'); 178 | 179 | if (displayTime !== controller.$viewValue) { 180 | controller.$setViewValue(displayTime); 181 | controller.$render(); 182 | } 183 | } 184 | 185 | /** 186 | * Update time with ngModelController model value 187 | * @param {Date} time 188 | * @param {ngController} controller 189 | */ 190 | function updateTime (time, controller) { 191 | var timeMatch = util.validateTime(controller.$modelValue), 192 | hours, minutes, meridian; 193 | 194 | if (timeMatch) { 195 | hours = +timeMatch[1]; 196 | minutes = +timeMatch[2]; 197 | meridian = timeMatch[3]; 198 | 199 | if (meridian == 'PM' && hours != 12) hours += 12; 200 | if (meridian == 'AM' && hours == 12) hours = 0; 201 | 202 | time.setHours(hours, minutes); 203 | } 204 | } 205 | 206 | return { 207 | getSelection: getSelection, 208 | incrementHour: incrementHour, 209 | incrementMinute: incrementMinute, 210 | initializeTime: initializeTime, 211 | selectHours: selectHours, 212 | selectMeridian: selectMeridian, 213 | selectMinutes: selectMinutes, 214 | selectNextSection: selectNextSection, 215 | selectPreviousSection: selectPreviousSection, 216 | selectRange: selectRange, 217 | setMeridian: setMeridian, 218 | toggleMeridian: toggleMeridian, 219 | updateInput: updateInput, 220 | updateTime: updateTime 221 | }; 222 | } 223 | ]); 224 | -------------------------------------------------------------------------------- /src/template/menu.html: -------------------------------------------------------------------------------- 1 |
2 |
    3 |
  • 9 |
  • 10 |
11 |
12 | -------------------------------------------------------------------------------- /src/template/tag_autocomplete.html: -------------------------------------------------------------------------------- 1 |
2 |
    3 |
  • 4 |
    ×
    5 |
    {{macTagAutocomplete.getTagLabel(tag)}}
    6 |
  • 7 |
  • 8 | 16 |
  • 17 |
18 |
19 | -------------------------------------------------------------------------------- /test/e2e/modal.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Test 16 | 17 | 18 | 19 | 20 | 21 | Test 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | Test 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /test/e2e/modal.spec.js: -------------------------------------------------------------------------------- 1 | describe("Mac Modal e2e test", function() { 2 | beforeEach(function() { 3 | browser.get("/test/e2e/modal.html"); 4 | }); 5 | 6 | it("should show the registered modal", function() { 7 | var button, modal; 8 | 9 | button = element(By.id("open-btn")); 10 | button.click(); 11 | modal = element(By.id("test-modal")); 12 | 13 | expect(modal.isDisplayed()).toBeTruthy(); 14 | }); 15 | 16 | it("should hide the modal", function() { 17 | var button, closeBtn, modal; 18 | button = element(By.id("open-btn")); 19 | button.click(); 20 | browser.sleep(500); 21 | modal = element(By.id("test-modal")); 22 | 23 | closeBtn = element(By.css("#test-modal .mac-close-modal")); 24 | closeBtn.click(); 25 | 26 | expect(modal.isDisplayed()).toBeFalsy(); 27 | }); 28 | 29 | it("should hide the modal with 'escape' key", function() { 30 | var button, modal; 31 | button = element(By.id("open-keyboard-btn")); 32 | button.click(); 33 | modal = element(By.id("keyboard-modal")); 34 | browser.actions().sendKeys(protractor.Key.ESCAPE).perform(); 35 | 36 | expect(modal.isDisplayed()).toBeFalsy(); 37 | }); 38 | 39 | it("should close the modal after clicking on overlay", function() { 40 | var button, modal; 41 | button = element(By.id("open-overlay-btn")); 42 | button.click(); 43 | modal = browser.findElement(By.id("overlay-modal")); 44 | 45 | browser.driver.executeScript("arguments[0].click()", modal).then(function() { 46 | expect(modal.isDisplayed()).toBeFalsy(); 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /test/e2e/time.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 19 | 20 | {{testStartTime}} 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /test/e2e/time.spec.js: -------------------------------------------------------------------------------- 1 | describe("Mac Time Input", function() { 2 | var input, output; 3 | beforeEach(function() { 4 | browser.get("/test/e2e/time.html"); 5 | 6 | input = element(by.css(".mac-date-time")); 7 | output = element(by.css(".output")); 8 | }); 9 | 10 | afterEach(function() { 11 | input = null; 12 | output = null; 13 | }); 14 | 15 | it("should return true", function() { 16 | input.click(); 17 | expect(output.getText()).toEqual("10:55 PM"); 18 | }); 19 | 20 | it("should change the meridian to AM after pressing the A button", function() { 21 | input.click(); 22 | browser.executeScript("return arguments[0].setSelectionRange(6, 8);", input.getWebElement()); 23 | 24 | input.sendKeys("A"); 25 | 26 | expect(output.getText()).toEqual("10:55 AM"); 27 | }); 28 | 29 | it("should not change the meridian to AM after pressing the P button", function() { 30 | input.click(); 31 | browser.executeScript("return arguments[0].setSelectionRange(6, 8);", input.getWebElement()); 32 | 33 | input.sendKeys("P"); 34 | expect(output.getText()).toEqual("10:55 PM"); 35 | }); 36 | 37 | it("should change the model after pressing down button", function() { 38 | input.click(); 39 | browser.executeScript("return arguments[0].setSelectionRange(6, 8);", input.getWebElement()); 40 | 41 | input.sendKeys(protractor.Key.DOWN); 42 | expect(output.getText()).toEqual("10:55 AM"); 43 | }); 44 | 45 | it("should change the model after pressing up button", function() { 46 | input.click(); 47 | browser.executeScript("return arguments[0].setSelectionRange(6, 8);", input.getWebElement()); 48 | 49 | input.sendKeys(protractor.Key.UP); 50 | expect(output.getText()).toEqual("10:55 AM"); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /test/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | sauceLabs: { 4 | startConnect: true, 5 | testName: "MacGyver" 6 | }, 7 | customLaunchers: { 8 | SL_Chrome: { 9 | base: "SauceLabs", 10 | browserName: "chrome" 11 | }, 12 | SL_Firefox: { 13 | base: 'SauceLabs', 14 | browserName: 'firefox' 15 | }, 16 | SL_Safari: { 17 | base: 'SauceLabs', 18 | browserName: 'safari', 19 | platform: 'OS X 10.11', 20 | version: '9' 21 | } 22 | }, 23 | 24 | // base path, that will be used to resolve files and exclude 25 | basePath: "../", 26 | frameworks: ["jasmine"], 27 | 28 | // list of files / patterns to load in the browser 29 | files: [ 30 | // 3rd party libraries 31 | "node_modules/angular/angular.js", 32 | 33 | // Template 34 | "src/template/*.html", 35 | 36 | // Test Code 37 | "src/main.js", 38 | "src/**/*.js", 39 | "node_modules/angular-mocks/angular-mocks.js", 40 | "test/vendor/browserTrigger.js", 41 | "test/unit/**/*.spec.js" 42 | ], 43 | reporters: ["dots"], 44 | logLevel: config.LOG_INFO, 45 | browsers: ["Electron"], 46 | electronOpts: { 47 | show: false 48 | }, 49 | preprocessors: { 50 | "**/*.html": ["ng-html2js"] 51 | }, 52 | plugins: ["karma-*"] 53 | }); 54 | 55 | if (process.env.TRAVIS) { 56 | var buildLabel = "TRAVIS #" + process.env.TRAVIS_BUILD_NUMBER + " (" + process.env.TRAVIS_BUILD_ID + ")"; 57 | config.sauceLabs.build = buildLabel; 58 | config.sauceLabs.startConnect = false; 59 | config.sauceLabs.tunnelIdentifier = process.env.TRAVIS_JOB_NUMBER; 60 | config.sauceLabs.recordScreenshots = true; 61 | 62 | config.logLevel = config.LOG_DEBUG; 63 | 64 | // Debug logging into a file, that we print out at the end of the build. 65 | config.loggers.push({ 66 | type: 'file', 67 | filename: process.env.LOGS_DIR + '/' + 'karma.log' 68 | }); 69 | } 70 | 71 | // Terrible hack to workaround inflexibility of log4js: 72 | // - ignore DEBUG logs (on Travis), we log them into a file instead. 73 | var log4js = require('../node_modules/log4js'); 74 | var layouts = require('../node_modules/log4js/lib/layouts'); 75 | 76 | var originalConfigure = log4js.configure; 77 | log4js.configure = function(log4jsConfig) { 78 | var consoleAppender = log4jsConfig.appenders.shift(); 79 | var originalResult = originalConfigure.call(log4js, log4jsConfig); 80 | var layout = layouts.layout(consoleAppender.layout.type, consoleAppender.layout); 81 | 82 | log4js.addAppender(function(log) { 83 | var msg = log.data[0]; 84 | 85 | // on Travis, ignore DEBUG statements 86 | if (process.env.TRAVIS && log.level.levelStr === config.LOG_DEBUG) { 87 | return; 88 | } 89 | 90 | console.log(layout(log)); 91 | }); 92 | 93 | return originalResult; 94 | }; 95 | }; 96 | -------------------------------------------------------------------------------- /test/protractor.conf.js: -------------------------------------------------------------------------------- 1 | var config = { 2 | specs: ["e2e/*.spec.js"], 3 | baseUrl: "http://localhost:9001", 4 | framework: "jasmine", 5 | 6 | capabilities: { 7 | "name": "MacGyver E2E", 8 | "browserName": process.env.BROWSER || 'chrome' 9 | }, 10 | 11 | onPrepare: function() { 12 | var disableNgAnimate; 13 | disableNgAnimate = function() { 14 | return angular.module('disableNgAnimate', []).run(function($animate) { 15 | return $animate.enabled(false); 16 | }); 17 | }; 18 | return browser.addMockModule('disableNgAnimate', disableNgAnimate); 19 | } 20 | }; 21 | 22 | if (process.env.VERSION) { 23 | config.capabilities.version = process.env.VERSION 24 | } 25 | 26 | if (process.env.TRAVIS) { 27 | config.sauceUser = process.env.SAUCE_USERNAME; 28 | config.sauceKey = process.env.SAUCE_ACCESS_KEY; 29 | 30 | config.capabilities['tunnel-identifier'] = process.env.TRAVIS_JOB_NUMBER 31 | config.capabilities.build = 'TRAVIS #' + process.env.TRAVIS_BUILD_NUMBER + " (" + process.env.TRAVIS_BUILD_ID + ")"; 32 | 33 | // Local e2e tests 34 | } else { 35 | config.seleniumAddress = 'http://localhost:4444/wd/hub'; 36 | } 37 | 38 | exports.config = config; 39 | -------------------------------------------------------------------------------- /test/unit/affix.spec.js: -------------------------------------------------------------------------------- 1 | describe("Mac Affix", function() { 2 | var $compile, $rootScope, element, ctrl, ctrlElement, windowEl; 3 | 4 | beforeEach(module("Mac")); 5 | beforeEach(inject(function(_$rootScope_, _$compile_, $controller, $window) { 6 | $rootScope = _$rootScope_; 7 | $compile = _$compile_; 8 | 9 | ctrlElement = angular.element('
'); 10 | 11 | ctrl = $controller('MacAffixController', { 12 | $element: ctrlElement 13 | }); 14 | 15 | windowEl = angular.element($window); 16 | })); 17 | 18 | afterEach(function() { 19 | if (element) { 20 | element.scope().$destroy(); 21 | element.remove(); 22 | } 23 | element = null; 24 | }); 25 | 26 | describe('MacAffixController', function () { 27 | it('should initialize affix controller', function () { 28 | expect(ctrl.$document).toBeDefined(); 29 | expect(ctrl.defaults).toBeDefined(); 30 | 31 | expect(ctrl.$element).toBe(ctrlElement); 32 | 33 | expect(ctrl.offset.top).toBe(0); 34 | expect(ctrl.offset.bottom).toBe(0); 35 | 36 | expect(ctrl.windowEl).toBeDefined(); 37 | expect(ctrl.disabled).toBe(false); 38 | 39 | expect(ctrl.lastAffix).toBe(null); 40 | expect(ctrl.unpin).toBe(null); 41 | expect(ctrl.pinnedOffset).toBe(null); 42 | }); 43 | 44 | describe('updateOffset', function () { 45 | it('should not update offset', function () { 46 | ctrl.updateOffset('doesNotExist', 'hi'); 47 | 48 | expect(ctrl.offset.doesNotExist).toBeUndefined(); 49 | }); 50 | 51 | it('should use default value', function () { 52 | ctrl.defaults.top = 321; 53 | ctrl.updateOffset('top', null, true); 54 | 55 | expect(ctrl.offset.top).toBe(321); 56 | }); 57 | 58 | it('should not update key', function () { 59 | ctrl.defaults.top = 321; 60 | ctrl.offset.top = 123; 61 | ctrl.updateOffset('top'); 62 | 63 | expect(ctrl.offset.top).toBe(123); 64 | }); 65 | 66 | it('should update based on passed in value', function () { 67 | ctrl.updateOffset('top', 245); 68 | expect(ctrl.offset.top).toBe(245); 69 | }); 70 | 71 | it('should not update when the value is invalid', function () { 72 | ctrl.offset.top = 0; 73 | ctrl.updateOffset('top', 'nop'); 74 | 75 | expect(ctrl.offset.top).toBe(0); 76 | }); 77 | }); 78 | }); 79 | 80 | it("should remove affix class when disabled", function() { 81 | $rootScope.affixDisabled = false; 82 | 83 | element = angular.element("
"); 84 | angular.element(document.body).append(element); 85 | $compile(element)($rootScope); 86 | 87 | $rootScope.$digest(); 88 | angular.element(window).triggerHandler("scroll"); 89 | 90 | $rootScope.affixDisabled = true; 91 | $rootScope.$digest(); 92 | 93 | expect(element.hasClass("affix-top")).toBe(false); 94 | }); 95 | 96 | it("should re-enable mac-affix", function() { 97 | $rootScope.affixDisabled = true; 98 | 99 | element = angular.element("
"); 100 | angular.element(document.body).append(element); 101 | $compile(element)($rootScope); 102 | $rootScope.$digest(); 103 | 104 | expect(element.hasClass("affix-top")).toBe(false); 105 | 106 | $rootScope.affixDisabled = false; 107 | $rootScope.$digest(); 108 | 109 | expect(element.hasClass("affix-top")).toBe(true); 110 | }); 111 | 112 | it('should update top offset', function () { 113 | element = angular.element("
"); 114 | angular.element(document.body).append(element); 115 | 116 | $rootScope.topOffset = 100; 117 | 118 | $compile(element)($rootScope); 119 | $rootScope.$digest(); 120 | 121 | var testCtrl = element.controller('macAffix') 122 | 123 | expect(testCtrl.offset.top).toBe(100); 124 | 125 | $rootScope.topOffset = 200; 126 | $rootScope.$digest(); 127 | 128 | expect(testCtrl.offset.top).toBe(200); 129 | }); 130 | 131 | it('should update bottom offset', function () { 132 | element = angular.element("
"); 133 | angular.element(document.body).append(element); 134 | 135 | $rootScope.bottomOffset = 100; 136 | 137 | $compile(element)($rootScope); 138 | $rootScope.$digest(); 139 | 140 | var testCtrl = element.controller('macAffix') 141 | 142 | expect(testCtrl.offset.bottom).toBe(100); 143 | 144 | $rootScope.bottomOffset = 200; 145 | $rootScope.$digest(); 146 | 147 | expect(testCtrl.offset.bottom).toBe(200); 148 | }); 149 | 150 | it('should reposition when calling refresh-mac-affix', function () { 151 | element = angular.element("
"); 152 | angular.element(document.body).append(element); 153 | $compile(element)($rootScope); 154 | $rootScope.$digest(); 155 | 156 | var testCtrl = element.controller('macAffix') 157 | 158 | spyOn(testCtrl, 'scrollEvent'); 159 | 160 | $rootScope.$broadcast('refresh-mac-affix'); 161 | $rootScope.$digest(); 162 | 163 | expect(testCtrl.scrollEvent).toHaveBeenCalled(); 164 | }); 165 | 166 | it('should unbind scrollEvent', function () { 167 | element = angular.element("
"); 168 | angular.element(document.body).append(element); 169 | $compile(element)($rootScope); 170 | $rootScope.$digest(); 171 | 172 | var testCtrl = element.controller('macAffix'); 173 | spyOn(testCtrl, 'scrollEvent'); 174 | 175 | $rootScope.$destroy(); 176 | $rootScope.$digest(); 177 | 178 | windowEl.triggerHandler('scroll'); 179 | $rootScope.$digest(); 180 | 181 | expect(testCtrl.scrollEvent).not.toHaveBeenCalled(); 182 | }); 183 | }); 184 | -------------------------------------------------------------------------------- /test/unit/events.keydown.spec.js: -------------------------------------------------------------------------------- 1 | describe('Keydown event', function() { 2 | var $rootScope, $compile, keys; 3 | 4 | beforeEach(module('Mac')); 5 | beforeEach(inject(function(_$compile_, _$rootScope_, _keys_) { 6 | $compile = _$compile_; 7 | $rootScope = _$rootScope_; 8 | keys = _keys_; 9 | })); 10 | 11 | var keydownEvents = ['enter', 'escape', 'space', 'left', 'up', 'right', 'down']; 12 | 13 | keydownEvents.forEach(function(key) { 14 | it('should trigger callback for ' + key + ' key', function() { 15 | $rootScope.callback = jasmine.createSpy(key); 16 | 17 | var input = $compile('')($rootScope); 18 | $rootScope.$digest(); 19 | 20 | input.triggerHandler({ 21 | type: 'keydown', 22 | which: keys[key.toUpperCase()] 23 | }); 24 | 25 | expect($rootScope.callback).toHaveBeenCalled(); 26 | }); 27 | }); 28 | }); -------------------------------------------------------------------------------- /test/unit/events.pause_typing.spec.js: -------------------------------------------------------------------------------- 1 | describe('Pause typing', function() { 2 | var $compile, $rootScope, $timeout, keys; 3 | 4 | beforeEach(module('Mac')); 5 | beforeEach(inject(function(_$compile_, _$timeout_, _$rootScope_, _keys_) { 6 | $compile = _$compile_; 7 | $timeout = _$timeout_; 8 | $rootScope = _$rootScope_; 9 | keys = _keys_; 10 | })); 11 | 12 | it('should invoke callback', function() { 13 | var input; 14 | $rootScope.callback = jasmine.createSpy('callback'); 15 | input = $compile('')($rootScope); 16 | 17 | $rootScope.$digest(); 18 | 19 | input.triggerHandler({ 20 | type: 'keyup', 21 | which: keys.F 22 | }); 23 | input.triggerHandler({ 24 | type: 'keyup', 25 | which: keys.O 26 | }); 27 | input.triggerHandler({ 28 | type: 'keyup', 29 | which: keys.O 30 | }); 31 | $timeout.flush(); 32 | expect($rootScope.callback).toHaveBeenCalled(); 33 | }); 34 | }); -------------------------------------------------------------------------------- /test/unit/events.window_resize.spec.js: -------------------------------------------------------------------------------- 1 | describe('Window resize', function() { 2 | var $compile, $rootScope, $window; 3 | 4 | beforeEach(module('Mac')); 5 | beforeEach(inject(function(_$compile_, _$rootScope_, _$window_) { 6 | $compile = _$compile_; 7 | $rootScope = _$rootScope_; 8 | $window = _$window_; 9 | })); 10 | 11 | it('should invoke callback', function() { 12 | $rootScope.callback = jasmine.createSpy('callback'); 13 | $compile('
')($rootScope); 14 | 15 | $rootScope.$digest(); 16 | 17 | angular.element($window).triggerHandler({ 18 | type: 'resize' 19 | }); 20 | 21 | $rootScope.$digest(); 22 | 23 | expect($rootScope.callback).toHaveBeenCalled(); 24 | }); 25 | 26 | it('should not invoke callback when scope is destroyed', function() { 27 | $rootScope.callback = jasmine.createSpy('callback'); 28 | $compile('
')($rootScope); 29 | 30 | $rootScope.$digest(); 31 | 32 | $rootScope.$destroy(); 33 | 34 | $rootScope.$digest(); 35 | 36 | angular.element($window).triggerHandler({ 37 | type: 'resize' 38 | }); 39 | 40 | $rootScope.$digest(); 41 | 42 | expect($rootScope.callback).not.toHaveBeenCalled(); 43 | }); 44 | 45 | it('should invoke both callbacks', function() { 46 | var anotherScope = $rootScope.$new(true); 47 | 48 | $rootScope.callback = jasmine.createSpy('callback 1'); 49 | anotherScope.callback = jasmine.createSpy('callback 2'); 50 | 51 | $compile('
')($rootScope); 52 | $compile('
')(anotherScope); 53 | 54 | $rootScope.$digest(); 55 | 56 | angular.element($window).triggerHandler({ 57 | type: 'resize' 58 | }); 59 | 60 | $rootScope.$digest(); 61 | 62 | expect($rootScope.callback).toHaveBeenCalled(); 63 | expect(anotherScope.callback).toHaveBeenCalled(); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /test/unit/filters.boolean.spec.js: -------------------------------------------------------------------------------- 1 | describe('Filter: boolean', function() { 2 | var booleanFilter; 3 | 4 | beforeEach(module('Mac')); 5 | beforeEach(inject(function($filter){ 6 | booleanFilter = $filter('boolean'); 7 | })); 8 | 9 | it('should return true string', function() { 10 | expect(booleanFilter(true, 'show this', 'not this'), 'show this'); 11 | }); 12 | 13 | it('should return false string', function() { 14 | expect(booleanFilter(false, 'nop', 'but this'), 'but this'); 15 | }); 16 | 17 | it('should return default true string', function() { 18 | expect(booleanFilter(true), 'true'); 19 | }); 20 | 21 | it('should return default false string', function() { 22 | expect(booleanFilter(false), 'false'); 23 | }); 24 | }); 25 | 26 | describe('Filter: true', function() { 27 | var trueFilter; 28 | 29 | beforeEach(module('Mac')); 30 | beforeEach(inject(function($filter) { 31 | trueFilter = $filter('true'); 32 | })); 33 | 34 | it('should return true string', function() { 35 | expect(trueFilter(true, 'show this'), 'show this'); 36 | }); 37 | 38 | it('should return empty string', function() { 39 | expect(trueFilter(false, 'nop'), ''); 40 | }); 41 | 42 | it('should return default true text', function() { 43 | expect(trueFilter(true), 'true'); 44 | }); 45 | }); 46 | 47 | describe('Filter: false', function() { 48 | var falseFilter; 49 | 50 | beforeEach(module('Mac')); 51 | beforeEach(inject(function($filter) { 52 | falseFilter = $filter('false'); 53 | })); 54 | 55 | it('should return false string', function() { 56 | expect(falseFilter(false, 'show this'), 'show this'); 57 | }); 58 | 59 | it('should return empty string', function() { 60 | expect(falseFilter(true, 'nop'), ''); 61 | }); 62 | 63 | it('should return default true text', function() { 64 | expect(falseFilter(false), 'false'); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /test/unit/filters.list.spec.js: -------------------------------------------------------------------------------- 1 | describe('Filter: list', function() { 2 | var listFilter; 3 | 4 | beforeEach(module('Mac')); 5 | beforeEach(inject(function($filter) { 6 | listFilter = $filter('list'); 7 | })); 8 | 9 | it('should format an array into a string', function() { 10 | expect(listFilter([1, 2, 3, 4])).toBe('1, 2, 3, 4'); 11 | }); 12 | 13 | it('should format an array into a string with a custom separator', function() { 14 | expect(listFilter([1, 2, 3], '|')).toBe('1|2|3'); 15 | }); 16 | 17 | it('should return the input when it is not array', function() { 18 | expect(listFilter('1,2,3')).toBe('1,2,3'); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /test/unit/filters.pluralize.spec.js: -------------------------------------------------------------------------------- 1 | describe('Filter: pluralize', function() { 2 | var pluralizeFilter, util; 3 | 4 | beforeEach(module('Mac')); 5 | beforeEach(inject(function($filter, _util_) { 6 | pluralizeFilter = $filter('pluralize'); 7 | util = _util_; 8 | 9 | spyOn(util, 'pluralize').and.callThrough(); 10 | })); 11 | 12 | it('should call util.pluralize', function() { 13 | pluralizeFilter('test'); 14 | expect(util.pluralize).toHaveBeenCalled(); 15 | }); 16 | 17 | it('should default includeCount to true', function() { 18 | pluralizeFilter('test', 2); 19 | expect(util.pluralize).toHaveBeenCalledWith('test', 2, true); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/unit/filters.timestamp.spec.js: -------------------------------------------------------------------------------- 1 | describe("Timestamp filter", function() { 2 | var timestampFilter; 3 | 4 | beforeEach(module("Mac")); 5 | beforeEach(inject(function($injector) { 6 | timestampFilter = $injector.get("timestampFilter"); 7 | })); 8 | 9 | it("should format timestamps", function() { 10 | var aMinuteAgo, fiveMinutesAgo, fourDaysAgo, justNow, now, sixMonthsAgo, tenYearsAgo, threeHoursAgo, twoWeeksAgo; 11 | now = Math.floor(Date.now() / 1000.0); 12 | justNow = now - 5; 13 | aMinuteAgo = now - 60; 14 | fiveMinutesAgo = now - 60 * 5; 15 | threeHoursAgo = now - 60 * 60 * 3; 16 | fourDaysAgo = now - 24 * 60 * 60 * 4; 17 | twoWeeksAgo = now - 7 * 24 * 60 * 60 * 2; 18 | sixMonthsAgo = now - 31 * 24 * 60 * 60 * 6; 19 | tenYearsAgo = now - 365 * 24 * 60 * 60 * 10; 20 | 21 | expect(timestampFilter(justNow)).toBe("just now"); 22 | expect(timestampFilter(aMinuteAgo)).toBe("about a minute ago"); 23 | expect(timestampFilter(fiveMinutesAgo)).toBe("5 min ago"); 24 | expect(timestampFilter(threeHoursAgo)).toBe("3 hours ago"); 25 | expect(timestampFilter(fourDaysAgo)).toBe("4 days ago"); 26 | expect(timestampFilter(twoWeeksAgo)).toBe("2 weeks ago"); 27 | expect(timestampFilter(sixMonthsAgo)).toBe("6 months ago"); 28 | expect(timestampFilter(tenYearsAgo)).toBe("10 years ago"); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/unit/main.spec.js: -------------------------------------------------------------------------------- 1 | describe("Mac main", function() { 2 | var $rootScope; 3 | beforeEach(module("Mac")); 4 | 5 | beforeEach(inject(function(_$rootScope_) { 6 | $rootScope = _$rootScope_; 7 | })); 8 | 9 | describe("isScope", function() { 10 | it("should be an Angular public method", function() { 11 | expect(angular.isScope).toBeDefined(); 12 | }); 13 | 14 | it("should return false on normal object", function() { 15 | expect(angular.isScope({})).toBe(false); 16 | }); 17 | 18 | it("should return true for Angular scope", function() { 19 | expect(angular.isScope($rootScope)).toBe(true); 20 | }); 21 | }); 22 | 23 | describe('position', function () { 24 | it('should be on angular element', function() { 25 | expect(angular.element.prototype.position).toBeDefined(); 26 | }); 27 | 28 | it('should get as offset relative to body', function () { 29 | var element = angular.element('
'); 30 | angular.element(document.body).append(element); 31 | 32 | var position = element.position(); 33 | expect(position.top).toBe(100); 34 | expect(position.left).toBe(0); 35 | 36 | element.remove(); 37 | }); 38 | 39 | it('should get position relative to parent element', function () { 40 | var element = angular.element('
'); 41 | var parent = angular.element('
'); 42 | 43 | parent.append(element); 44 | angular.element(document.body).append(parent); 45 | 46 | var position = element.position(); 47 | expect(position.top).toBe(100); 48 | expect(position.left).toBe(0); 49 | 50 | parent.remove(); 51 | }); 52 | }); 53 | 54 | describe('offset', function () { 55 | beforeEach(function () { 56 | angular.element(document.body).css({ 57 | margin: 0, 58 | padding: 0 59 | }); 60 | }); 61 | 62 | it('should be on angular element', function() { 63 | expect(angular.element.prototype.offset).toBeDefined(); 64 | }); 65 | 66 | it('should get offset', function () { 67 | var element = angular.element('
'); 68 | angular.element(document.body).append(element); 69 | 70 | var position = element.offset(); 71 | expect(position.top).toBe(100); 72 | expect(position.left).toBe(0); 73 | 74 | element.remove(); 75 | }); 76 | 77 | it('should get offset correctly with parent element', function () { 78 | var element = angular.element('
'); 79 | var parent = angular.element('
'); 80 | 81 | parent.append(element); 82 | angular.element(document.body).append(parent); 83 | 84 | var position = element.offset(); 85 | expect(position.top).toBe(200); 86 | expect(position.left).toBe(0); 87 | 88 | parent.remove(); 89 | }); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /test/unit/menu.spec.js: -------------------------------------------------------------------------------- 1 | describe("Mac menu", function() { 2 | var $compile, $rootScope, getMenuText, items, queryMenu; 3 | items = [ 4 | { 5 | label: "item1" 6 | }, { 7 | label: "item2" 8 | }, { 9 | label: "item3" 10 | } 11 | ]; 12 | 13 | queryMenu = function(element) { 14 | return element.querySelector(".mac-menu-item"); 15 | }; 16 | 17 | getMenuText = function(element) { 18 | var item; 19 | item = queryMenu(element); 20 | return item.innerText || item.textContent; 21 | }; 22 | 23 | beforeEach(module("Mac")); 24 | beforeEach(module("template/menu.html")); 25 | beforeEach(inject(function(_$compile_, _$rootScope_) { 26 | $compile = _$compile_; 27 | $rootScope = _$rootScope_; 28 | })); 29 | 30 | describe("templating", function() { 31 | it("should use default template", function() { 32 | var element; 33 | $rootScope.items = items; 34 | element = $compile("")($rootScope); 35 | $rootScope.$digest(); 36 | 37 | expect(getMenuText(element[0])).toBe("item1"); 38 | }); 39 | 40 | it("should use custom template", function() { 41 | var element; 42 | $rootScope.items = items; 43 | element = $compile("{{item.label}} {{$index}}")($rootScope); 44 | $rootScope.$digest(); 45 | 46 | return expect(getMenuText(element[0])).toBe("item1 0"); 47 | }); 48 | }); 49 | 50 | it("should show three items", function() { 51 | var element; 52 | $rootScope.items = items; 53 | element = $compile("")($rootScope); 54 | $rootScope.$digest(); 55 | 56 | expect(element[0].querySelectorAll(".mac-menu-item").length).toBe(3); 57 | }); 58 | 59 | it("should update scope index", function() { 60 | $rootScope.items = items; 61 | $rootScope.index = 0; 62 | 63 | $compile("")($rootScope); 64 | $rootScope.$digest(); 65 | 66 | $rootScope.$$childHead.setIndex(2); 67 | $rootScope.$digest(); 68 | 69 | expect($rootScope.index).toBe(2); 70 | }); 71 | 72 | it("should add custom style", function() { 73 | var element; 74 | $rootScope.items = items; 75 | $rootScope.style = { 76 | color: 'red' 77 | }; 78 | 79 | element = $compile("")($rootScope); 80 | $rootScope.$digest(); 81 | 82 | expect(element[0].style.color).toBe("red"); 83 | }); 84 | 85 | it("should fire select callback", function() { 86 | var callback = jasmine.createSpy("select"); 87 | $rootScope.items = items; 88 | $rootScope.select = callback; 89 | 90 | $compile("")($rootScope); 91 | $rootScope.$digest(); 92 | 93 | $rootScope.$$childHead.selectItem(3); 94 | expect(callback).toHaveBeenCalled(); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /test/unit/placeholder.spec.js: -------------------------------------------------------------------------------- 1 | describe("Mac placeholder", function() { 2 | beforeEach(module("Mac")); 3 | 4 | describe("initialization", function() { 5 | var $compile, $rootScope; 6 | beforeEach(inject(function(_$compile_, _$rootScope_) { 7 | $compile = _$compile_; 8 | $rootScope = _$rootScope_; 9 | })); 10 | 11 | it("should set placeholder with a scope variable", function() { 12 | var element = $compile("")($rootScope); 13 | $rootScope.placeholder = "Test"; 14 | $rootScope.$digest(); 15 | 16 | expect(element.prop("placeholder")).toBe("Test"); 17 | }); 18 | 19 | it("should set placeholder with string variable", function() { 20 | var element = $compile("")($rootScope); 21 | $rootScope.$digest(); 22 | 23 | expect(element.prop("placeholder")).toBe("foobar"); 24 | }); 25 | 26 | it("should not have placeholder property", function() { 27 | var element = $compile("")($rootScope); 28 | $rootScope.$digest(); 29 | 30 | expect(element.prop("placeholder")).toBe(""); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /test/unit/popover.spec.js: -------------------------------------------------------------------------------- 1 | describe("Popover directive", function() { 2 | var $rootScope, popover, $compile; 3 | beforeEach(module("Mac")); 4 | 5 | beforeEach(inject(function(_$compile_, _$rootScope_, _popover_) { 6 | $compile = _$compile_; 7 | $rootScope = _$rootScope_; 8 | popover = _popover_; 9 | })); 10 | 11 | describe("popover element", function() { 12 | var element; 13 | 14 | beforeEach(function () { 15 | var template = "
Test
"; 16 | element = $compile(template)($rootScope); 17 | $rootScope.$digest(); 18 | }); 19 | 20 | it("should throw an error when id is missing", function() { 21 | var toCompile = function() { 22 | $compile("")($rootScope); 23 | }; 24 | 25 | expect(toCompile).toThrow(Error('macPopover: Missing id')); 26 | }); 27 | 28 | it("should register the popover", function() { 29 | expect(popover.registered.testPopover).toBeDefined(); 30 | }); 31 | 32 | it("should store the correct template", function() { 33 | var popoverObj = popover.registered.testPopover; 34 | expect(popoverObj.template).toBe("Test"); 35 | }); 36 | 37 | it("should be replaced with a comment", function() { 38 | expect(element.contents()[0].nodeType).toBe(8); 39 | }); 40 | 41 | it("should interpolate id before registering", function() { 42 | var template; 43 | $rootScope.someId = '12345'; 44 | template = "
Test
"; 45 | element = $compile(template)($rootScope); 46 | $rootScope.$digest(); 47 | expect(popover.registered.testPopover12345).toBeDefined(); 48 | }); 49 | }); 50 | 51 | describe("popover element register", function() { 52 | it('should register with refreshOn options', function() { 53 | var template = "
Test
"; 54 | $compile(template)($rootScope); 55 | $rootScope.$digest(); 56 | 57 | var testPopoverOptions = popover.registered.testPopover 58 | expect(testPopoverOptions).toBeDefined(); 59 | expect(testPopoverOptions.refreshOn).toBe('testEvent'); 60 | }); 61 | }); 62 | 63 | describe("popover trigger", function () { 64 | var trigger, $timeout; 65 | 66 | beforeEach(inject(function (_$timeout_) { 67 | $timeout = _$timeout_; 68 | 69 | trigger = $compile("
")($rootScope); 70 | $rootScope.$digest(); 71 | 72 | spyOn(popover, 'show'); 73 | spyOn(popover, 'hide'); 74 | })); 75 | 76 | it('should bind click event', function () { 77 | var bindTrigger = angular.element("
"); 78 | spyOn(bindTrigger[0], 'addEventListener'); 79 | 80 | $compile(bindTrigger)($rootScope); 81 | $rootScope.$digest(); 82 | 83 | expect(bindTrigger[0].addEventListener).toHaveBeenCalled(); 84 | expect(bindTrigger[0].addEventListener.calls.argsFor(0)[0]).toEqual('click'); 85 | }); 86 | 87 | it('should bind focus event', function () { 88 | var bindTrigger = angular.element("
"); 89 | spyOn(bindTrigger[0], 'addEventListener'); 90 | 91 | $compile(bindTrigger)($rootScope); 92 | $rootScope.$digest(); 93 | 94 | expect(bindTrigger[0].addEventListener).toHaveBeenCalled(); 95 | expect(bindTrigger[0].addEventListener.calls.argsFor(0)[0]).toEqual('focusin'); 96 | expect(bindTrigger[0].addEventListener.calls.argsFor(1)[0]).toEqual('focusout'); 97 | }); 98 | 99 | it('should bind hover event', function () { 100 | var bindTrigger = angular.element("
"); 101 | spyOn(bindTrigger[0], 'addEventListener'); 102 | 103 | $compile(bindTrigger)($rootScope); 104 | $rootScope.$digest(); 105 | 106 | expect(bindTrigger[0].addEventListener).toHaveBeenCalled(); 107 | expect(bindTrigger[0].addEventListener.calls.argsFor(0)[0]).toEqual('mouseover'); 108 | expect(bindTrigger[0].addEventListener.calls.argsFor(1)[0]).toEqual('mouseout'); 109 | }); 110 | 111 | it('should show popover', function () { 112 | trigger.triggerHandler('click'); 113 | $timeout.flush(); 114 | 115 | expect(popover.show).toHaveBeenCalled(); 116 | }); 117 | 118 | it('should toggle and hide popover when clicking the same trigger', function () { 119 | spyOn(popover, 'last').and.returnValue({ 120 | element: trigger 121 | }); 122 | 123 | trigger.triggerHandler('click'); 124 | $timeout.flush(); 125 | 126 | expect(popover.hide).toHaveBeenCalled(); 127 | expect(popover.show).not.toHaveBeenCalled(); 128 | }); 129 | 130 | it('should hide and show new popover', function () { 131 | spyOn(popover, 'last').and.returnValue({ 132 | element: angular.element('
') 133 | }); 134 | 135 | trigger.triggerHandler('click'); 136 | $timeout.flush(); 137 | 138 | expect(popover.hide).toHaveBeenCalled(); 139 | expect(popover.show).toHaveBeenCalled(); 140 | }); 141 | 142 | it('should hide on scope $destroy', function () { 143 | $rootScope.$destroy(); 144 | $timeout.flush(); 145 | 146 | expect(popover.hide).toHaveBeenCalledWith(trigger); 147 | }); 148 | }); 149 | }); 150 | -------------------------------------------------------------------------------- /test/unit/scroll_spy.spec.js: -------------------------------------------------------------------------------- 1 | describe("Mac scroll spy", function() { 2 | var $compile, $rootScope, scrollspy; 3 | beforeEach(module("Mac")); 4 | 5 | beforeEach(inject(function(_$rootScope_, _$compile_, _scrollSpy_) { 6 | $rootScope = _$rootScope_; 7 | $compile = _$compile_; 8 | scrollspy = _scrollSpy_; 9 | })); 10 | 11 | describe("scrollspy service", function() { 12 | it("should register an anchor", function() { 13 | var element = angular.element("
"), anchor; 14 | angular.element(document.body).append(element); 15 | anchor = scrollspy.register("test", element); 16 | 17 | expect(scrollspy.registered.length).toBe(1); 18 | expect(anchor).toBeDefined(); 19 | }); 20 | 21 | it("should unregister an anchor", function() { 22 | var element = angular.element("
"); 23 | angular.element(document.body).append(element); 24 | scrollspy.register("test", element); 25 | scrollspy.unregister("test"); 26 | 27 | expect(scrollspy.registered.length).toBe(0); 28 | }); 29 | 30 | it("should sort correctly based on top", function () { 31 | scrollspy.registered.push({id: 'test1', top: 200}); 32 | scrollspy.registered.push({id: 'test1', top: 300}); 33 | scrollspy.registered.push({id: 'test1', top: 100}); 34 | scrollspy.registered.push({id: 'test1', top: 400}); 35 | 36 | scrollspy.sort(); 37 | 38 | expect(scrollspy.registered[0].top).toBe(100); 39 | expect(scrollspy.registered[1].top).toBe(200); 40 | expect(scrollspy.registered[2].top).toBe(300); 41 | expect(scrollspy.registered[3].top).toBe(400); 42 | }); 43 | 44 | it("should add listener", function() { 45 | var callback = angular.noop; 46 | 47 | scrollspy.addListener(callback); 48 | expect(scrollspy.listeners.length).toBe(1); 49 | }); 50 | 51 | it("should remove listener", function() { 52 | var callback = angular.noop; 53 | 54 | scrollspy.addListener(callback); 55 | scrollspy.removeListener(callback); 56 | 57 | expect(scrollspy.listeners.length).toBe(0); 58 | }); 59 | 60 | it("should not update when active is the same element", function() { 61 | var callback = jasmine.createSpy("listener"); 62 | scrollspy.active = { 63 | id: 'current' 64 | }; 65 | 66 | scrollspy.addListener(callback); 67 | scrollspy.setActive({ 68 | id: 'current' 69 | }); 70 | 71 | expect(callback).not.toHaveBeenCalled(); 72 | }); 73 | 74 | it("should update active and fire listener", function() { 75 | var callback = jasmine.createSpy("listener"); 76 | scrollspy.active = { 77 | id: 'previous' 78 | }; 79 | 80 | scrollspy.addListener(callback); 81 | scrollspy.setActive({}); 82 | 83 | expect(callback).toHaveBeenCalled(); 84 | }); 85 | }); 86 | 87 | describe("initializing container", function () { 88 | var container; 89 | 90 | beforeEach(function () { 91 | container = angular.element('
'); 92 | }); 93 | 94 | afterEach(function () { 95 | container.remove(); 96 | }); 97 | 98 | it('should not call setActive when there is nothing registered', function () { 99 | spyOn(scrollspy, 'setActive'); 100 | spyOn(scrollspy, 'last'); 101 | 102 | $compile(container)($rootScope); 103 | $rootScope.$digest(); 104 | 105 | container.triggerHandler('scroll'); 106 | 107 | expect(scrollspy.setActive).not.toHaveBeenCalled(); 108 | }); 109 | 110 | it('should call setActive with last anchor', function () { 111 | scrollspy.registered[0] = {}; 112 | 113 | spyOn(scrollspy, 'setActive'); 114 | spyOn(scrollspy, 'last'); 115 | 116 | $compile(container)($rootScope); 117 | $rootScope.$digest(); 118 | 119 | container.triggerHandler('scroll'); 120 | 121 | expect(scrollspy.setActive).toHaveBeenCalled(); 122 | expect(scrollspy.last).toHaveBeenCalled(); 123 | }); 124 | 125 | it('should set the anchor active', function () { 126 | spyOn(scrollspy, 'setActive'); 127 | 128 | container[0].setAttribute('style', 'height: 200px; padding-bottom: 500px;'); 129 | angular.element(document.body).append(container); 130 | 131 | var anchor = {top: 0}; 132 | scrollspy.registered[0] = anchor; 133 | 134 | $compile(container)($rootScope); 135 | $rootScope.$digest(); 136 | 137 | container.triggerHandler('scroll'); 138 | 139 | expect(scrollspy.setActive).toHaveBeenCalled(); 140 | expect(scrollspy.setActive.calls.argsFor(0)[0]).toBe(anchor); 141 | }); 142 | }); 143 | 144 | describe("initializing an anchor", function() { 145 | it("should register with the service", function() { 146 | var element = angular.element("
"); 147 | angular.element(document.body).append(element); 148 | $compile(element)($rootScope); 149 | $rootScope.$digest(); 150 | 151 | expect(scrollspy.registered.length).toBe(1); 152 | expect(scrollspy.registered[0].id).toBe("test-anchor"); 153 | }); 154 | 155 | it("should register with the service with an interpolated id in mac-scroll-spy-anchor attr", function() { 156 | var element; 157 | $rootScope.name = 'test-anchor2'; 158 | element = angular.element("
"); 159 | angular.element(document.body).append(element); 160 | $compile(element)($rootScope); 161 | $rootScope.$digest(); 162 | 163 | expect(scrollspy.registered.length).toBe(1); 164 | expect(scrollspy.registered[0].id).toBe("test-anchor2"); 165 | }); 166 | 167 | it("should register with the service with an interpolated id", function() { 168 | var element; 169 | $rootScope.name = 'test-anchor4'; 170 | element = angular.element("
"); 171 | angular.element(document.body).append(element); 172 | $compile(element)($rootScope); 173 | $rootScope.$digest(); 174 | 175 | expect(scrollspy.registered.length).toBe(1); 176 | expect(scrollspy.registered[0].id).toBe("test-anchor4"); 177 | }); 178 | 179 | it("should throw an error when id is not provided", function() { 180 | var create = function() { 181 | $compile("
")($rootScope); 182 | }; 183 | expect(create).toThrow(); 184 | }); 185 | 186 | it("should update anchor on refresh-scroll-spy event", function() { 187 | var element, origTop; 188 | element = angular.element("
"); 189 | angular.element(document.body).append(element); 190 | $compile(element)($rootScope); 191 | 192 | origTop = element.offset().top; 193 | element.css("margin-top", "200px"); 194 | 195 | $rootScope.$broadcast("refresh-scroll-spy"); 196 | $rootScope.$digest(); 197 | 198 | expect(origTop).not.toBe(scrollspy.registered[0].top); 199 | }); 200 | 201 | it("should unregister when scope gets destroy", function() { 202 | var element = angular.element("
"); 203 | angular.element(document.body).append(element); 204 | $compile(element)($rootScope); 205 | $rootScope.$digest(); 206 | 207 | $rootScope.$destroy(); 208 | expect(scrollspy.registered.length).toBe(0); 209 | }); 210 | }); 211 | 212 | describe("scroll spy target", function() { 213 | it("should throw an error when name is not provided", function() { 214 | var create = function() { 215 | $compile("
")($rootScope); 216 | }; 217 | expect(create).toThrow(); 218 | }); 219 | 220 | it("should add a listener with interpolated name", function() { 221 | $rootScope.name = "test"; 222 | $compile("
")($rootScope); 223 | $rootScope.$digest(); 224 | 225 | expect(scrollspy.listeners.length).toBe(1); 226 | }); 227 | 228 | it("should add a listener", function() { 229 | $compile("
")($rootScope); 230 | $rootScope.$digest(); 231 | 232 | expect(scrollspy.listeners.length).toBe(1); 233 | }); 234 | 235 | it("should add 'active' class", function() { 236 | var element = $compile("
")($rootScope); 237 | $rootScope.$digest(); 238 | 239 | $rootScope.$apply(function() { 240 | scrollspy.setActive({ 241 | id: "test", 242 | element: angular.element("
"), 243 | top: 123 244 | }); 245 | }); 246 | 247 | expect(element.hasClass("active")).toBeTruthy(); 248 | }); 249 | 250 | it("should add custom set class", function() { 251 | var element = $compile("
")($rootScope); 252 | $rootScope.$digest(); 253 | 254 | $rootScope.$apply(function() { 255 | scrollspy.setActive({ 256 | id: "test", 257 | element: angular.element("
"), 258 | top: 123 259 | }); 260 | }); 261 | 262 | expect(element.hasClass("class2")).toBeTruthy(); 263 | }); 264 | 265 | it("should remove listener when scope is destroyed", function() { 266 | $compile("
")($rootScope); 267 | $rootScope.$digest(); 268 | 269 | $rootScope.$destroy(); 270 | expect(scrollspy.listeners.length).toBe(0); 271 | }); 272 | }); 273 | }); 274 | -------------------------------------------------------------------------------- /test/unit/services/time_util.spec.js: -------------------------------------------------------------------------------- 1 | describe('Mac Time Util', function () { 2 | var macTimeUtil, $timeout, element; 3 | 4 | beforeEach(module('Mac')); 5 | 6 | beforeEach(inject(function(_$timeout_, _macTimeUtil_) { 7 | $timeout = _$timeout_; 8 | macTimeUtil = _macTimeUtil_; 9 | 10 | // Create a fake object to simulate text input since PhantomJS 11 | // has trouble with element.selectionStart 12 | element = [{ 13 | selectionStart: 0, 14 | selectionEnd: 0, 15 | setSelectionRange: function (start, end) { 16 | this.selectionStart = start; 17 | this.selectionEnd = end; 18 | } 19 | }]; 20 | })); 21 | 22 | describe('initializeTime', function () { 23 | it('should use options passed in', function () { 24 | var today = new Date(), 25 | output = macTimeUtil.initializeTime({ 26 | default: '02:00 AM' 27 | }), 28 | expected = new Date(today.toDateString() + ' 02:00 AM'); 29 | 30 | expect(output).toEqual(expected); 31 | }); 32 | 33 | it('should use default when option is invalid', function () { 34 | var today = new Date(), 35 | output = macTimeUtil.initializeTime({ 36 | default: '14:00 AM' 37 | }), 38 | expected = new Date(today.toDateString() + ' 12:00 AM'); 39 | 40 | expect(output).toEqual(expected); 41 | }); 42 | }); 43 | 44 | describe('getSelection', function () { 45 | it('should select hour from 0 to 2', function () { 46 | var i; 47 | for (i = 0; i < 3; i++) { 48 | element[0].selectionStart = i; 49 | expect(macTimeUtil.getSelection(element)).toBe('hour'); 50 | } 51 | }); 52 | 53 | it('should select hour from 0 to 2', function () { 54 | var i; 55 | for (i = 0; i < 3; i++) { 56 | element[0].selectionStart = i; 57 | expect(macTimeUtil.getSelection(element)).toBe('hour'); 58 | } 59 | }); 60 | 61 | it('should select hour from 3 to 5', function () { 62 | var i; 63 | for (i = 3; i < 6; i++) { 64 | element[0].selectionStart = i; 65 | expect(macTimeUtil.getSelection(element)).toBe('minute'); 66 | } 67 | }); 68 | 69 | it('should select hour from 6 to 8', function () { 70 | var i; 71 | for (i = 6; i < 9; i++) { 72 | element[0].selectionStart = i; 73 | expect(macTimeUtil.getSelection(element)).toBe('meridian'); 74 | } 75 | }); 76 | }); 77 | 78 | describe('setSelectionRange', function () { 79 | it('should selectRange properly', function () { 80 | macTimeUtil.selectRange(element, 3, 5); 81 | 82 | $timeout.flush(); 83 | 84 | expect(element[0].selectionStart).toBe(3); 85 | expect(element[0].selectionEnd).toBe(5); 86 | }); 87 | 88 | it('should selectHours properly', function () { 89 | macTimeUtil.selectHours(element); 90 | 91 | $timeout.flush(); 92 | 93 | expect(element[0].selectionStart).toBe(0); 94 | expect(element[0].selectionEnd).toBe(2); 95 | }); 96 | 97 | it('should selectMinutes properly', function () { 98 | macTimeUtil.selectMinutes(element); 99 | 100 | $timeout.flush(); 101 | 102 | expect(element[0].selectionStart).toBe(3); 103 | expect(element[0].selectionEnd).toBe(5); 104 | }); 105 | 106 | it('should selectMeridian properly', function () { 107 | macTimeUtil.selectMeridian(element); 108 | 109 | $timeout.flush(); 110 | 111 | expect(element[0].selectionStart).toBe(6); 112 | expect(element[0].selectionEnd).toBe(8); 113 | }); 114 | }); 115 | 116 | describe('selectSection', function () { 117 | it('should selectNextSection minute', function () { 118 | macTimeUtil.selectHours(element); 119 | $timeout.flush(); 120 | macTimeUtil.selectNextSection(element); 121 | $timeout.flush(); 122 | 123 | expect(element[0].selectionStart).toBe(3); 124 | expect(element[0].selectionEnd).toBe(5); 125 | }); 126 | 127 | it('should selectNextSection meridian', function () { 128 | macTimeUtil.selectMinutes(element); 129 | $timeout.flush(); 130 | macTimeUtil.selectNextSection(element); 131 | $timeout.flush(); 132 | 133 | expect(element[0].selectionStart).toBe(6); 134 | expect(element[0].selectionEnd).toBe(8); 135 | }); 136 | 137 | it('should selectNextSection meridian', function () { 138 | macTimeUtil.selectMeridian(element); 139 | $timeout.flush(); 140 | macTimeUtil.selectNextSection(element); 141 | $timeout.flush(); 142 | 143 | expect(element[0].selectionStart).toBe(6); 144 | expect(element[0].selectionEnd).toBe(8); 145 | }); 146 | 147 | it('should selectPreviousSection hour', function () { 148 | macTimeUtil.selectHours(element); 149 | $timeout.flush(); 150 | macTimeUtil.selectPreviousSection(element); 151 | $timeout.flush(); 152 | 153 | expect(element[0].selectionStart).toBe(0); 154 | expect(element[0].selectionEnd).toBe(2); 155 | }); 156 | 157 | it('should selectPreviousSection hour', function () { 158 | macTimeUtil.selectMinutes(element); 159 | $timeout.flush(); 160 | macTimeUtil.selectPreviousSection(element); 161 | $timeout.flush(); 162 | 163 | expect(element[0].selectionStart).toBe(0); 164 | expect(element[0].selectionEnd).toBe(2); 165 | }); 166 | 167 | it('should selectPreviousSection minute', function () { 168 | macTimeUtil.selectMeridian(element); 169 | $timeout.flush(); 170 | macTimeUtil.selectPreviousSection(element); 171 | $timeout.flush(); 172 | 173 | expect(element[0].selectionStart).toBe(3); 174 | expect(element[0].selectionEnd).toBe(5); 175 | }); 176 | }); 177 | 178 | describe('setMeridian', function () { 179 | it('should flip hour to AM', function () { 180 | var time = new Date('Thu Jan 01 2015 13:12:00 GMT-0800 (PST)'); 181 | 182 | spyOn(time, 'getHours').and.returnValue(13); 183 | spyOn(time, 'setHours'); 184 | 185 | macTimeUtil.setMeridian(time, 'AM'); 186 | expect(time.setHours.calls.argsFor(0)[0]).toBe(1); 187 | }); 188 | 189 | it('should keep the hour for AM', function () { 190 | var time = new Date('Thu Jan 01 2015 07:12:00 GMT-0800 (PST)'); 191 | 192 | spyOn(time, 'getHours').and.returnValue(7); 193 | spyOn(time, 'setHours'); 194 | 195 | macTimeUtil.setMeridian(time, 'AM'); 196 | expect(time.setHours.calls.argsFor(0)[0]).toBe(7); 197 | }); 198 | 199 | it('should flip hour to PM', function () { 200 | var time = new Date('Thu Jan 01 2015 07:12:00 GMT-0800 (PST)'); 201 | 202 | spyOn(time, 'getHours').and.returnValue(7); 203 | spyOn(time, 'setHours'); 204 | 205 | macTimeUtil.setMeridian(time, 'PM'); 206 | expect(time.setHours.calls.argsFor(0)[0]).toBe(19); 207 | }); 208 | 209 | it('should keep the hour for PM', function () { 210 | var time = new Date('Thu Jan 01 2015 15:12:00 GMT-0800 (PST)'); 211 | 212 | spyOn(time, 'getHours').and.returnValue(15); 213 | spyOn(time, 'setHours'); 214 | 215 | macTimeUtil.setMeridian(time, 'PM'); 216 | expect(time.setHours.calls.argsFor(0)[0]).toBe(15); 217 | }); 218 | 219 | it('should keep the hour with invalid meridian value', function () { 220 | var time = new Date('Thu Jan 01 2015 15:12:00 GMT-0800 (PST)'); 221 | 222 | spyOn(time, 'getHours').and.returnValue(15); 223 | spyOn(time, 'setHours'); 224 | 225 | macTimeUtil.setMeridian(time, 'GM'); 226 | expect(time.setHours.calls.argsFor(0)[0]).toBe(15); 227 | }); 228 | }); 229 | 230 | describe('toggleMeridian', function () { 231 | it('should change 7 -> 19', function () { 232 | var time = new Date('Thu Jan 01 2015 07:12:00 GMT-0800 (PST)'); 233 | 234 | spyOn(time, 'getHours').and.returnValue(7); 235 | spyOn(time, 'setHours'); 236 | 237 | macTimeUtil.toggleMeridian(time); 238 | expect(time.setHours.calls.argsFor(0)[0]).toBe(19); 239 | }); 240 | 241 | it('should change 19 -> 7', function () { 242 | var time = new Date('Thu Jan 01 2015 19:12:00 GMT-0800 (PST)'); 243 | 244 | spyOn(time, 'getHours').and.returnValue(19); 245 | spyOn(time, 'setHours'); 246 | 247 | macTimeUtil.toggleMeridian(time); 248 | expect(time.setHours.calls.argsFor(0)[0]).toBe(7); 249 | }); 250 | }); 251 | 252 | describe('increment', function () { 253 | var time; 254 | 255 | beforeEach(function() { 256 | time = new Date('Thu Jan 01 2015 05:14:00 GMT-0800 (PST)'); 257 | 258 | spyOn(time, 'getHours').and.returnValue(5); 259 | spyOn(time, 'setHours'); 260 | spyOn(time, 'setMinutes'); 261 | }); 262 | 263 | it('should increment hour', function() { 264 | macTimeUtil.incrementHour(time, 5); 265 | expect(time.setHours.calls.argsFor(0)[0]).toBe(10); 266 | }); 267 | 268 | it('should decrement hour', function() { 269 | macTimeUtil.incrementHour(time, -2); 270 | expect(time.setHours.calls.argsFor(0)[0]).toBe(3); 271 | }); 272 | 273 | it('should increment minute', function() { 274 | macTimeUtil.incrementMinute(time, 26); 275 | expect(time.setMinutes.calls.argsFor(0)[0]).toBe(40); 276 | }); 277 | 278 | it('should decrement minute', function() { 279 | macTimeUtil.incrementMinute(time, -12); 280 | expect(time.setMinutes.calls.argsFor(0)[0]).toBe(2); 281 | }); 282 | }); 283 | }); 284 | -------------------------------------------------------------------------------- /test/unit/spinner.spec.js: -------------------------------------------------------------------------------- 1 | describe("Mac Spinner", function() { 2 | beforeEach(module("Mac")); 3 | describe("Basic Initialization", function() { 4 | var $compile, $rootScope, prefixes; 5 | prefixes = ["webkit", "Moz", "ms", "O"]; 6 | 7 | beforeEach(inject(function(_$compile_, _$rootScope_) { 8 | $compile = _$compile_; 9 | $rootScope = _$rootScope_; 10 | })); 11 | 12 | it("should replace with template", function() { 13 | var element = $compile("")($rootScope); 14 | $rootScope.$digest(); 15 | 16 | expect(element[0].className.indexOf("mac-spinner")).not.toBe(-1); 17 | }); 18 | 19 | it("should create 10 bars", function() { 20 | var bars, element; 21 | element = $compile("")($rootScope); 22 | $rootScope.$digest(); 23 | 24 | bars = element[0].querySelectorAll(".bar"); 25 | 26 | return expect(bars.length).toBe(10); 27 | }); 28 | 29 | it("should update animation", function() { 30 | var bar, element, hasAnimation, i, len, prefix; 31 | element = $compile("")($rootScope); 32 | $rootScope.$digest(); 33 | 34 | bar = element[0].querySelector(".bar"); 35 | hasAnimation = false; 36 | 37 | for (i = 0, len = prefixes.length; i < len; i++) { 38 | prefix = prefixes[i]; 39 | if (bar.style[prefix + "Animation"] != null) { 40 | hasAnimation = true; 41 | } 42 | } 43 | 44 | expect(hasAnimation).toBe(true); 45 | }); 46 | 47 | it("should update transform", function() { 48 | var bar, element, hasTransform, i, len, prefix; 49 | 50 | element = $compile("")($rootScope); 51 | $rootScope.$digest(); 52 | 53 | bar = element[0].querySelector(".bar"); 54 | hasTransform = false; 55 | 56 | for (i = 0, len = prefixes.length; i < len; i++) { 57 | prefix = prefixes[i]; 58 | if (bar.style[prefix + "Transform"] != null) { 59 | hasTransform = true; 60 | } 61 | } 62 | 63 | expect(hasTransform).toBe(true); 64 | }); 65 | 66 | it("should update spinner size", function() { 67 | var element, isCorrectSize; 68 | element = $compile("")($rootScope); 69 | $rootScope.$digest(); 70 | 71 | isCorrectSize = element[0].style.height === "30px" && element[0].style.width === "30px"; 72 | 73 | expect(isCorrectSize).toBe(true); 74 | }); 75 | 76 | it("should update z-index", function() { 77 | var element = $compile("")($rootScope); 78 | $rootScope.$digest(); 79 | 80 | expect(element.css("zIndex")).toBe("9001"); 81 | }); 82 | 83 | it("should update the background color", function() { 84 | var bar, element; 85 | element = $compile("")($rootScope); 86 | $rootScope.$digest(); 87 | 88 | bar = element[0].querySelector(".bar"); 89 | 90 | expect(bar.style.background.indexOf("rgb(18, 49, 35)")).not.toBe(-1); 91 | }); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /test/unit/tooltip.spec.js: -------------------------------------------------------------------------------- 1 | describe("Mac Tooltip", function() { 2 | var $compile, $rootScope, $timeout; 3 | 4 | function queryTooltip () { 5 | return document.querySelector(".mac-tooltip"); 6 | } 7 | 8 | beforeEach(module("Mac")); 9 | beforeEach(inject(function(_$compile_, _$rootScope_, _$timeout_) { 10 | $compile = _$compile_; 11 | $rootScope = _$rootScope_; 12 | $timeout = _$timeout_; 13 | })); 14 | 15 | afterEach(function() { 16 | var i, tooltip, tooltips; 17 | tooltips = document.querySelectorAll(".mac-tooltip"); 18 | for (i = 0; i < tooltips.length; i++) { 19 | tooltip = tooltips[i]; 20 | tooltip.parentNode.removeChild(tooltip); 21 | } 22 | }); 23 | 24 | describe("Basic Initialization", function() { 25 | it("should append to body on mouseenter", function() { 26 | var tip = $compile("
")($rootScope); 27 | $rootScope.$digest(); 28 | 29 | tip.triggerHandler("mouseenter"); 30 | 31 | expect(queryTooltip()).not.toBe(null); 32 | }); 33 | 34 | it("should only append one tooltip", function() { 35 | var tip, tooltip; 36 | tip = $compile("
")($rootScope); 37 | $rootScope.$digest(); 38 | 39 | tip.triggerHandler("mouseenter"); 40 | tip.triggerHandler("mouseenter"); 41 | 42 | tooltip = document.querySelectorAll(".mac-tooltip"); 43 | expect(tooltip.length).toBe(1); 44 | }); 45 | 46 | it("should display the correct message", function() { 47 | var text, tip = $compile("
")($rootScope); 48 | $rootScope.$digest(); 49 | 50 | tip.triggerHandler("mouseenter"); 51 | 52 | text = queryTooltip().innerText || queryTooltip().textContent; 53 | expect(text).toBe("hello world"); 54 | }); 55 | }); 56 | 57 | describe("Trigger", function() { 58 | it("should remove tooltip on mouseleave", function() { 59 | var tip = $compile("
")($rootScope); 60 | $rootScope.$digest(); 61 | 62 | tip.triggerHandler("mouseenter"); 63 | 64 | expect(queryTooltip()).not.toBe(null); 65 | 66 | tip.triggerHandler("mouseleave"); 67 | $timeout.flush(); 68 | 69 | expect(queryTooltip()).toBe(null); 70 | }); 71 | 72 | it("should show and hide on click", function() { 73 | var tip = $compile("
")($rootScope); 74 | $rootScope.$digest(); 75 | 76 | tip.triggerHandler("click"); 77 | 78 | expect(queryTooltip()).not.toBe(null); 79 | 80 | tip.triggerHandler("click"); 81 | $timeout.flush(); 82 | 83 | expect(queryTooltip()).toBe(null); 84 | }); 85 | 86 | it("should throw an error with invalid trigger", function() { 87 | var compile = function() { 88 | $compile("
")($rootScope); 89 | $rootScope.$digest(); 90 | }; 91 | expect(compile).toThrow(); 92 | }); 93 | }); 94 | 95 | describe("Direction", function() { 96 | it("should set direction to top as default", function() { 97 | var tip = $compile("
")($rootScope); 98 | $rootScope.$digest(); 99 | 100 | tip.triggerHandler("mouseenter"); 101 | 102 | expect(queryTooltip().className.indexOf("top")).not.toBe(-1); 103 | }); 104 | 105 | it("should set the direction to bottom", function() { 106 | var tip = $compile("
")($rootScope); 107 | $rootScope.$digest(); 108 | 109 | tip.triggerHandler("mouseenter"); 110 | 111 | expect(queryTooltip().className.indexOf("bottom")).not.toBe(-1); 112 | }); 113 | }); 114 | 115 | describe("disabled", function() { 116 | it("should not create a tooltip", function() { 117 | var tip = $compile("
")($rootScope); 118 | $rootScope.$digest(); 119 | 120 | tip.triggerHandler("mouseenter"); 121 | 122 | expect(queryTooltip()).toBe(null); 123 | }); 124 | 125 | it("should create a tooltip", function() { 126 | var tip = $compile("
")($rootScope); 127 | $rootScope.$digest(); 128 | 129 | tip.triggerHandler("mouseenter"); 130 | 131 | expect(queryTooltip()).not.toBe(null); 132 | }); 133 | }); 134 | 135 | describe("Inside", function() { 136 | it("should append tooltip inside of trigger", function() { 137 | var tip = $compile("
")($rootScope); 138 | $rootScope.$digest(); 139 | 140 | tip.triggerHandler("mouseenter"); 141 | 142 | expect(tip[0].querySelector(".mac-tooltip")).not.toBe(null); 143 | }); 144 | 145 | it("should not append tooltip inside of trigger", function() { 146 | var tip = $compile("
")($rootScope); 147 | $rootScope.$digest(); 148 | 149 | tip.triggerHandler("mouseenter"); 150 | 151 | expect(tip[0].querySelector(".mac-tooltip")).toBe(null); 152 | }); 153 | 154 | it('should call not call offset', function() { 155 | spyOn(angular.element.prototype, 'offset'); 156 | 157 | var tip = $compile("
")($rootScope); 158 | $rootScope.$digest(); 159 | 160 | tip.triggerHandler("mouseenter"); 161 | 162 | expect(angular.element.prototype.offset).not.toHaveBeenCalled(); 163 | }) 164 | }); 165 | }); 166 | -------------------------------------------------------------------------------- /test/vendor/browserTrigger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (function() { 4 | var msie = parseInt((/msie (\d+)/.exec(navigator.userAgent.toLowerCase()) || [])[1], 10); 5 | 6 | function indexOf(array, obj) { 7 | if (array.indexOf) return array.indexOf(obj); 8 | 9 | for ( var i = 0; i < array.length; i++) { 10 | if (obj === array[i]) return i; 11 | } 12 | return -1; 13 | } 14 | 15 | 16 | 17 | /** 18 | * Triggers a browser event. Attempts to choose the right event if one is 19 | * not specified. 20 | * 21 | * @param {Object} element Either a wrapped jQuery/jqLite node or a DOMElement 22 | * @param {string} eventType Optional event type. 23 | * @param {Array.=} keys Optional list of pressed keys 24 | * (valid values: 'alt', 'meta', 'shift', 'ctrl') 25 | * @param {number} x Optional x-coordinate for mouse/touch events. 26 | * @param {number} y Optional y-coordinate for mouse/touch events. 27 | */ 28 | window.browserTrigger = function browserTrigger(element, eventType, keys, x, y) { 29 | if (element && !element.nodeName) element = element[0]; 30 | if (!element) return; 31 | 32 | var inputType = (element.type) ? element.type.toLowerCase() : null, 33 | nodeName = element.nodeName.toLowerCase(); 34 | 35 | if (!eventType) { 36 | eventType = { 37 | 'text': 'change', 38 | 'textarea': 'change', 39 | 'hidden': 'change', 40 | 'password': 'change', 41 | 'button': 'click', 42 | 'submit': 'click', 43 | 'reset': 'click', 44 | 'image': 'click', 45 | 'checkbox': 'click', 46 | 'radio': 'click', 47 | 'select-one': 'change', 48 | 'select-multiple': 'change', 49 | '_default_': 'click' 50 | }[inputType || '_default_']; 51 | } 52 | 53 | if (nodeName == 'option') { 54 | element.parentNode.value = element.value; 55 | element = element.parentNode; 56 | eventType = 'change'; 57 | } 58 | 59 | keys = keys || []; 60 | function pressed(key) { 61 | return indexOf(keys, key) !== -1; 62 | } 63 | 64 | if (msie < 9) { 65 | if (inputType == 'radio' || inputType == 'checkbox') { 66 | element.checked = !element.checked; 67 | } 68 | 69 | // WTF!!! Error: Unspecified error. 70 | // Don't know why, but some elements when detached seem to be in inconsistent state and 71 | // calling .fireEvent() on them will result in very unhelpful error (Error: Unspecified error) 72 | // forcing the browser to compute the element position (by reading its CSS) 73 | // puts the element in consistent state. 74 | element.style.posLeft; 75 | 76 | // TODO(vojta): create event objects with pressed keys to get it working on IE<9 77 | var ret = element.fireEvent('on' + eventType); 78 | if (inputType == 'submit') { 79 | while(element) { 80 | if (element.nodeName.toLowerCase() == 'form') { 81 | element.fireEvent('onsubmit'); 82 | break; 83 | } 84 | element = element.parentNode; 85 | } 86 | } 87 | return ret; 88 | } else { 89 | var evnt = document.createEvent('MouseEvents'), 90 | originalPreventDefault = evnt.preventDefault, 91 | appWindow = element.ownerDocument.defaultView, 92 | fakeProcessDefault = true, 93 | finalProcessDefault, 94 | angular = appWindow.angular || {}; 95 | 96 | // igor: temporary fix for https://bugzilla.mozilla.org/show_bug.cgi?id=684208 97 | angular['ff-684208-preventDefault'] = false; 98 | evnt.preventDefault = function() { 99 | fakeProcessDefault = false; 100 | return originalPreventDefault.apply(evnt, arguments); 101 | }; 102 | 103 | x = x || 0; 104 | y = y || 0; 105 | evnt.initMouseEvent(eventType, true, true, window, 0, x, y, x, y, pressed('ctrl'), pressed('alt'), 106 | pressed('shift'), pressed('meta'), 0, element); 107 | 108 | element.dispatchEvent(evnt); 109 | finalProcessDefault = !(angular['ff-684208-preventDefault'] || !fakeProcessDefault); 110 | 111 | delete angular['ff-684208-preventDefault']; 112 | 113 | return finalProcessDefault; 114 | } 115 | } 116 | }()); 117 | --------------------------------------------------------------------------------