rails_angularui_bootstrap 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. checksums.yaml +15 -0
  2. data/MIT-LICENSE +20 -0
  3. data/Rakefile +9 -0
  4. data/lib/rails_angularui_bootstrap.rb +11 -0
  5. data/lib/rails_angularui_bootstrap/railtie.rb +10 -0
  6. data/lib/rails_angularui_bootstrap/version.rb +3 -0
  7. data/lib/tasks/rails_angularui_bootstrap.rake +180 -0
  8. data/vendor/assets/javascripts/rails-angularui-bootstrap/index.js +5 -0
  9. data/vendor/assets/javascripts/rails-angularui-bootstrap/rails_angularui_bootstrap.coffee +15 -0
  10. data/vendor/assets/javascripts/rails-angularui-bootstrap/ui-bootstrap-0.6.0-SNAPSHOT.js +3629 -0
  11. data/vendor/assets/javascripts/templates/rails-angularui-bootstrap/accordion/accordion-group.hamlc +6 -0
  12. data/vendor/assets/javascripts/templates/rails-angularui-bootstrap/accordion/accordion.hamlc +1 -0
  13. data/vendor/assets/javascripts/templates/rails-angularui-bootstrap/alert/alert.hamlc +3 -0
  14. data/vendor/assets/javascripts/templates/rails-angularui-bootstrap/carousel/carousel.hamlc +8 -0
  15. data/vendor/assets/javascripts/templates/rails-angularui-bootstrap/carousel/slide.hamlc +1 -0
  16. data/vendor/assets/javascripts/templates/rails-angularui-bootstrap/datepicker/datepicker.hamlc +27 -0
  17. data/vendor/assets/javascripts/templates/rails-angularui-bootstrap/datepicker/popup.hamlc +9 -0
  18. data/vendor/assets/javascripts/templates/rails-angularui-bootstrap/dialog/message.hamlc +6 -0
  19. data/vendor/assets/javascripts/templates/rails-angularui-bootstrap/modal/backdrop.hamlc +1 -0
  20. data/vendor/assets/javascripts/templates/rails-angularui-bootstrap/modal/window.hamlc +3 -0
  21. data/vendor/assets/javascripts/templates/rails-angularui-bootstrap/pagination/pager.hamlc +3 -0
  22. data/vendor/assets/javascripts/templates/rails-angularui-bootstrap/pagination/pagination.hamlc +3 -0
  23. data/vendor/assets/javascripts/templates/rails-angularui-bootstrap/popover/popover.hamlc +5 -0
  24. data/vendor/assets/javascripts/templates/rails-angularui-bootstrap/progressbar/bar.hamlc +1 -0
  25. data/vendor/assets/javascripts/templates/rails-angularui-bootstrap/progressbar/progress.hamlc +2 -0
  26. data/vendor/assets/javascripts/templates/rails-angularui-bootstrap/rating/rating.hamlc +2 -0
  27. data/vendor/assets/javascripts/templates/rails-angularui-bootstrap/tabs/tab.hamlc +2 -0
  28. data/vendor/assets/javascripts/templates/rails-angularui-bootstrap/tabs/tabset-titles.hamlc +1 -0
  29. data/vendor/assets/javascripts/templates/rails-angularui-bootstrap/tabs/tabset.hamlc +5 -0
  30. data/vendor/assets/javascripts/templates/rails-angularui-bootstrap/timepicker/timepicker.hamlc +27 -0
  31. data/vendor/assets/javascripts/templates/rails-angularui-bootstrap/tooltip/tooltip-html-unsafe-popup.hamlc +3 -0
  32. data/vendor/assets/javascripts/templates/rails-angularui-bootstrap/tooltip/tooltip-popup.hamlc +3 -0
  33. data/vendor/assets/javascripts/templates/rails-angularui-bootstrap/typeahead/typeahead-match.hamlc +1 -0
  34. data/vendor/assets/javascripts/templates/rails-angularui-bootstrap/typeahead/typeahead-popup.hamlc +3 -0
  35. metadata +134 -0
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ YjE5Njk4ZDAyYTI2ODUxNmUzMjM4Yjg1ZTlhZGNkOWRkZjg4YWEwZQ==
5
+ data.tar.gz: !binary |-
6
+ ODIyYWNiZjJiNjFkMGM0ZmZjZjc5NmNmOGJjZDhjODc5NWQyOGU3MA==
7
+ !binary "U0hBNTEy":
8
+ metadata.gz: !binary |-
9
+ YzM0MGU3Y2ZjMzg3NmI5YzlkZmYyYmVlYzgxMzkyYTQxY2Y1MTRjYzY3ZGYz
10
+ NGRjNjZjNGJkOGE5ZjUxZmU1ZmNlZjdjYTdlMjRlYWRhOTNlYWI0ZDJlYzcx
11
+ NWI3YTYzMTU0ZTkwNTAyNDA0YjJlNjkzOGVhZDlkZmVmYjZkMGE=
12
+ data.tar.gz: !binary |-
13
+ NThhYzA1NzkxMmFiMDdhNzcxNThjNzFkOTI4NzBlNWI0MmJkYWY4ZDc0MDM1
14
+ MzFjYzFhNzJhMzJhZjJhYzI3ZWIzYzBhMWVkOTY0Mzk4YmQ4MTA3ODM0NzA4
15
+ NzIyZDRjMThlMjQ5ZWIyM2VkMWE2NGNjYzA5YWJlMzRkMGRkZjQ=
@@ -0,0 +1,20 @@
1
+ Copyright 2013 Cameron Ellis
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,9 @@
1
+ load './lib/tasks/rails_angularui_bootstrap.rake'
2
+
3
+ namespace :angularui do
4
+ desc 'Download and install templates of given branch of angularui for the *gem*'
5
+ task :generate_templates, :repo, :branch do |t, args|
6
+ args.with_defaults(ctx: 'vendor')
7
+ gen_templates args
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ require "rails_angularui_bootstrap/version"
2
+ require "rails_angularui_bootstrap/railtie"
3
+
4
+ module Rails
5
+ module AngularUI
6
+ module Bootstrap
7
+ class Engine < ::Rails::Engine
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,10 @@
1
+ require 'rails_angularui_bootstrap'
2
+ require 'rails'
3
+
4
+ module RailsAngularuiBootstrap
5
+ class Railtie < Rails::Railtie
6
+ rake_tasks do
7
+ load File.expand_path('../../tasks/rails_angularui_bootstrap.rake', __FILE__)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,3 @@
1
+ module RailsAngularuiBootstrap
2
+ VERSION = "0.1.1"
3
+ end
@@ -0,0 +1,180 @@
1
+ require "json"
2
+ require "colorize"
3
+
4
+ namespace :angularui do
5
+ ##
6
+ # @param filename [String] the name of the file to write the version to.
7
+ # @param version_comments_list [String] the list of comment lines to write.
8
+ # @param offset [String] the offset so subsequent runs don't pile up
9
+ #
10
+ def write_version(filename, version_comments_list, offset)
11
+ old_file = File.open(filename, "r+")
12
+ old_file_lines = old_file.readlines
13
+ old_file.close
14
+
15
+ # add the require line to the top
16
+ new_file_with_version_lines = version_comments_list + old_file_lines.pop(old_file_lines.length - offset)
17
+
18
+ new_file = File.new(filename, "w")
19
+ new_file_with_version_lines.each do |line|
20
+ new_file.write(line)
21
+ end
22
+ new_file.close
23
+ end
24
+
25
+ ##
26
+ #
27
+ # @param [Hash] options
28
+ # @option options [String] :repo The name of the angularui/bootstrap repo or fork
29
+ # @option options [String] :branch The branch of the repo you wish to use
30
+ # @option options [String] :ctx Whether this is to be generated for the gem default templates or for the app using the gem
31
+ #
32
+ @has_run = false # running twice in client app and have no idea why
33
+ def gen_templates(options = {})
34
+
35
+ if @has_run
36
+ return
37
+ end
38
+
39
+ @has_run = true
40
+ puts "running generate_templates with #{options.inspect}".colorize(:light_blue)
41
+
42
+ defaults = {
43
+ repo: 'https://github.com/elerch/bootstrap.git', # using elerchs branch because I just need collapse / accordion to work for now
44
+ branch: 'bootstrap3_bis2',
45
+ ctx: 'app'
46
+ }
47
+
48
+ # if the app developer specified a repo, check to see if it's the default repo
49
+ need_clone = false
50
+ options = options.to_hash
51
+ if options.has_key?(:repo)
52
+ if options[:repo] != defaults[:repo]
53
+ # clone this repo into tmp
54
+ need_clone = true
55
+ else
56
+ if options.has_key?(:branch) && options[:branch] != defaults[:branch]
57
+ # different branch, clone repo
58
+ need_clone = true
59
+ end
60
+ end
61
+ end
62
+
63
+ # remove old templates
64
+ `rm -rf #{ options[:ctx] }/assets/javascripts/templates/rails-angularui-bootstrap`
65
+
66
+ # merge with default options
67
+ options = defaults.merge!(options)
68
+
69
+ gem_js_directory = "#{ options[:ctx] }/assets/javascripts/rails-angularui-bootstrap"
70
+
71
+
72
+ # ensure javascripts dir
73
+ if !File.directory?("#{ gem_js_directory }")
74
+ `mkdir -p #{ gem_js_directory }`
75
+ end
76
+
77
+ # make sure the app has a templates / rails-angularui-bootstrap dir
78
+ if !File.directory?("#{ options[:ctx] }/assets/javascripts/templates/rails-angularui-bootstrap")
79
+ `mkdir -p #{ options[:ctx] }/assets/javascripts/templates/rails-angularui-bootstrap`
80
+ end
81
+
82
+ if need_clone || options[:ctx] == 'vendor'
83
+ puts "Either you're the gem maintainer or you chose a different branch and repo, i'll have to try to clone and build it ...".colorize(:yellow)
84
+
85
+ if File.directory?('tmp/bootstrap')
86
+ # remove the old git if it was there from previous run
87
+ `rm -rf tmp/bootstrap`
88
+
89
+ # remove any old bootstrap files
90
+ `rm -rf #{options[:ctx]}/assets/javascripts/templates/rails-angularui-bootstrap/*`
91
+ end
92
+
93
+ # clone the repo of angular ui
94
+ `git clone -b #{options[:branch]} #{options[:repo]} tmp/bootstrap`
95
+
96
+ # get the node settings / version
97
+ npm_settings = JSON.parse( File.read("tmp/bootstrap/package.json"))
98
+ js_version = npm_settings["version"]
99
+
100
+ Dir.chdir('tmp/bootstrap')
101
+
102
+ # install dependencies, make use of the tmp directory
103
+ `npm install`
104
+
105
+ # build the angular ui repo, even if tests fail.
106
+ puts "Running Grunt...".colorize(:light_blue)
107
+ `grunt --force`
108
+
109
+ # write a version comment to this file so it's easy to know whence this file came
110
+ write_version("dist/ui-bootstrap-#{ js_version }.js",
111
+ ["// generated from #{options[:repo]}:#{options[:branch]} version: #{ js_version } on #{Time.new().strftime("%m-%d-%Y %H:%M:%S")} \n"
112
+ ], 0)
113
+
114
+ # now copy the built version of the js in dist, without the templates
115
+ `cp dist/ui-bootstrap-#{ js_version }.js ../../#{ gem_js_directory }`
116
+
117
+ # remove all the old templates
118
+ `rm -rf #{options[:ctx]}/assets/javascripts/templates/rails-angularui-bootstrap/*`
119
+
120
+ # pull the templates out into our <ctx>/assets/javascripts/templates/rails-angularui-bootstrap dir
121
+ `cp -r template/* ../../#{options[:ctx]}/assets/javascripts/templates/rails-angularui-bootstrap/`
122
+
123
+ # move to the parent directory
124
+ Dir.chdir('../..')
125
+
126
+ # convert templates to haml, suppress warnings
127
+ puts 'Converting to hamlc...'.colorize(:light_blue)
128
+ `for file in #{options[:ctx]}/assets/javascripts/templates/rails-angularui-bootstrap/**/*.html; do html2haml -e $file ${file%html}hamlc 2>&1 && rm $file; done`
129
+
130
+ # clean up
131
+ # remove all js files
132
+ puts 'Removing temp files...'
133
+ `rm #{options[:ctx]}/assets/javascripts/templates/rails-angularui-bootstrap/**/*.js`
134
+ # remove the bootstrap dir
135
+ `rm -rf tmp/bootstrap`
136
+
137
+ end
138
+
139
+ if options[:ctx] == "app"
140
+ if need_clone
141
+ source = File.join(Gem.loaded_specs["rails_angularui_bootstrap"].full_gem_path, "vendor/assets/javascripts/rails-angularui-bootstrap", "rails_angularui_bootstrap.coffee")
142
+ target = File.join(Rails.root, "app/assets/javascripts/rails-angularui-bootstrap", "rails_angularui_bootstrap.coffee")
143
+ `cp #{source} #{target}`
144
+
145
+ # write version into rails_angularui_bootstrap
146
+ write_version("#{options[:ctx]}/assets/javascripts/rails-angularui-bootstrap/rails_angularui_bootstrap.coffee",[
147
+ "# require the ui-bootstrap module, in case the user wants to selectively load \n",
148
+ "#= require rails-angularui-bootstrap/ui-bootstrap-#{ js_version }.js \n"
149
+ ],2)
150
+ else
151
+ puts "Generating files".colorize(:cyan)
152
+ # same repo and branch, no need to clone, just copy from gem
153
+ js_source = File.join(Gem.loaded_specs["rails_angularui_bootstrap"].full_gem_path, "vendor/assets/javascripts/rails-angularui-bootstrap")
154
+ js_target = File.join(Rails.root, "app/assets/javascripts/")
155
+
156
+ `cp -r #{js_source} #{js_target}`
157
+
158
+ # index file not needed
159
+ `rm #{js_target}rails-angularui-bootstrap/index.js`
160
+
161
+ template_source = File.join(Gem.loaded_specs["rails_angularui_bootstrap"].full_gem_path, "vendor/assets/javascripts/templates/rails-angularui-bootstrap")
162
+ template_target = File.join(Rails.root, "app/assets/javascripts/templates")
163
+
164
+ `cp -r #{template_source} #{template_target}`
165
+ end
166
+
167
+ puts "Overrode rails_angularui_bootstrap.coffee from gem by placing it in #{gem_js_directory}".colorize(:yellow)
168
+ end
169
+
170
+
171
+ puts "Templates were generated in #{options[:ctx]}/assets/javascripts/templates/rails-angularui-bootstrap".colorize(:green)
172
+ puts "Options: #{options.inspect}".colorize(:light_blue)
173
+ end
174
+
175
+ desc 'Download and install templates of given branch of angularui for the *client app*'
176
+ task :generate, :repo, :branch do |t, args|
177
+ args.with_defaults(ctx: 'app')
178
+ gen_templates args
179
+ end
180
+ end
@@ -0,0 +1,5 @@
1
+ // First require all the templates
2
+ //= require_tree ../templates/rails-angularui-bootstrap/
3
+
4
+ // Then require the angular template loading mechanism.
5
+ //= require rails-angularui-bootstrap/rails_angularui_bootstrap
@@ -0,0 +1,15 @@
1
+ # require the ui-bootstrap module, in case the user wants to selectively load
2
+ #= require rails-angularui-bootstrap/ui-bootstrap-0.6.0-SNAPSHOT.js
3
+ #= require hamlcoffee
4
+ angular.module('rails-angularui-bootstrap',[]).run(['$templateCache', ($templateCache)->
5
+ JST = window.JST
6
+ if JST
7
+ namespace = "rails_angularui_bootstrap/"
8
+ for own key, template of JST
9
+ if key.indexOf(namespace) is 0
10
+ # this is a rails-angularui-bootstrap template, so cache it as an angular-ui template
11
+ angularUIBootstrapName = key.replace(namespace,"")
12
+ cacheKey = "template/#{angularUIBootstrapName}.html".replace("_","-")
13
+ $templateCache.put(cacheKey, template(null))
14
+ return
15
+ ])
@@ -0,0 +1,3629 @@
1
+ // generated from https://github.com/elerch/bootstrap.git:bootstrap3_bis2 version: 0.6.0-SNAPSHOT on 11-12-2013 21:32:23
2
+ angular.module("ui.bootstrap", ["ui.bootstrap.transition","ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.bindHtml","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.position","ui.bootstrap.datepicker","ui.bootstrap.dropdownToggle","ui.bootstrap.modal","ui.bootstrap.pagination","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.typeahead"]);
3
+ angular.module('ui.bootstrap.transition', [])
4
+
5
+ /**
6
+ * $transition service provides a consistent interface to trigger CSS 3 transitions and to be informed when they complete.
7
+ * @param {DOMElement} element The DOMElement that will be animated.
8
+ * @param {string|object|function} trigger The thing that will cause the transition to start:
9
+ * - As a string, it represents the css class to be added to the element.
10
+ * - As an object, it represents a hash of style attributes to be applied to the element.
11
+ * - As a function, it represents a function to be called that will cause the transition to occur.
12
+ * @return {Promise} A promise that is resolved when the transition finishes.
13
+ */
14
+ .factory('$transition', ['$q', '$timeout', '$rootScope', function($q, $timeout, $rootScope) {
15
+
16
+ var $transition = function(element, trigger, options) {
17
+ options = options || {};
18
+ var deferred = $q.defer();
19
+ var endEventName = $transition[options.animation ? "animationEndEventName" : "transitionEndEventName"];
20
+
21
+ var transitionEndHandler = function(event) {
22
+ $rootScope.$apply(function() {
23
+ element.unbind(endEventName, transitionEndHandler);
24
+ deferred.resolve(element);
25
+ });
26
+ };
27
+
28
+ if (endEventName) {
29
+ element.bind(endEventName, transitionEndHandler);
30
+ }
31
+
32
+ // Wrap in a timeout to allow the browser time to update the DOM before the transition is to occur
33
+ $timeout(function() {
34
+ if ( angular.isString(trigger) ) {
35
+ element.addClass(trigger);
36
+ } else if ( angular.isFunction(trigger) ) {
37
+ trigger(element);
38
+ } else if ( angular.isObject(trigger) ) {
39
+ element.css(trigger);
40
+ }
41
+ //If browser does not support transitions, instantly resolve
42
+ if ( !endEventName ) {
43
+ deferred.resolve(element);
44
+ }
45
+ });
46
+
47
+ // Add our custom cancel function to the promise that is returned
48
+ // We can call this if we are about to run a new transition, which we know will prevent this transition from ending,
49
+ // i.e. it will therefore never raise a transitionEnd event for that transition
50
+ deferred.promise.cancel = function() {
51
+ if ( endEventName ) {
52
+ element.unbind(endEventName, transitionEndHandler);
53
+ }
54
+ deferred.reject('Transition cancelled');
55
+ };
56
+
57
+ // Emulate transitionend event, useful when support is assumed to be
58
+ // available, but may not actually be used due to a transition property
59
+ // not being used in CSS (for example, in versions of firefox prior to 16,
60
+ // only -moz-transition is supported -- and is not used in Bootstrap3's CSS
61
+ // -- As such, no transitionend event would be fired due to no transition
62
+ // ever taking place. This method allows a fallback for such browsers.)
63
+ deferred.promise.emulateTransitionEnd = function(duration) {
64
+ var called = false;
65
+ deferred.promise.then(
66
+ function() { called = true; },
67
+ function() { called = true; }
68
+ );
69
+
70
+ var callback = function() {
71
+ if ( !called ) {
72
+ // If we got here, we probably aren't going to get a real
73
+ // transitionend event. Emit a dummy to the handler.
74
+ element.triggerHandler(endEventName);
75
+ }
76
+ };
77
+
78
+ $timeout(callback, duration);
79
+ return deferred.promise;
80
+ };
81
+
82
+ return deferred.promise;
83
+ };
84
+
85
+ // Work out the name of the transitionEnd event
86
+ var transElement = document.createElement('trans');
87
+ var transitionEndEventNames = {
88
+ 'WebkitTransition': 'webkitTransitionEnd',
89
+ 'MozTransition': 'transitionend',
90
+ 'OTransition': 'oTransitionEnd',
91
+ 'transition': 'transitionend'
92
+ };
93
+ var animationEndEventNames = {
94
+ 'WebkitTransition': 'webkitAnimationEnd',
95
+ 'MozTransition': 'animationend',
96
+ 'OTransition': 'oAnimationEnd',
97
+ 'transition': 'animationend'
98
+ };
99
+ function findEndEventName(endEventNames) {
100
+ for (var name in endEventNames){
101
+ if (transElement.style[name] !== undefined) {
102
+ return endEventNames[name];
103
+ }
104
+ }
105
+ }
106
+ $transition.transitionEndEventName = findEndEventName(transitionEndEventNames);
107
+ $transition.animationEndEventName = findEndEventName(animationEndEventNames);
108
+ return $transition;
109
+ }]);
110
+ angular.module('ui.bootstrap.collapse',['ui.bootstrap.transition'])
111
+
112
+ // The collapsible directive indicates a block of html that will expand and collapse
113
+ .directive('collapse', ['$transition', function($transition) {
114
+ // CSS transitions don't work with height: auto, so we have to manually change the height to a
115
+ // specific value and then once the animation completes, we can reset the height to auto.
116
+ // Unfortunately if you do this while the CSS transitions are specified (i.e. in the CSS class
117
+ // "collapse") then you trigger a change to height 0 in between.
118
+ // The fix is to remove the "collapse" CSS class while changing the height back to auto - phew!
119
+ var fixUpHeight = function(scope, element, height) {
120
+ // We remove the collapse CSS class to prevent a transition when we change to height: auto
121
+ var collapse = element.hasClass('collapse');
122
+ element.removeClass('collapse');
123
+ element.css({ height: height });
124
+ // It appears that reading offsetWidth makes the browser realise that we have changed the
125
+ // height already :-/
126
+ var x = element[0].offsetWidth;
127
+ if(collapse) {
128
+ element.addClass('collapse');
129
+ }
130
+ };
131
+
132
+ return {
133
+ link: function(scope, element, attrs) {
134
+
135
+ var isCollapsed;
136
+ var initialAnimSkip = true;
137
+ scope.$watch(function (){ return element[0].scrollHeight; }, function (value) {
138
+ //The listener is called when scollHeight changes
139
+ //It actually does on 2 scenarios:
140
+ // 1. Parent is set to display none
141
+ // 2. angular bindings inside are resolved
142
+ //When we have a change of scrollHeight we are setting again the correct height if the group is opened
143
+ if (element[0].scrollHeight !== 0) {
144
+ if (!isCollapsed) {
145
+ if (initialAnimSkip) {
146
+ fixUpHeight(scope, element, element[0].scrollHeight + 'px');
147
+ } else {
148
+ fixUpHeight(scope, element, 'auto');
149
+ }
150
+ }
151
+ }
152
+ });
153
+
154
+ scope.$watch(attrs.collapse, function(value) {
155
+ if (value) {
156
+ collapse();
157
+ } else {
158
+ expand();
159
+ }
160
+ });
161
+
162
+ // Some jQuery-like functionality, based on implementation in Prototype.
163
+ //
164
+ // There is a problem with these: We're instantiating them for every
165
+ // instance of the directive, and that's not very good.
166
+ //
167
+ // But we do need a more robust way to calculate dimensions of an item,
168
+ // scrollWidth/scrollHeight is not super reliable, and we can't rely on
169
+ // jQuery or Prototype or any other framework being used.
170
+ var helpers = {
171
+ style: function(element, prop) {
172
+ var elem = element;
173
+ if(typeof elem.length === 'number') {
174
+ elem = elem[0];
175
+ }
176
+ function camelcase(name) {
177
+ return name.replace(/-+(.)?/g, function(match, chr) {
178
+ return chr ? chr.toUpperCase() : '';
179
+ });
180
+ }
181
+ prop = prop === 'float' ? 'cssFloat' : camelcase(prop);
182
+ var value = elem.style[prop];
183
+ if (!value || value === 'auto') {
184
+ var css = window.getComputedStyle(elem, null);
185
+ value = css ? css[prop] : null;
186
+ }
187
+ if (prop === 'opacity') {
188
+ return value ? parseFloat(value) : 1.0;
189
+ }
190
+ return value === 'auto' ? null : value;
191
+ },
192
+
193
+ size: function(element) {
194
+ var dom = element[0];
195
+ var display = helpers.style(element, 'display');
196
+
197
+ if (display && display !== 'none') {
198
+ // Fast case: rely on offset dimensions
199
+ return { width: dom.offsetWidth, height: dom.offsetHeight };
200
+ }
201
+
202
+ // Slow case -- Save original CSS properties, update the CSS, and then
203
+ // use offset dimensions, and restore the original CSS
204
+ var currentStyle = dom.style;
205
+ var originalStyles = {
206
+ visibility: currentStyle.visibility,
207
+ position: currentStyle.position,
208
+ display: currentStyle.display
209
+ };
210
+
211
+ var newStyles = {
212
+ visibility: 'hidden',
213
+ display: 'block'
214
+ };
215
+
216
+ // Switching `fixed` to `absolute` causes issues in Safari.
217
+ if (originalStyles.position !== 'fixed') {
218
+ newStyles.position = 'absolute';
219
+ }
220
+
221
+ // Quickly swap-in styles which would allow us to utilize offset
222
+ // dimensions
223
+ element.css(newStyles);
224
+
225
+ var dimensions = {
226
+ width: dom.offsetWidth,
227
+ height: dom.offsetHeight
228
+ };
229
+
230
+ // And restore the original styles
231
+ element.css(originalStyles);
232
+
233
+ return dimensions;
234
+ },
235
+
236
+ width: function(element, value) {
237
+ if(typeof value === 'number' || typeof value === 'string') {
238
+ if(typeof value === 'number') {
239
+ value = value + 'px';
240
+ }
241
+ element.css({ 'width': value });
242
+ return;
243
+ }
244
+ return helpers.size(element).width;
245
+ },
246
+
247
+ height: function(element, value) {
248
+ if(typeof value === 'number' || typeof value === 'string') {
249
+ if(typeof value === 'number') {
250
+ value = value + 'px';
251
+ }
252
+ element.css({ 'height': value });
253
+ return;
254
+ }
255
+ return helpers.size(element).height;
256
+ },
257
+
258
+ dimension: function() {
259
+ var hasWidth = element.hasClass('width');
260
+ return hasWidth ? 'width' : 'height';
261
+ }
262
+ };
263
+
264
+ var events = {
265
+ beforeShow: function(dimension, dimensions) {
266
+ element
267
+ .removeClass('collapse')
268
+ .removeClass('collapsed')
269
+ .addClass('collapsing');
270
+ helpers[dimension](element, 0);
271
+ },
272
+
273
+ beforeHide: function(dimension, dimensions) {
274
+ // Read offsetHeight and reset height:
275
+ helpers[dimension](element, dimensions[dimension] + "px");
276
+ var unused = element[0].offsetWidth,
277
+ unused2 = element[0].offsetHeight;
278
+ element
279
+ .addClass('collapsing')
280
+ .removeClass('collapse')
281
+ .removeClass('in');
282
+ },
283
+
284
+ afterShow: function(dimension) {
285
+ element
286
+ .removeClass('collapsing')
287
+ .addClass('in');
288
+ helpers[dimension](element, 'auto');
289
+ isCollapsed = false;
290
+ },
291
+
292
+ afterHide: function(dimension) {
293
+ element
294
+ .removeClass('collapsing')
295
+ .addClass('collapsed')
296
+ .addClass('collapse');
297
+ isCollapsed = true;
298
+ }
299
+ };
300
+
301
+ var currentTransition;
302
+ var doTransition = function(showing, pixels) {
303
+ if (currentTransition || showing === element.hasClass('in')) {
304
+ return;
305
+ }
306
+
307
+ var dimension = helpers.dimension();
308
+ var dimensions = helpers.size(element);
309
+ var name = showing ? 'Show' : 'Hide';
310
+
311
+ events['before' + name](dimension, dimensions);
312
+
313
+ var query = {};
314
+ var makeUpper = function(name) {
315
+ return name.charAt(0).toUpperCase() + name.slice(1);
316
+ };
317
+ if(pixels==='scroll') {
318
+ pixels = element[0][pixels + makeUpper(dimension)];
319
+ }
320
+ if(typeof pixels === 'number') {
321
+ pixels = pixels + "px";
322
+ }
323
+ query[dimension] = pixels;
324
+ currentTransition = $transition(element,query);
325
+ currentTransition.then(
326
+ function() {
327
+ events['after' + name](dimension);
328
+ currentTransition = undefined;
329
+ },
330
+ function(reason) {
331
+ var descr = showing ? 'expansion' : 'collapse';
332
+ currentTransition = undefined;
333
+ }
334
+ );
335
+ return currentTransition;
336
+ };
337
+
338
+ var expand = function() {
339
+ if (initialAnimSkip || !$transition.transitionEndEventName) {
340
+ initialAnimSkip = false;
341
+ var dimension = helpers.dimension();
342
+ helpers[dimension](element, 'auto');
343
+ events.afterShow(dimension);
344
+ } else {
345
+ doTransition(true, 'scroll');
346
+ }
347
+ };
348
+
349
+ var collapse = function() {
350
+ if (initialAnimSkip || !$transition.transitionEndEventName) {
351
+ initialAnimSkip = false;
352
+ var dimension = helpers.dimension();
353
+ helpers[dimension](element, 0);
354
+ events.afterHide(dimension);
355
+ } else {
356
+ doTransition(false, '0');
357
+ }
358
+ };
359
+ }
360
+ };
361
+ }]);
362
+
363
+ angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse'])
364
+
365
+ .constant('accordionConfig', {
366
+ closeOthers: true,
367
+ templateUrl: 'template/accordion/accordion.html'
368
+ })
369
+
370
+ .controller('AccordionController', ['$scope', '$attrs', 'accordionConfig', function ($scope, $attrs, accordionConfig) {
371
+
372
+ // This array keeps track of the accordion groups
373
+ this.groups = [];
374
+
375
+ // Ensure that all the groups in this accordion are closed, unless close-others explicitly says not to
376
+ this.closeOthers = function(openGroup) {
377
+ var closeOthers = angular.isDefined($attrs.closeOthers) ? $scope.$eval($attrs.closeOthers) : accordionConfig.closeOthers;
378
+ if ( closeOthers ) {
379
+ angular.forEach(this.groups, function (group) {
380
+ if ( group !== openGroup ) {
381
+ group.isOpen = false;
382
+ }
383
+ });
384
+ }
385
+ };
386
+
387
+ // This is called from the accordion-group directive to add itself to the accordion
388
+ this.addGroup = function(groupScope) {
389
+ var that = this;
390
+ this.groups.push(groupScope);
391
+
392
+ groupScope.$on('$destroy', function (event) {
393
+ that.removeGroup(groupScope);
394
+ });
395
+ };
396
+
397
+ // This is called from the accordion-group directive when to remove itself
398
+ this.removeGroup = function(group) {
399
+ var index = this.groups.indexOf(group);
400
+ if ( index !== -1 ) {
401
+ this.groups.splice(this.groups.indexOf(group), 1);
402
+ }
403
+ };
404
+
405
+ }])
406
+
407
+ // The accordion directive simply sets up the directive controller
408
+ // and adds an accordion CSS class to itself element.
409
+ .directive('accordion', function (accordionConfig) {
410
+ return {
411
+ restrict:'EA',
412
+ controller:'AccordionController',
413
+ transclude: true,
414
+ replace: false,
415
+ templateUrl: accordionConfig.templateUrl
416
+ };
417
+ })
418
+
419
+ .constant('accordionGroupConfig', {
420
+ templateUrl: 'template/accordion/accordion-group.html'
421
+ })
422
+
423
+ // The accordion-group directive indicates a block of html that will expand and collapse in an accordion
424
+ .directive('accordionGroup', ['$parse', '$transition', '$timeout', 'accordionGroupConfig', function($parse, $transition, $timeout, accordionGroupConfig) {
425
+ return {
426
+ require:'^accordion', // We need this directive to be inside an accordion
427
+ restrict:'EA',
428
+ transclude:true, // It transcludes the contents of the directive into the template
429
+ replace: true, // The element containing the directive will be replaced with the template
430
+ templateUrl: accordionGroupConfig.templateUrl,
431
+ scope:{ heading:'@' }, // Create an isolated scope and interpolate the heading attribute onto this scope
432
+ controller: ['$scope', function($scope) {
433
+ this.setHeading = function(element) {
434
+ this.heading = element;
435
+ };
436
+ }],
437
+ link: function(scope, element, attrs, accordionCtrl) {
438
+ var getIsOpen, setIsOpen;
439
+
440
+ accordionCtrl.addGroup(scope);
441
+
442
+ scope.isOpen = false;
443
+
444
+ if ( attrs.isOpen ) {
445
+ getIsOpen = $parse(attrs.isOpen);
446
+ setIsOpen = getIsOpen.assign;
447
+
448
+ scope.$watch(
449
+ function watchIsOpen() { return getIsOpen(scope.$parent); },
450
+ function updateOpen(value) { scope.isOpen = value; }
451
+ );
452
+
453
+ scope.isOpen = getIsOpen ? getIsOpen(scope.$parent) : false;
454
+ }
455
+
456
+ scope.$watch('isOpen', function(value) {
457
+ if ( value ) {
458
+ accordionCtrl.closeOthers(scope);
459
+ }
460
+ if ( setIsOpen ) {
461
+ setIsOpen(scope.$parent, value);
462
+ }
463
+ });
464
+ }
465
+ };
466
+ }])
467
+
468
+ // Use accordion-heading below an accordion-group to provide a heading containing HTML
469
+ // <accordion-group>
470
+ // <accordion-heading>Heading containing HTML - <img src="..."></accordion-heading>
471
+ // </accordion-group>
472
+ .directive('accordionHeading', function() {
473
+ return {
474
+ restrict: 'EA',
475
+ transclude: true, // Grab the contents to be used as the heading
476
+ template: '', // In effect remove this element!
477
+ replace: true,
478
+ require: '^accordionGroup',
479
+ compile: function(element, attr, transclude) {
480
+ return function link(scope, element, attr, accordionGroupCtrl) {
481
+ // Pass the heading to the accordion-group controller
482
+ // so that it can be transcluded into the right place in the template
483
+ // [The second parameter to transclude causes the elements to be cloned so that they work in ng-repeat]
484
+ accordionGroupCtrl.setHeading(transclude(scope, function() {}));
485
+ };
486
+ }
487
+ };
488
+ })
489
+
490
+ // Use in the accordion-group template to indicate where you want the heading to be transcluded
491
+ // You must provide the property on the accordion-group controller that will hold the transcluded element
492
+ // <div class="accordion-group">
493
+ // <div class="accordion-heading" ><a ... accordion-transclude="heading">...</a></div>
494
+ // ...
495
+ // </div>
496
+ .directive('accordionTransclude', function() {
497
+ return {
498
+ require: '^accordionGroup',
499
+ link: function(scope, element, attr, controller) {
500
+ scope.$watch(function() { return controller[attr.accordionTransclude]; }, function(heading) {
501
+ if ( heading ) {
502
+ element.html('');
503
+ element.append(heading);
504
+ }
505
+ });
506
+ }
507
+ };
508
+ });
509
+
510
+ angular.module("ui.bootstrap.alert", [])
511
+
512
+ .constant('alertConfig', {
513
+ templateUrl: 'template/alert/alert.html'
514
+ })
515
+
516
+ .directive('alert', ['alertConfig', function (alertConfig) {
517
+ return {
518
+ restrict:'EA',
519
+ templateUrl: alertConfig.templateUrl,
520
+ transclude:true,
521
+ replace:true,
522
+ scope: {
523
+ type: '=',
524
+ close: '&'
525
+ },
526
+ link: function(scope, iElement, iAttrs, controller) {
527
+ scope.closeable = "close" in iAttrs;
528
+ }
529
+ };
530
+ }]);
531
+
532
+ angular.module('ui.bootstrap.bindHtml', [])
533
+
534
+ .directive('bindHtmlUnsafe', function () {
535
+ return function (scope, element, attr) {
536
+ element.addClass('ng-binding').data('$binding', attr.bindHtmlUnsafe);
537
+ scope.$watch(attr.bindHtmlUnsafe, function bindHtmlUnsafeWatchAction(value) {
538
+ element.html(value || '');
539
+ });
540
+ };
541
+ });
542
+ angular.module('ui.bootstrap.buttons', [])
543
+
544
+ .constant('buttonConfig', {
545
+ activeClass:'active',
546
+ toggleEvent:'click'
547
+ })
548
+
549
+ .directive('btnRadio', ['buttonConfig', function (buttonConfig) {
550
+ var activeClass = buttonConfig.activeClass || 'active';
551
+ var toggleEvent = buttonConfig.toggleEvent || 'click';
552
+
553
+ return {
554
+
555
+ require:'ngModel',
556
+ link:function (scope, element, attrs, ngModelCtrl) {
557
+
558
+ //model -> UI
559
+ ngModelCtrl.$render = function () {
560
+ element.toggleClass(activeClass, angular.equals(ngModelCtrl.$modelValue, scope.$eval(attrs.btnRadio)));
561
+ };
562
+
563
+ //ui->model
564
+ element.bind(toggleEvent, function () {
565
+ if (!element.hasClass(activeClass)) {
566
+ scope.$apply(function () {
567
+ ngModelCtrl.$setViewValue(scope.$eval(attrs.btnRadio));
568
+ ngModelCtrl.$render();
569
+ });
570
+ }
571
+ });
572
+ }
573
+ };
574
+ }])
575
+
576
+ .directive('btnCheckbox', ['buttonConfig', function (buttonConfig) {
577
+
578
+ var activeClass = buttonConfig.activeClass || 'active';
579
+ var toggleEvent = buttonConfig.toggleEvent || 'click';
580
+
581
+ return {
582
+ require:'ngModel',
583
+ link:function (scope, element, attrs, ngModelCtrl) {
584
+
585
+ function getTrueValue() {
586
+ var trueValue = scope.$eval(attrs.btnCheckboxTrue);
587
+ return angular.isDefined(trueValue) ? trueValue : true;
588
+ }
589
+
590
+ function getFalseValue() {
591
+ var falseValue = scope.$eval(attrs.btnCheckboxFalse);
592
+ return angular.isDefined(falseValue) ? falseValue : false;
593
+ }
594
+
595
+ //model -> UI
596
+ ngModelCtrl.$render = function () {
597
+ element.toggleClass(activeClass, angular.equals(ngModelCtrl.$modelValue, getTrueValue()));
598
+ };
599
+
600
+ //ui->model
601
+ element.bind(toggleEvent, function () {
602
+ scope.$apply(function () {
603
+ ngModelCtrl.$setViewValue(element.hasClass(activeClass) ? getFalseValue() : getTrueValue());
604
+ ngModelCtrl.$render();
605
+ });
606
+ });
607
+ }
608
+ };
609
+ }]);
610
+ /**
611
+ * @ngdoc overview
612
+ * @name ui.bootstrap.carousel
613
+ *
614
+ * @description
615
+ * AngularJS version of an image carousel.
616
+ *
617
+ */
618
+ angular.module('ui.bootstrap.carousel', ['ui.bootstrap.transition'])
619
+ .controller('CarouselController', ['$scope', '$timeout', '$transition', '$q', function ($scope, $timeout, $transition, $q) {
620
+ var self = this,
621
+ slides = self.slides = [],
622
+ currentIndex = -1,
623
+ currentTimeout, isPlaying;
624
+ self.currentSlide = null;
625
+
626
+ /* direction: "prev" or "next" */
627
+ self.select = function(nextSlide, direction) {
628
+ var nextIndex = slides.indexOf(nextSlide);
629
+ //Decide direction if it's not given
630
+ if (direction === undefined) {
631
+ direction = nextIndex > currentIndex ? "next" : "prev";
632
+ }
633
+ if (nextSlide && nextSlide !== self.currentSlide) {
634
+ if ($scope.$currentTransition) {
635
+ $scope.$currentTransition.cancel();
636
+ //Timeout so ng-class in template has time to fix classes for finished slide
637
+ $timeout(goNext);
638
+ } else {
639
+ goNext();
640
+ }
641
+ }
642
+ function goNext() {
643
+ //If we have a slide to transition from and we have a transition type and we're allowed, go
644
+ if (self.currentSlide && angular.isString(direction) && !$scope.noTransition && nextSlide.$element) {
645
+ //We shouldn't do class manip in here, but it's the same weird thing bootstrap does. need to fix sometime
646
+ nextSlide.$element.addClass(direction);
647
+ var reflow = nextSlide.$element[0].offsetWidth; //force reflow
648
+
649
+ //Set all other slides to stop doing their stuff for the new transition
650
+ angular.forEach(slides, function(slide) {
651
+ angular.extend(slide, {direction: '', entering: false, leaving: false, active: false});
652
+ });
653
+ angular.extend(nextSlide, {direction: direction, active: true, entering: true});
654
+ angular.extend(self.currentSlide||{}, {direction: direction, leaving: true});
655
+
656
+ $scope.$currentTransition = $transition(nextSlide.$element, {});
657
+ //We have to create new pointers inside a closure since next & current will change
658
+ (function(next,current) {
659
+ $scope.$currentTransition.then(
660
+ function(){ transitionDone(next, current); },
661
+ function(){ transitionDone(next, current); }
662
+ );
663
+ }(nextSlide, self.currentSlide));
664
+ } else {
665
+ transitionDone(nextSlide, self.currentSlide);
666
+ }
667
+ self.currentSlide = nextSlide;
668
+ currentIndex = nextIndex;
669
+ //every time you change slides, reset the timer
670
+ restartTimer();
671
+ }
672
+ function transitionDone(next, current) {
673
+ angular.extend(next, {direction: '', active: true, leaving: false, entering: false});
674
+ angular.extend(current||{}, {direction: '', active: false, leaving: false, entering: false});
675
+ $scope.$currentTransition = null;
676
+ }
677
+ };
678
+
679
+ /* Allow outside people to call indexOf on slides array */
680
+ self.indexOfSlide = function(slide) {
681
+ return slides.indexOf(slide);
682
+ };
683
+
684
+ $scope.next = function() {
685
+ var newIndex = (currentIndex + 1) % slides.length;
686
+
687
+ //Prevent this user-triggered transition from occurring if there is already one in progress
688
+ if (!$scope.$currentTransition) {
689
+ return self.select(slides[newIndex], 'next');
690
+ }
691
+ };
692
+
693
+ $scope.prev = function() {
694
+ var newIndex = currentIndex - 1 < 0 ? slides.length - 1 : currentIndex - 1;
695
+
696
+ //Prevent this user-triggered transition from occurring if there is already one in progress
697
+ if (!$scope.$currentTransition) {
698
+ return self.select(slides[newIndex], 'prev');
699
+ }
700
+ };
701
+
702
+ $scope.select = function(slide) {
703
+ self.select(slide);
704
+ };
705
+
706
+ $scope.isActive = function(slide) {
707
+ return self.currentSlide === slide;
708
+ };
709
+
710
+ $scope.slides = function() {
711
+ return slides;
712
+ };
713
+
714
+ $scope.$watch('interval', restartTimer);
715
+ function restartTimer() {
716
+ if (currentTimeout) {
717
+ $timeout.cancel(currentTimeout);
718
+ }
719
+ function go() {
720
+ if (isPlaying) {
721
+ $scope.next();
722
+ restartTimer();
723
+ } else {
724
+ $scope.pause();
725
+ }
726
+ }
727
+ var interval = +$scope.interval;
728
+ if (!isNaN(interval) && interval>=0) {
729
+ currentTimeout = $timeout(go, interval);
730
+ }
731
+ }
732
+ $scope.play = function() {
733
+ if (!isPlaying) {
734
+ isPlaying = true;
735
+ restartTimer();
736
+ }
737
+ };
738
+ $scope.pause = function() {
739
+ if (!$scope.noPause) {
740
+ isPlaying = false;
741
+ if (currentTimeout) {
742
+ $timeout.cancel(currentTimeout);
743
+ }
744
+ }
745
+ };
746
+
747
+ self.addSlide = function(slide, element) {
748
+ slide.$element = element;
749
+ slides.push(slide);
750
+ //if this is the first slide or the slide is set to active, select it
751
+ if(slides.length === 1 || slide.active) {
752
+ self.select(slides[slides.length-1]);
753
+ if (slides.length == 1) {
754
+ $scope.play();
755
+ }
756
+ } else {
757
+ slide.active = false;
758
+ }
759
+ };
760
+
761
+ self.removeSlide = function(slide) {
762
+ //get the index of the slide inside the carousel
763
+ var index = slides.indexOf(slide);
764
+ slides.splice(index, 1);
765
+ if (slides.length > 0 && slide.active) {
766
+ if (index >= slides.length) {
767
+ self.select(slides[index-1]);
768
+ } else {
769
+ self.select(slides[index]);
770
+ }
771
+ } else if (currentIndex > index) {
772
+ currentIndex--;
773
+ }
774
+ };
775
+ }])
776
+
777
+ .constant('carouselConfig', {
778
+ templateUrl: 'template/carousel/carousel.html'
779
+ })
780
+
781
+ /**
782
+ * @ngdoc directive
783
+ * @name ui.bootstrap.carousel.directive:carousel
784
+ * @restrict EA
785
+ *
786
+ * @description
787
+ * Carousel is the outer container for a set of image 'slides' to showcase.
788
+ *
789
+ * @param {number=} interval The time, in milliseconds, that it will take the carousel to go to the next slide.
790
+ * @param {boolean=} noTransition Whether to disable transitions on the carousel.
791
+ * @param {boolean=} noPause Whether to disable pausing on the carousel (by default, the carousel interval pauses on hover).
792
+ *
793
+ * @example
794
+ <example module="ui.bootstrap">
795
+ <file name="index.html">
796
+ <carousel>
797
+ <slide>
798
+ <img src="http://placekitten.com/150/150" style="margin:auto;">
799
+ <div class="carousel-caption">
800
+ <p>Beautiful!</p>
801
+ </div>
802
+ </slide>
803
+ <slide>
804
+ <img src="http://placekitten.com/100/150" style="margin:auto;">
805
+ <div class="carousel-caption">
806
+ <p>D'aww!</p>
807
+ </div>
808
+ </slide>
809
+ </carousel>
810
+ </file>
811
+ <file name="demo.css">
812
+ .carousel-indicators {
813
+ top: auto;
814
+ bottom: 15px;
815
+ }
816
+ </file>
817
+ </example>
818
+ */
819
+ .directive('carousel', ['carouselConfig', function(carouselConfig) {
820
+ return {
821
+ restrict: 'EA',
822
+ transclude: true,
823
+ replace: true,
824
+ controller: 'CarouselController',
825
+ require: 'carousel',
826
+ templateUrl: carouselConfig.templateUrl,
827
+ scope: {
828
+ interval: '=',
829
+ noTransition: '=',
830
+ noPause: '='
831
+ }
832
+ };
833
+ }])
834
+
835
+ .constant('carouselSlideConfig', {
836
+ templateUrl: 'template/carousel/slide.html'
837
+ })
838
+
839
+ /**
840
+ * @ngdoc directive
841
+ * @name ui.bootstrap.carousel.directive:slide
842
+ * @restrict EA
843
+ *
844
+ * @description
845
+ * Creates a slide inside a {@link ui.bootstrap.carousel.directive:carousel carousel}. Must be placed as a child of a carousel element.
846
+ *
847
+ * @param {boolean=} active Model binding, whether or not this slide is currently active.
848
+ *
849
+ * @example
850
+ <example module="ui.bootstrap">
851
+ <file name="index.html">
852
+ <div ng-controller="CarouselDemoCtrl">
853
+ <carousel>
854
+ <slide ng-repeat="slide in slides" active="slide.active">
855
+ <img ng-src="{{slide.image}}" style="margin:auto;">
856
+ <div class="carousel-caption">
857
+ <h4>Slide {{$index}}</h4>
858
+ <p>{{slide.text}}</p>
859
+ </div>
860
+ </slide>
861
+ </carousel>
862
+ <div class="row-fluid">
863
+ <div class="span6">
864
+ <ul>
865
+ <li ng-repeat="slide in slides">
866
+ <button class="btn btn-mini" ng-class="{'btn-info': !slide.active, 'btn-success': slide.active}" ng-disabled="slide.active" ng-click="slide.active = true">select</button>
867
+ {{$index}}: {{slide.text}}
868
+ </li>
869
+ </ul>
870
+ <a class="btn" ng-click="addSlide()">Add Slide</a>
871
+ </div>
872
+ <div class="span6">
873
+ Interval, in milliseconds: <input type="number" ng-model="myInterval">
874
+ <br />Enter a negative number to stop the interval.
875
+ </div>
876
+ </div>
877
+ </div>
878
+ </file>
879
+ <file name="script.js">
880
+ function CarouselDemoCtrl($scope) {
881
+ $scope.myInterval = 5000;
882
+ var slides = $scope.slides = [];
883
+ $scope.addSlide = function() {
884
+ var newWidth = 200 + ((slides.length + (25 * slides.length)) % 150);
885
+ slides.push({
886
+ image: 'http://placekitten.com/' + newWidth + '/200',
887
+ text: ['More','Extra','Lots of','Surplus'][slides.length % 4] + ' '
888
+ ['Cats', 'Kittys', 'Felines', 'Cutes'][slides.length % 4]
889
+ });
890
+ };
891
+ for (var i=0; i<4; i++) $scope.addSlide();
892
+ }
893
+ </file>
894
+ <file name="demo.css">
895
+ .carousel-indicators {
896
+ top: auto;
897
+ bottom: 15px;
898
+ }
899
+ </file>
900
+ </example>
901
+ */
902
+
903
+ .directive('slide', ['$parse', 'carouselSlideConfig', function($parse, carouselSlideConfig) {
904
+ return {
905
+ require: '^carousel',
906
+ restrict: 'EA',
907
+ transclude: true,
908
+ replace: true,
909
+ templateUrl: carouselSlideConfig.templateUrl,
910
+ scope: {
911
+ },
912
+ link: function (scope, element, attrs, carouselCtrl) {
913
+ //Set up optional 'active' = binding
914
+ if (attrs.active) {
915
+ var getActive = $parse(attrs.active);
916
+ var setActive = getActive.assign;
917
+ var lastValue = scope.active = getActive(scope.$parent);
918
+ scope.$watch(function parentActiveWatch() {
919
+ var parentActive = getActive(scope.$parent);
920
+
921
+ if (parentActive !== scope.active) {
922
+ // we are out of sync and need to copy
923
+ if (parentActive !== lastValue) {
924
+ // parent changed and it has precedence
925
+ lastValue = scope.active = parentActive;
926
+ } else {
927
+ // if the parent can be assigned then do so
928
+ setActive(scope.$parent, parentActive = lastValue = scope.active);
929
+ }
930
+ }
931
+ return parentActive;
932
+ });
933
+ }
934
+
935
+ carouselCtrl.addSlide(scope, element);
936
+ //when the scope is destroyed then remove the slide from the current slides array
937
+ scope.$on('$destroy', function() {
938
+ carouselCtrl.removeSlide(scope);
939
+ });
940
+
941
+ scope.$watch('active', function(active) {
942
+ if (active) {
943
+ carouselCtrl.select(scope);
944
+ }
945
+ });
946
+ }
947
+ };
948
+ }]);
949
+
950
+ angular.module('ui.bootstrap.position', [])
951
+
952
+ /**
953
+ * A set of utility methods that can be use to retrieve position of DOM elements.
954
+ * It is meant to be used where we need to absolute-position DOM elements in
955
+ * relation to other, existing elements (this is the case for tooltips, popovers,
956
+ * typeahead suggestions etc.).
957
+ */
958
+ .factory('$position', ['$document', '$window', function ($document, $window) {
959
+
960
+ function getStyle(el, cssprop) {
961
+ if (el.currentStyle) { //IE
962
+ return el.currentStyle[cssprop];
963
+ } else if ($window.getComputedStyle) {
964
+ return $window.getComputedStyle(el)[cssprop];
965
+ }
966
+ // finally try and get inline style
967
+ return el.style[cssprop];
968
+ }
969
+
970
+ /**
971
+ * Checks if a given element is statically positioned
972
+ * @param element - raw DOM element
973
+ */
974
+ function isStaticPositioned(element) {
975
+ return (getStyle(element, "position") || 'static' ) === 'static';
976
+ }
977
+
978
+ /**
979
+ * returns the closest, non-statically positioned parentOffset of a given element
980
+ * @param element
981
+ */
982
+ var parentOffsetEl = function (element) {
983
+ var docDomEl = $document[0];
984
+ var offsetParent = element.offsetParent || docDomEl;
985
+ while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) {
986
+ offsetParent = offsetParent.offsetParent;
987
+ }
988
+ return offsetParent || docDomEl;
989
+ };
990
+
991
+ /**
992
+ * Temporarily swap CSS properties in, and perform an operation. Based on jQuery's
993
+ * internal 'jQuery.swap' routine.
994
+ *
995
+ * @param element
996
+ * @param css object containing css property keys and values to swap in
997
+ * @callback operation to perform while the CSS properties are swapped in
998
+ */
999
+ var swapCss = function (element, css, callback) {
1000
+ var ret, prop, old = {};
1001
+ element = angular.element(element);
1002
+ args = Array.prototype.slice.call(arguments, 3);
1003
+
1004
+ for (prop in css) {
1005
+ old[prop] = element[0].style[prop];
1006
+ element[0].style[prop] = css[prop];
1007
+ }
1008
+
1009
+ ret = callback.apply(element, args);
1010
+
1011
+ for (prop in css) {
1012
+ element[0].style[prop] = old[prop];
1013
+ }
1014
+
1015
+ return ret;
1016
+ };
1017
+
1018
+ var swapDisplay = /^(none|table(?!-c[ea]).+)/;
1019
+ var cssShow = {
1020
+ position: 'absolute',
1021
+ visibility: 'hidden',
1022
+ display: 'block'
1023
+ };
1024
+
1025
+ /**
1026
+ * Return offsetWidth or offsetHeight of an element.
1027
+ */
1028
+ var widthOrHeight = function(element, name) {
1029
+ if (typeof element === 'string') {
1030
+ name = element;
1031
+ element = this;
1032
+ }
1033
+ return element[0][('offset' + name.charAt(0).toUpperCase() + name.substr(1))];
1034
+ };
1035
+
1036
+ var service = {
1037
+ /**
1038
+ * Provides read-only equivalent of jQuery's position function:
1039
+ * http://api.jquery.com/position/
1040
+ */
1041
+ position: function (element) {
1042
+ var elBCR = this.offset(element);
1043
+ var offsetParentBCR = { top: 0, left: 0 };
1044
+ var offsetParentEl = parentOffsetEl(element[0]);
1045
+ if (offsetParentEl != $document[0]) {
1046
+ offsetParentBCR = this.offset(angular.element(offsetParentEl));
1047
+ offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop;
1048
+ offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft;
1049
+ }
1050
+
1051
+ return {
1052
+ width: element.prop('offsetWidth'),
1053
+ height: element.prop('offsetHeight'),
1054
+ top: elBCR.top - offsetParentBCR.top,
1055
+ left: elBCR.left - offsetParentBCR.left
1056
+ };
1057
+ },
1058
+
1059
+ /**
1060
+ * Provides read-only equivalent of jQuery's offset function:
1061
+ * http://api.jquery.com/offset/
1062
+ */
1063
+ offset: function (element) {
1064
+ var boundingClientRect = element[0].getBoundingClientRect();
1065
+ return {
1066
+ width: element.prop('offsetWidth'),
1067
+ height: element.prop('offsetHeight'),
1068
+ top: boundingClientRect.top + ($window.pageYOffset || $document[0].body.scrollTop || $document[0].documentElement.scrollTop),
1069
+ left: boundingClientRect.left + ($window.pageXOffset || $document[0].body.scrollLeft || $document[0].documentElement.scrollLeft)
1070
+ };
1071
+ }
1072
+ };
1073
+
1074
+ angular.forEach(['width', 'height'], function(name) {
1075
+ service[name] = function(element, value) {
1076
+ element = angular.element(element);
1077
+ if (arguments.length > 1) {
1078
+ return element.css(name, value);
1079
+ }
1080
+ if (element[0].offsetWidth === 0 && swapDisplay.test(getStyle(element[0], 'display'))) {
1081
+ return swapCss(element, cssShow, widthOrHeight, name);
1082
+ }
1083
+ return widthOrHeight(element, name);
1084
+ };
1085
+ });
1086
+
1087
+ return service;
1088
+ }]);
1089
+
1090
+ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.position'])
1091
+
1092
+ .constant('datepickerConfig', {
1093
+ dayFormat: 'dd',
1094
+ monthFormat: 'MMMM',
1095
+ yearFormat: 'yyyy',
1096
+ dayHeaderFormat: 'EEE',
1097
+ dayTitleFormat: 'MMMM yyyy',
1098
+ monthTitleFormat: 'yyyy',
1099
+ showWeeks: true,
1100
+ startingDay: 0,
1101
+ yearRange: 20,
1102
+ minDate: null,
1103
+ maxDate: null
1104
+ })
1105
+
1106
+ .controller('DatepickerController', ['$scope', '$attrs', 'dateFilter', 'datepickerConfig', function($scope, $attrs, dateFilter, dtConfig) {
1107
+ var format = {
1108
+ day: getValue($attrs.dayFormat, dtConfig.dayFormat),
1109
+ month: getValue($attrs.monthFormat, dtConfig.monthFormat),
1110
+ year: getValue($attrs.yearFormat, dtConfig.yearFormat),
1111
+ dayHeader: getValue($attrs.dayHeaderFormat, dtConfig.dayHeaderFormat),
1112
+ dayTitle: getValue($attrs.dayTitleFormat, dtConfig.dayTitleFormat),
1113
+ monthTitle: getValue($attrs.monthTitleFormat, dtConfig.monthTitleFormat)
1114
+ },
1115
+ startingDay = getValue($attrs.startingDay, dtConfig.startingDay),
1116
+ yearRange = getValue($attrs.yearRange, dtConfig.yearRange);
1117
+
1118
+ this.minDate = dtConfig.minDate ? new Date(dtConfig.minDate) : null;
1119
+ this.maxDate = dtConfig.maxDate ? new Date(dtConfig.maxDate) : null;
1120
+
1121
+ function getValue(value, defaultValue) {
1122
+ return angular.isDefined(value) ? $scope.$parent.$eval(value) : defaultValue;
1123
+ }
1124
+
1125
+ function getDaysInMonth( year, month ) {
1126
+ return new Date(year, month, 0).getDate();
1127
+ }
1128
+
1129
+ function getDates(startDate, n) {
1130
+ var dates = new Array(n);
1131
+ var current = startDate, i = 0;
1132
+ while (i < n) {
1133
+ dates[i++] = new Date(current);
1134
+ current.setDate( current.getDate() + 1 );
1135
+ }
1136
+ return dates;
1137
+ }
1138
+
1139
+ function makeDate(date, format, isSelected, isSecondary) {
1140
+ return { date: date, label: dateFilter(date, format), selected: !!isSelected, secondary: !!isSecondary };
1141
+ }
1142
+
1143
+ this.modes = [
1144
+ {
1145
+ name: 'day',
1146
+ getVisibleDates: function(date, selected) {
1147
+ var year = date.getFullYear(), month = date.getMonth(), firstDayOfMonth = new Date(year, month, 1);
1148
+ var difference = startingDay - firstDayOfMonth.getDay(),
1149
+ numDisplayedFromPreviousMonth = (difference > 0) ? 7 - difference : - difference,
1150
+ firstDate = new Date(firstDayOfMonth), numDates = 0;
1151
+
1152
+ if ( numDisplayedFromPreviousMonth > 0 ) {
1153
+ firstDate.setDate( - numDisplayedFromPreviousMonth + 1 );
1154
+ numDates += numDisplayedFromPreviousMonth; // Previous
1155
+ }
1156
+ numDates += getDaysInMonth(year, month + 1); // Current
1157
+ numDates += (7 - numDates % 7) % 7; // Next
1158
+
1159
+ var days = getDates(firstDate, numDates), labels = new Array(7);
1160
+ for (var i = 0; i < numDates; i ++) {
1161
+ var dt = new Date(days[i]);
1162
+ days[i] = makeDate(dt, format.day, (selected && selected.getDate() === dt.getDate() && selected.getMonth() === dt.getMonth() && selected.getFullYear() === dt.getFullYear()), dt.getMonth() !== month);
1163
+ }
1164
+ for (var j = 0; j < 7; j++) {
1165
+ labels[j] = dateFilter(days[j].date, format.dayHeader);
1166
+ }
1167
+ return { objects: days, title: dateFilter(date, format.dayTitle), labels: labels };
1168
+ },
1169
+ compare: function(date1, date2) {
1170
+ return (new Date( date1.getFullYear(), date1.getMonth(), date1.getDate() ) - new Date( date2.getFullYear(), date2.getMonth(), date2.getDate() ) );
1171
+ },
1172
+ split: 7,
1173
+ step: { months: 1 }
1174
+ },
1175
+ {
1176
+ name: 'month',
1177
+ getVisibleDates: function(date, selected) {
1178
+ var months = new Array(12), year = date.getFullYear();
1179
+ for ( var i = 0; i < 12; i++ ) {
1180
+ var dt = new Date(year, i, 1);
1181
+ months[i] = makeDate(dt, format.month, (selected && selected.getMonth() === i && selected.getFullYear() === year));
1182
+ }
1183
+ return { objects: months, title: dateFilter(date, format.monthTitle) };
1184
+ },
1185
+ compare: function(date1, date2) {
1186
+ return new Date( date1.getFullYear(), date1.getMonth() ) - new Date( date2.getFullYear(), date2.getMonth() );
1187
+ },
1188
+ split: 3,
1189
+ step: { years: 1 }
1190
+ },
1191
+ {
1192
+ name: 'year',
1193
+ getVisibleDates: function(date, selected) {
1194
+ var years = new Array(yearRange), year = date.getFullYear(), startYear = parseInt((year - 1) / yearRange, 10) * yearRange + 1;
1195
+ for ( var i = 0; i < yearRange; i++ ) {
1196
+ var dt = new Date(startYear + i, 0, 1);
1197
+ years[i] = makeDate(dt, format.year, (selected && selected.getFullYear() === dt.getFullYear()));
1198
+ }
1199
+ return { objects: years, title: [years[0].label, years[yearRange - 1].label].join(' - ') };
1200
+ },
1201
+ compare: function(date1, date2) {
1202
+ return date1.getFullYear() - date2.getFullYear();
1203
+ },
1204
+ split: 5,
1205
+ step: { years: yearRange }
1206
+ }
1207
+ ];
1208
+
1209
+ this.isDisabled = function(date, mode) {
1210
+ var currentMode = this.modes[mode || 0];
1211
+ return ((this.minDate && currentMode.compare(date, this.minDate) < 0) || (this.maxDate && currentMode.compare(date, this.maxDate) > 0) || ($scope.dateDisabled && $scope.dateDisabled({date: date, mode: currentMode.name})));
1212
+ };
1213
+ }])
1214
+
1215
+ .directive( 'datepicker', ['dateFilter', '$parse', 'datepickerConfig', '$log', function (dateFilter, $parse, datepickerConfig, $log) {
1216
+ return {
1217
+ restrict: 'EA',
1218
+ replace: true,
1219
+ templateUrl: 'template/datepicker/datepicker.html',
1220
+ scope: {
1221
+ dateDisabled: '&'
1222
+ },
1223
+ require: ['datepicker', '?^ngModel'],
1224
+ controller: 'DatepickerController',
1225
+ link: function(scope, element, attrs, ctrls) {
1226
+ var datepickerCtrl = ctrls[0], ngModel = ctrls[1];
1227
+
1228
+ if (!ngModel) {
1229
+ return; // do nothing if no ng-model
1230
+ }
1231
+
1232
+ // Configuration parameters
1233
+ var mode = 0, selected = new Date(), showWeeks = datepickerConfig.showWeeks;
1234
+
1235
+ if (attrs.showWeeks) {
1236
+ scope.$parent.$watch($parse(attrs.showWeeks), function(value) {
1237
+ showWeeks = !! value;
1238
+ updateShowWeekNumbers();
1239
+ });
1240
+ } else {
1241
+ updateShowWeekNumbers();
1242
+ }
1243
+
1244
+ if (attrs.min) {
1245
+ scope.$parent.$watch($parse(attrs.min), function(value) {
1246
+ datepickerCtrl.minDate = value ? new Date(value) : null;
1247
+ refill();
1248
+ });
1249
+ }
1250
+ if (attrs.max) {
1251
+ scope.$parent.$watch($parse(attrs.max), function(value) {
1252
+ datepickerCtrl.maxDate = value ? new Date(value) : null;
1253
+ refill();
1254
+ });
1255
+ }
1256
+
1257
+ function updateShowWeekNumbers() {
1258
+ scope.showWeekNumbers = mode === 0 && showWeeks;
1259
+ }
1260
+
1261
+ // Split array into smaller arrays
1262
+ function split(arr, size) {
1263
+ var arrays = [];
1264
+ while (arr.length > 0) {
1265
+ arrays.push(arr.splice(0, size));
1266
+ }
1267
+ return arrays;
1268
+ }
1269
+
1270
+ function refill( updateSelected ) {
1271
+ var date = null, valid = true;
1272
+
1273
+ if ( ngModel.$modelValue ) {
1274
+ date = new Date( ngModel.$modelValue );
1275
+
1276
+ if ( isNaN(date) ) {
1277
+ valid = false;
1278
+ $log.error('Datepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.');
1279
+ } else if ( updateSelected ) {
1280
+ selected = date;
1281
+ }
1282
+ }
1283
+ ngModel.$setValidity('date', valid);
1284
+
1285
+ var currentMode = datepickerCtrl.modes[mode], data = currentMode.getVisibleDates(selected, date);
1286
+ angular.forEach(data.objects, function(obj) {
1287
+ obj.disabled = datepickerCtrl.isDisabled(obj.date, mode);
1288
+ });
1289
+
1290
+ ngModel.$setValidity('date-disabled', (!date || !datepickerCtrl.isDisabled(date)));
1291
+
1292
+ scope.rows = split(data.objects, currentMode.split);
1293
+ scope.labels = data.labels || [];
1294
+ scope.title = data.title;
1295
+ }
1296
+
1297
+ function setMode(value) {
1298
+ mode = value;
1299
+ updateShowWeekNumbers();
1300
+ refill();
1301
+ }
1302
+
1303
+ ngModel.$render = function() {
1304
+ refill( true );
1305
+ };
1306
+
1307
+ scope.select = function( date ) {
1308
+ if ( mode === 0 ) {
1309
+ var dt = new Date( ngModel.$modelValue );
1310
+ dt.setFullYear( date.getFullYear(), date.getMonth(), date.getDate() );
1311
+ ngModel.$setViewValue( dt );
1312
+ refill( true );
1313
+ } else {
1314
+ selected = date;
1315
+ setMode( mode - 1 );
1316
+ }
1317
+ };
1318
+ scope.move = function(direction) {
1319
+ var step = datepickerCtrl.modes[mode].step;
1320
+ selected.setMonth( selected.getMonth() + direction * (step.months || 0) );
1321
+ selected.setFullYear( selected.getFullYear() + direction * (step.years || 0) );
1322
+ refill();
1323
+ };
1324
+ scope.toggleMode = function() {
1325
+ setMode( (mode + 1) % datepickerCtrl.modes.length );
1326
+ };
1327
+ scope.getWeekNumber = function(row) {
1328
+ return ( mode === 0 && scope.showWeekNumbers && row.length === 7 ) ? getISO8601WeekNumber(row[0].date) : null;
1329
+ };
1330
+
1331
+ function getISO8601WeekNumber(date) {
1332
+ var checkDate = new Date(date);
1333
+ checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); // Thursday
1334
+ var time = checkDate.getTime();
1335
+ checkDate.setMonth(0); // Compare with Jan 1
1336
+ checkDate.setDate(1);
1337
+ return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1;
1338
+ }
1339
+ }
1340
+ };
1341
+ }])
1342
+
1343
+ .constant('datepickerPopupConfig', {
1344
+ dateFormat: 'yyyy-MM-dd',
1345
+ currentText: 'Today',
1346
+ toggleWeeksText: 'Weeks',
1347
+ clearText: 'Clear',
1348
+ closeText: 'Done',
1349
+ closeOnDateSelection: true,
1350
+ appendToBody: false
1351
+ })
1352
+
1353
+ .directive('datepickerPopup', ['$compile', '$parse', '$document', '$position', 'dateFilter', 'datepickerPopupConfig',
1354
+ function ($compile, $parse, $document, $position, dateFilter, datepickerPopupConfig) {
1355
+ return {
1356
+ restrict: 'EA',
1357
+ require: 'ngModel',
1358
+ link: function(originalScope, element, attrs, ngModel) {
1359
+ var dateFormat;
1360
+ attrs.$observe('datepickerPopup', function(value) {
1361
+ dateFormat = value || datepickerPopupConfig.dateFormat;
1362
+ ngModel.$render();
1363
+ });
1364
+
1365
+ var closeOnDateSelection = angular.isDefined(attrs.closeOnDateSelection) ? originalScope.$eval(attrs.closeOnDateSelection) : datepickerPopupConfig.closeOnDateSelection;
1366
+ var appendToBody = angular.isDefined(attrs.datepickerAppendToBody) ? originalScope.$eval(attrs.datepickerAppendToBody) : datepickerPopupConfig.appendToBody;
1367
+
1368
+ // create a child scope for the datepicker directive so we are not polluting original scope
1369
+ var scope = originalScope.$new();
1370
+
1371
+ originalScope.$on('$destroy', function() {
1372
+ scope.$destroy();
1373
+ });
1374
+
1375
+ attrs.$observe('currentText', function(text) {
1376
+ scope.currentText = angular.isDefined(text) ? text : datepickerPopupConfig.currentText;
1377
+ });
1378
+ attrs.$observe('toggleWeeksText', function(text) {
1379
+ scope.toggleWeeksText = angular.isDefined(text) ? text : datepickerPopupConfig.toggleWeeksText;
1380
+ });
1381
+ attrs.$observe('clearText', function(text) {
1382
+ scope.clearText = angular.isDefined(text) ? text : datepickerPopupConfig.clearText;
1383
+ });
1384
+ attrs.$observe('closeText', function(text) {
1385
+ scope.closeText = angular.isDefined(text) ? text : datepickerPopupConfig.closeText;
1386
+ });
1387
+
1388
+ var getIsOpen, setIsOpen;
1389
+ if ( attrs.isOpen ) {
1390
+ getIsOpen = $parse(attrs.isOpen);
1391
+ setIsOpen = getIsOpen.assign;
1392
+
1393
+ originalScope.$watch(getIsOpen, function updateOpen(value) {
1394
+ scope.isOpen = !! value;
1395
+ });
1396
+ }
1397
+ scope.isOpen = getIsOpen ? getIsOpen(originalScope) : false; // Initial state
1398
+
1399
+ function setOpen( value ) {
1400
+ if (setIsOpen) {
1401
+ setIsOpen(originalScope, !!value);
1402
+ } else {
1403
+ scope.isOpen = !!value;
1404
+ }
1405
+ }
1406
+
1407
+ var documentClickBind = function(event) {
1408
+ if (scope.isOpen && event.target !== element[0]) {
1409
+ scope.$apply(function() {
1410
+ setOpen(false);
1411
+ });
1412
+ }
1413
+ };
1414
+
1415
+ var elementFocusBind = function() {
1416
+ scope.$apply(function() {
1417
+ setOpen( true );
1418
+ });
1419
+ };
1420
+
1421
+ // popup element used to display calendar
1422
+ var popupEl = angular.element('<datepicker-popup-wrap><datepicker></datepicker></datepicker-popup-wrap>');
1423
+ popupEl.attr({
1424
+ 'ng-model': 'date',
1425
+ 'ng-change': 'dateSelection()'
1426
+ });
1427
+ var datepickerEl = popupEl.find('datepicker');
1428
+ if (attrs.datepickerOptions) {
1429
+ datepickerEl.attr(angular.extend({}, originalScope.$eval(attrs.datepickerOptions)));
1430
+ }
1431
+
1432
+ // TODO: reverse from dateFilter string to Date object
1433
+ function parseDate(viewValue) {
1434
+ if (!viewValue) {
1435
+ ngModel.$setValidity('date', true);
1436
+ return null;
1437
+ } else if (angular.isDate(viewValue)) {
1438
+ ngModel.$setValidity('date', true);
1439
+ return viewValue;
1440
+ } else if (angular.isString(viewValue)) {
1441
+ var date = new Date(viewValue);
1442
+ if (isNaN(date)) {
1443
+ ngModel.$setValidity('date', false);
1444
+ return undefined;
1445
+ } else {
1446
+ ngModel.$setValidity('date', true);
1447
+ return date;
1448
+ }
1449
+ } else {
1450
+ ngModel.$setValidity('date', false);
1451
+ return undefined;
1452
+ }
1453
+ }
1454
+ ngModel.$parsers.unshift(parseDate);
1455
+
1456
+ // Inner change
1457
+ scope.dateSelection = function() {
1458
+ ngModel.$setViewValue(scope.date);
1459
+ ngModel.$render();
1460
+
1461
+ if (closeOnDateSelection) {
1462
+ setOpen( false );
1463
+ }
1464
+ };
1465
+
1466
+ element.bind('input change keyup', function() {
1467
+ scope.$apply(function() {
1468
+ updateCalendar();
1469
+ });
1470
+ });
1471
+
1472
+ // Outter change
1473
+ ngModel.$render = function() {
1474
+ var date = ngModel.$viewValue ? dateFilter(ngModel.$viewValue, dateFormat) : '';
1475
+ element.val(date);
1476
+
1477
+ updateCalendar();
1478
+ };
1479
+
1480
+ function updateCalendar() {
1481
+ scope.date = ngModel.$modelValue;
1482
+ updatePosition();
1483
+ }
1484
+
1485
+ function addWatchableAttribute(attribute, scopeProperty, datepickerAttribute) {
1486
+ if (attribute) {
1487
+ originalScope.$watch($parse(attribute), function(value){
1488
+ scope[scopeProperty] = value;
1489
+ });
1490
+ datepickerEl.attr(datepickerAttribute || scopeProperty, scopeProperty);
1491
+ }
1492
+ }
1493
+ addWatchableAttribute(attrs.min, 'min');
1494
+ addWatchableAttribute(attrs.max, 'max');
1495
+ if (attrs.showWeeks) {
1496
+ addWatchableAttribute(attrs.showWeeks, 'showWeeks', 'show-weeks');
1497
+ } else {
1498
+ scope.showWeeks = true;
1499
+ datepickerEl.attr('show-weeks', 'showWeeks');
1500
+ }
1501
+ if (attrs.dateDisabled) {
1502
+ datepickerEl.attr('date-disabled', attrs.dateDisabled);
1503
+ }
1504
+
1505
+ function updatePosition() {
1506
+ scope.position = $position.position(element);
1507
+ scope.position.top = scope.position.top + element.prop('offsetHeight');
1508
+ }
1509
+
1510
+ var documentBindingInitialized = false, elementFocusInitialized = false;
1511
+ scope.$watch('isOpen', function(value) {
1512
+ if (value) {
1513
+ updatePosition();
1514
+ $document.bind('click', documentClickBind);
1515
+ if(elementFocusInitialized) {
1516
+ element.unbind('focus', elementFocusBind);
1517
+ }
1518
+ element[0].focus();
1519
+ documentBindingInitialized = true;
1520
+ } else {
1521
+ if(documentBindingInitialized) {
1522
+ $document.unbind('click', documentClickBind);
1523
+ }
1524
+ element.bind('focus', elementFocusBind);
1525
+ elementFocusInitialized = true;
1526
+ }
1527
+
1528
+ if ( setIsOpen ) {
1529
+ setIsOpen(originalScope, value);
1530
+ }
1531
+ });
1532
+
1533
+ var $setModelValue = $parse(attrs.ngModel).assign;
1534
+
1535
+ scope.today = function() {
1536
+ $setModelValue(originalScope, new Date());
1537
+ };
1538
+ scope.clear = function() {
1539
+ $setModelValue(originalScope, null);
1540
+ };
1541
+
1542
+ var $popup = $compile(popupEl)(scope);
1543
+ if ( appendToBody ) {
1544
+ $document.find('body').append($popup);
1545
+ } else {
1546
+ element.after($popup);
1547
+ }
1548
+ }
1549
+ };
1550
+ }])
1551
+
1552
+ .directive('datepickerPopupWrap', function() {
1553
+ return {
1554
+ restrict:'E',
1555
+ replace: true,
1556
+ transclude: true,
1557
+ templateUrl: 'template/datepicker/popup.html',
1558
+ link:function (scope, element, attrs) {
1559
+ element.bind('click', function(event) {
1560
+ event.preventDefault();
1561
+ event.stopPropagation();
1562
+ });
1563
+ }
1564
+ };
1565
+ });
1566
+
1567
+ /*
1568
+ * dropdownToggle - Provides dropdown menu functionality in place of bootstrap js
1569
+ * @restrict class or attribute
1570
+ * @example:
1571
+ <li class="dropdown">
1572
+ <a class="dropdown-toggle">My Dropdown Menu</a>
1573
+ <ul class="dropdown-menu">
1574
+ <li ng-repeat="choice in dropChoices">
1575
+ <a ng-href="{{choice.href}}">{{choice.text}}</a>
1576
+ </li>
1577
+ </ul>
1578
+ </li>
1579
+ */
1580
+
1581
+ angular.module('ui.bootstrap.dropdownToggle', []).directive('dropdownToggle', ['$document', '$location', function ($document, $location) {
1582
+ var openElement = null,
1583
+ closeMenu = angular.noop;
1584
+ return {
1585
+ restrict: 'CA',
1586
+ link: function(scope, element, attrs) {
1587
+ scope.$watch('$location.path', function() { closeMenu(); });
1588
+ element.parent().bind('click', function() { closeMenu(); });
1589
+ element.bind('click', function (event) {
1590
+
1591
+ var elementWasOpen = (element === openElement);
1592
+
1593
+ event.preventDefault();
1594
+ event.stopPropagation();
1595
+
1596
+ if (!!openElement) {
1597
+ closeMenu();
1598
+ }
1599
+
1600
+ if (!elementWasOpen) {
1601
+ element.parent().addClass('open');
1602
+ openElement = element;
1603
+ closeMenu = function (event) {
1604
+ if (event) {
1605
+ event.preventDefault();
1606
+ event.stopPropagation();
1607
+ }
1608
+ $document.unbind('click', closeMenu);
1609
+ element.parent().removeClass('open');
1610
+ closeMenu = angular.noop;
1611
+ openElement = null;
1612
+ };
1613
+ $document.bind('click', closeMenu);
1614
+ }
1615
+ });
1616
+ }
1617
+ };
1618
+ }]);
1619
+ angular.module('ui.bootstrap.modal', [])
1620
+
1621
+ /**
1622
+ * A helper, internal data structure that acts as a map but also allows getting / removing
1623
+ * elements in the LIFO order
1624
+ */
1625
+ .factory('$$stackedMap', function () {
1626
+ return {
1627
+ createNew: function () {
1628
+ var stack = [];
1629
+
1630
+ return {
1631
+ add: function (key, value) {
1632
+ stack.push({
1633
+ key: key,
1634
+ value: value
1635
+ });
1636
+ },
1637
+ get: function (key) {
1638
+ for (var i = 0; i < stack.length; i++) {
1639
+ if (key == stack[i].key) {
1640
+ return stack[i];
1641
+ }
1642
+ }
1643
+ },
1644
+ keys: function() {
1645
+ var keys = [];
1646
+ for (var i = 0; i < stack.length; i++) {
1647
+ keys.push(stack[i].key);
1648
+ }
1649
+ return keys;
1650
+ },
1651
+ top: function () {
1652
+ return stack[stack.length - 1];
1653
+ },
1654
+ remove: function (key) {
1655
+ var idx = -1;
1656
+ for (var i = 0; i < stack.length; i++) {
1657
+ if (key == stack[i].key) {
1658
+ idx = i;
1659
+ break;
1660
+ }
1661
+ }
1662
+ return stack.splice(idx, 1)[0];
1663
+ },
1664
+ removeTop: function () {
1665
+ return stack.splice(stack.length - 1, 1)[0];
1666
+ },
1667
+ length: function () {
1668
+ return stack.length;
1669
+ }
1670
+ };
1671
+ }
1672
+ };
1673
+ })
1674
+
1675
+ .constant('modalConfig', {
1676
+ backdropTemplateUrl: 'template/modal/backdrop.html',
1677
+ windowTemplateUrl: 'template/modal/window.html'
1678
+ })
1679
+
1680
+ /**
1681
+ * A helper directive for the $modal service. It creates a backdrop element.
1682
+ */
1683
+ .directive('modalBackdrop', ['$modalStack', '$timeout', 'modalConfig', function ($modalStack, $timeout, modalConfig) {
1684
+ return {
1685
+ restrict: 'EA',
1686
+ replace: true,
1687
+ templateUrl: modalConfig.backdropTemplateUrl,
1688
+ link: function (scope, element, attrs) {
1689
+
1690
+ //trigger CSS transitions
1691
+ $timeout(function () {
1692
+ scope.animate = true;
1693
+ });
1694
+
1695
+ scope.close = function (evt) {
1696
+ var modal = $modalStack.getTop();
1697
+ if (modal && modal.value.backdrop && modal.value.backdrop != 'static') {
1698
+ evt.preventDefault();
1699
+ evt.stopPropagation();
1700
+ $modalStack.dismiss(modal.key, 'backdrop click');
1701
+ }
1702
+ };
1703
+ }
1704
+ };
1705
+ }])
1706
+
1707
+ .directive('modalWindow', ['$timeout', 'modalConfig', function ($timeout, modalConfig) {
1708
+ return {
1709
+ restrict: 'EA',
1710
+ scope: {
1711
+ index: '@'
1712
+ },
1713
+ replace: true,
1714
+ transclude: true,
1715
+ templateUrl: modalConfig.windowTemplateUrl,
1716
+ link: function (scope, element, attrs) {
1717
+ scope.windowClass = attrs.windowClass || '';
1718
+
1719
+ //trigger CSS transitions
1720
+ $timeout(function () {
1721
+ scope.animate = true;
1722
+ });
1723
+ }
1724
+ };
1725
+ }])
1726
+
1727
+ .factory('$modalStack', ['$document', '$compile', '$rootScope', '$$stackedMap',
1728
+ function ($document, $compile, $rootScope, $$stackedMap) {
1729
+
1730
+ var backdropjqLiteEl, backdropDomEl;
1731
+ var backdropScope = $rootScope.$new(true);
1732
+ var body = $document.find('body').eq(0);
1733
+ var openedWindows = $$stackedMap.createNew();
1734
+ var $modalStack = {};
1735
+
1736
+ function backdropIndex() {
1737
+ var topBackdropIndex = -1;
1738
+ var opened = openedWindows.keys();
1739
+ for (var i = 0; i < opened.length; i++) {
1740
+ if (openedWindows.get(opened[i]).value.backdrop) {
1741
+ topBackdropIndex = i;
1742
+ }
1743
+ }
1744
+ return topBackdropIndex;
1745
+ }
1746
+
1747
+ $rootScope.$watch(backdropIndex, function(newBackdropIndex){
1748
+ backdropScope.index = newBackdropIndex;
1749
+ });
1750
+
1751
+ function removeModalWindow(modalInstance) {
1752
+
1753
+ var modalWindow = openedWindows.get(modalInstance).value;
1754
+
1755
+ //clean up the stack
1756
+ openedWindows.remove(modalInstance);
1757
+
1758
+ //remove window DOM element
1759
+ modalWindow.modalDomEl.remove();
1760
+
1761
+ //remove backdrop if no longer needed
1762
+ if (backdropDomEl && backdropIndex() == -1) {
1763
+ backdropDomEl.remove();
1764
+ backdropDomEl = undefined;
1765
+ }
1766
+
1767
+ //destroy scope
1768
+ modalWindow.modalScope.$destroy();
1769
+ }
1770
+
1771
+ $document.bind('keydown', function (evt) {
1772
+ var modal;
1773
+
1774
+ if (evt.which === 27) {
1775
+ modal = openedWindows.top();
1776
+ if (modal && modal.value.keyboard) {
1777
+ $rootScope.$apply(function () {
1778
+ $modalStack.dismiss(modal.key);
1779
+ });
1780
+ }
1781
+ }
1782
+ });
1783
+
1784
+ $modalStack.open = function (modalInstance, modal) {
1785
+
1786
+ openedWindows.add(modalInstance, {
1787
+ deferred: modal.deferred,
1788
+ modalScope: modal.scope,
1789
+ backdrop: modal.backdrop,
1790
+ keyboard: modal.keyboard
1791
+ });
1792
+
1793
+ var angularDomEl = angular.element('<div modal-window></div>');
1794
+ angularDomEl.attr('window-class', modal.windowClass);
1795
+ angularDomEl.attr('index', openedWindows.length() - 1);
1796
+ angularDomEl.html(modal.content);
1797
+
1798
+ var modalDomEl = $compile(angularDomEl)(modal.scope);
1799
+ openedWindows.top().value.modalDomEl = modalDomEl;
1800
+ body.append(modalDomEl);
1801
+
1802
+ if (backdropIndex() >= 0 && !backdropDomEl) {
1803
+ backdropjqLiteEl = angular.element('<div modal-backdrop></div>');
1804
+ backdropDomEl = $compile(backdropjqLiteEl)(backdropScope);
1805
+ body.append(backdropDomEl);
1806
+ }
1807
+ };
1808
+
1809
+ $modalStack.close = function (modalInstance, result) {
1810
+ var modal = openedWindows.get(modalInstance);
1811
+ if (modal) {
1812
+ modal.value.deferred.resolve(result);
1813
+ removeModalWindow(modalInstance);
1814
+ }
1815
+ };
1816
+
1817
+ $modalStack.dismiss = function (modalInstance, reason) {
1818
+ var modalWindow = openedWindows.get(modalInstance).value;
1819
+ if (modalWindow) {
1820
+ modalWindow.deferred.reject(reason);
1821
+ removeModalWindow(modalInstance);
1822
+ }
1823
+ };
1824
+
1825
+ $modalStack.getTop = function () {
1826
+ return openedWindows.top();
1827
+ };
1828
+
1829
+ return $modalStack;
1830
+ }])
1831
+
1832
+ .provider('$modal', function () {
1833
+
1834
+ var $modalProvider = {
1835
+ options: {
1836
+ backdrop: true, //can be also false or 'static'
1837
+ keyboard: true
1838
+ },
1839
+ $get: ['$injector', '$rootScope', '$q', '$http', '$templateCache', '$controller', '$modalStack',
1840
+ function ($injector, $rootScope, $q, $http, $templateCache, $controller, $modalStack) {
1841
+
1842
+ var $modal = {};
1843
+
1844
+ function getTemplatePromise(options) {
1845
+ return options.template ? $q.when(options.template) :
1846
+ $http.get(options.templateUrl, {cache: $templateCache}).then(function (result) {
1847
+ return result.data;
1848
+ });
1849
+ }
1850
+
1851
+ function getResolvePromises(resolves) {
1852
+ var promisesArr = [];
1853
+ angular.forEach(resolves, function (value, key) {
1854
+ if (angular.isFunction(value) || angular.isArray(value)) {
1855
+ promisesArr.push($q.when($injector.invoke(value)));
1856
+ }
1857
+ });
1858
+ return promisesArr;
1859
+ }
1860
+
1861
+ $modal.open = function (modalOptions) {
1862
+
1863
+ var modalResultDeferred = $q.defer();
1864
+ var modalOpenedDeferred = $q.defer();
1865
+
1866
+ //prepare an instance of a modal to be injected into controllers and returned to a caller
1867
+ var modalInstance = {
1868
+ result: modalResultDeferred.promise,
1869
+ opened: modalOpenedDeferred.promise,
1870
+ close: function (result) {
1871
+ $modalStack.close(modalInstance, result);
1872
+ },
1873
+ dismiss: function (reason) {
1874
+ $modalStack.dismiss(modalInstance, reason);
1875
+ }
1876
+ };
1877
+
1878
+ //merge and clean up options
1879
+ modalOptions = angular.extend({}, $modalProvider.options, modalOptions);
1880
+ modalOptions.resolve = modalOptions.resolve || {};
1881
+
1882
+ //verify options
1883
+ if (!modalOptions.template && !modalOptions.templateUrl) {
1884
+ throw new Error('One of template or templateUrl options is required.');
1885
+ }
1886
+
1887
+ var templateAndResolvePromise =
1888
+ $q.all([getTemplatePromise(modalOptions)].concat(getResolvePromises(modalOptions.resolve)));
1889
+
1890
+
1891
+ templateAndResolvePromise.then(function resolveSuccess(tplAndVars) {
1892
+
1893
+ var modalScope = (modalOptions.scope || $rootScope).$new();
1894
+ modalScope.$close = modalInstance.close;
1895
+ modalScope.$dismiss = modalInstance.dismiss;
1896
+
1897
+ var ctrlInstance, ctrlLocals = {};
1898
+ var resolveIter = 1;
1899
+
1900
+ //controllers
1901
+ if (modalOptions.controller) {
1902
+ ctrlLocals.$scope = modalScope;
1903
+ ctrlLocals.$modalInstance = modalInstance;
1904
+ angular.forEach(modalOptions.resolve, function (value, key) {
1905
+ ctrlLocals[key] = tplAndVars[resolveIter++];
1906
+ });
1907
+
1908
+ ctrlInstance = $controller(modalOptions.controller, ctrlLocals);
1909
+ }
1910
+
1911
+ $modalStack.open(modalInstance, {
1912
+ scope: modalScope,
1913
+ deferred: modalResultDeferred,
1914
+ content: tplAndVars[0],
1915
+ backdrop: modalOptions.backdrop,
1916
+ keyboard: modalOptions.keyboard,
1917
+ windowClass: modalOptions.windowClass
1918
+ });
1919
+
1920
+ }, function resolveError(reason) {
1921
+ modalResultDeferred.reject(reason);
1922
+ });
1923
+
1924
+ templateAndResolvePromise.then(function () {
1925
+ modalOpenedDeferred.resolve(true);
1926
+ }, function () {
1927
+ modalOpenedDeferred.reject(false);
1928
+ });
1929
+
1930
+ return modalInstance;
1931
+ };
1932
+
1933
+ return $modal;
1934
+ }]
1935
+ };
1936
+
1937
+ return $modalProvider;
1938
+ });
1939
+
1940
+ angular.module('ui.bootstrap.pagination', [])
1941
+
1942
+ .controller('PaginationController', ['$scope', '$attrs', '$parse', '$interpolate', function ($scope, $attrs, $parse, $interpolate) {
1943
+ var self = this;
1944
+
1945
+ this.init = function(defaultItemsPerPage) {
1946
+ if ($attrs.itemsPerPage) {
1947
+ $scope.$parent.$watch($parse($attrs.itemsPerPage), function(value) {
1948
+ self.itemsPerPage = parseInt(value, 10);
1949
+ $scope.totalPages = self.calculateTotalPages();
1950
+ });
1951
+ } else {
1952
+ this.itemsPerPage = defaultItemsPerPage;
1953
+ }
1954
+ };
1955
+
1956
+ this.noPrevious = function() {
1957
+ return this.page === 1;
1958
+ };
1959
+ this.noNext = function() {
1960
+ return this.page === $scope.totalPages;
1961
+ };
1962
+
1963
+ this.isActive = function(page) {
1964
+ return this.page === page;
1965
+ };
1966
+
1967
+ this.calculateTotalPages = function() {
1968
+ return this.itemsPerPage < 1 ? 1 : Math.ceil($scope.totalItems / this.itemsPerPage);
1969
+ };
1970
+
1971
+ this.getAttributeValue = function(attribute, defaultValue, interpolate) {
1972
+ return angular.isDefined(attribute) ? (interpolate ? $interpolate(attribute)($scope.$parent) : $scope.$parent.$eval(attribute)) : defaultValue;
1973
+ };
1974
+
1975
+ this.render = function() {
1976
+ this.page = parseInt($scope.page, 10) || 1;
1977
+ $scope.pages = this.getPages(this.page, $scope.totalPages);
1978
+ };
1979
+
1980
+ $scope.selectPage = function(page) {
1981
+ if ( ! self.isActive(page) && page > 0 && page <= $scope.totalPages) {
1982
+ $scope.page = page;
1983
+ $scope.onSelectPage({ page: page });
1984
+ }
1985
+ };
1986
+
1987
+ $scope.$watch('totalItems', function() {
1988
+ $scope.totalPages = self.calculateTotalPages();
1989
+ });
1990
+
1991
+ $scope.$watch('totalPages', function(value) {
1992
+ if ( $attrs.numPages ) {
1993
+ $scope.numPages = value; // Readonly variable
1994
+ }
1995
+
1996
+ if ( self.page > value ) {
1997
+ $scope.selectPage(value);
1998
+ } else {
1999
+ self.render();
2000
+ }
2001
+ });
2002
+
2003
+ $scope.$watch('page', function() {
2004
+ self.render();
2005
+ });
2006
+ }])
2007
+
2008
+ .constant('paginationConfig', {
2009
+ itemsPerPage: 10,
2010
+ boundaryLinks: false,
2011
+ directionLinks: true,
2012
+ firstText: 'First',
2013
+ previousText: 'Previous',
2014
+ nextText: 'Next',
2015
+ lastText: 'Last',
2016
+ rotate: true,
2017
+ templateUrl: 'template/pagination/pagination.html'
2018
+ })
2019
+
2020
+ .directive('pagination', ['$parse', 'paginationConfig', function($parse, config) {
2021
+ return {
2022
+ restrict: 'EA',
2023
+ scope: {
2024
+ page: '=',
2025
+ totalItems: '=',
2026
+ onSelectPage:' &',
2027
+ numPages: '='
2028
+ },
2029
+ controller: 'PaginationController',
2030
+ templateUrl: config.templateUrl,
2031
+ replace: true,
2032
+ link: function(scope, element, attrs, paginationCtrl) {
2033
+
2034
+ // Setup configuration parameters
2035
+ var maxSize,
2036
+ boundaryLinks = paginationCtrl.getAttributeValue(attrs.boundaryLinks, config.boundaryLinks ),
2037
+ directionLinks = paginationCtrl.getAttributeValue(attrs.directionLinks, config.directionLinks ),
2038
+ firstText = paginationCtrl.getAttributeValue(attrs.firstText, config.firstText, true),
2039
+ previousText = paginationCtrl.getAttributeValue(attrs.previousText, config.previousText, true),
2040
+ nextText = paginationCtrl.getAttributeValue(attrs.nextText, config.nextText, true),
2041
+ lastText = paginationCtrl.getAttributeValue(attrs.lastText, config.lastText, true),
2042
+ rotate = paginationCtrl.getAttributeValue(attrs.rotate, config.rotate);
2043
+
2044
+ paginationCtrl.init(config.itemsPerPage);
2045
+
2046
+ if (attrs.maxSize) {
2047
+ scope.$parent.$watch($parse(attrs.maxSize), function(value) {
2048
+ maxSize = parseInt(value, 10);
2049
+ paginationCtrl.render();
2050
+ });
2051
+ }
2052
+
2053
+ // Create page object used in template
2054
+ function makePage(number, text, isActive, isDisabled) {
2055
+ return {
2056
+ number: number,
2057
+ text: text,
2058
+ active: isActive,
2059
+ disabled: isDisabled
2060
+ };
2061
+ }
2062
+
2063
+ paginationCtrl.getPages = function(currentPage, totalPages) {
2064
+ var pages = [];
2065
+
2066
+ // Default page limits
2067
+ var startPage = 1, endPage = totalPages;
2068
+ var isMaxSized = ( angular.isDefined(maxSize) && maxSize < totalPages );
2069
+
2070
+ // recompute if maxSize
2071
+ if ( isMaxSized ) {
2072
+ if ( rotate ) {
2073
+ // Current page is displayed in the middle of the visible ones
2074
+ startPage = Math.max(currentPage - Math.floor(maxSize/2), 1);
2075
+ endPage = startPage + maxSize - 1;
2076
+
2077
+ // Adjust if limit is exceeded
2078
+ if (endPage > totalPages) {
2079
+ endPage = totalPages;
2080
+ startPage = endPage - maxSize + 1;
2081
+ }
2082
+ } else {
2083
+ // Visible pages are paginated with maxSize
2084
+ startPage = ((Math.ceil(currentPage / maxSize) - 1) * maxSize) + 1;
2085
+
2086
+ // Adjust last page if limit is exceeded
2087
+ endPage = Math.min(startPage + maxSize - 1, totalPages);
2088
+ }
2089
+ }
2090
+
2091
+ // Add page number links
2092
+ for (var number = startPage; number <= endPage; number++) {
2093
+ var page = makePage(number, number, paginationCtrl.isActive(number), false);
2094
+ pages.push(page);
2095
+ }
2096
+
2097
+ // Add links to move between page sets
2098
+ if ( isMaxSized && ! rotate ) {
2099
+ if ( startPage > 1 ) {
2100
+ var previousPageSet = makePage(startPage - 1, '...', false, false);
2101
+ pages.unshift(previousPageSet);
2102
+ }
2103
+
2104
+ if ( endPage < totalPages ) {
2105
+ var nextPageSet = makePage(endPage + 1, '...', false, false);
2106
+ pages.push(nextPageSet);
2107
+ }
2108
+ }
2109
+
2110
+ // Add previous & next links
2111
+ if (directionLinks) {
2112
+ var previousPage = makePage(currentPage - 1, previousText, false, paginationCtrl.noPrevious());
2113
+ pages.unshift(previousPage);
2114
+
2115
+ var nextPage = makePage(currentPage + 1, nextText, false, paginationCtrl.noNext());
2116
+ pages.push(nextPage);
2117
+ }
2118
+
2119
+ // Add first & last links
2120
+ if (boundaryLinks) {
2121
+ var firstPage = makePage(1, firstText, false, paginationCtrl.noPrevious());
2122
+ pages.unshift(firstPage);
2123
+
2124
+ var lastPage = makePage(totalPages, lastText, false, paginationCtrl.noNext());
2125
+ pages.push(lastPage);
2126
+ }
2127
+
2128
+ return pages;
2129
+ };
2130
+ }
2131
+ };
2132
+ }])
2133
+
2134
+ .constant('pagerConfig', {
2135
+ itemsPerPage: 10,
2136
+ previousText: '« Previous',
2137
+ nextText: 'Next »',
2138
+ align: true,
2139
+ templateUrl: 'template/pagination/pager.html'
2140
+ })
2141
+
2142
+ .directive('pager', ['pagerConfig', function(config) {
2143
+ return {
2144
+ restrict: 'EA',
2145
+ scope: {
2146
+ page: '=',
2147
+ totalItems: '=',
2148
+ onSelectPage:' &',
2149
+ numPages: '='
2150
+ },
2151
+ controller: 'PaginationController',
2152
+ templateUrl: config.templateUrl,
2153
+ replace: true,
2154
+ link: function(scope, element, attrs, paginationCtrl) {
2155
+
2156
+ // Setup configuration parameters
2157
+ var previousText = paginationCtrl.getAttributeValue(attrs.previousText, config.previousText, true),
2158
+ nextText = paginationCtrl.getAttributeValue(attrs.nextText, config.nextText, true),
2159
+ align = paginationCtrl.getAttributeValue(attrs.align, config.align);
2160
+
2161
+ paginationCtrl.init(config.itemsPerPage);
2162
+
2163
+ // Create page object used in template
2164
+ function makePage(number, text, isDisabled, isPrevious, isNext) {
2165
+ return {
2166
+ number: number,
2167
+ text: text,
2168
+ disabled: isDisabled,
2169
+ previous: ( align && isPrevious ),
2170
+ next: ( align && isNext )
2171
+ };
2172
+ }
2173
+
2174
+ paginationCtrl.getPages = function(currentPage) {
2175
+ return [
2176
+ makePage(currentPage - 1, previousText, paginationCtrl.noPrevious(), true, false),
2177
+ makePage(currentPage + 1, nextText, paginationCtrl.noNext(), false, true)
2178
+ ];
2179
+ };
2180
+ }
2181
+ };
2182
+ }]);
2183
+
2184
+ /**
2185
+ * The following features are still outstanding: animation as a
2186
+ * function, placement as a function, inside, support for more triggers than
2187
+ * just mouse enter/leave, html tooltips, and selector delegation.
2188
+ */
2189
+ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap.bindHtml' ] )
2190
+
2191
+ /**
2192
+ * The $tooltip service creates tooltip- and popover-like directives as well as
2193
+ * houses global options for them.
2194
+ */
2195
+ .provider( '$tooltip', function () {
2196
+ // The default options tooltip and popover.
2197
+ var defaultOptions = {
2198
+ placement: 'top',
2199
+ animation: true,
2200
+ popupDelay: 0
2201
+ };
2202
+
2203
+ // Default hide triggers for each show trigger
2204
+ var triggerMap = {
2205
+ 'mouseenter': 'mouseleave',
2206
+ 'click': 'click',
2207
+ 'focus': 'blur'
2208
+ };
2209
+
2210
+ // The options specified to the provider globally.
2211
+ var globalOptions = {};
2212
+
2213
+ /**
2214
+ * `options({})` allows global configuration of all tooltips in the
2215
+ * application.
2216
+ *
2217
+ * var app = angular.module( 'App', ['ui.bootstrap.tooltip'], function( $tooltipProvider ) {
2218
+ * // place tooltips left instead of top by default
2219
+ * $tooltipProvider.options( { placement: 'left' } );
2220
+ * });
2221
+ */
2222
+ this.options = function( value ) {
2223
+ angular.extend( globalOptions, value );
2224
+ };
2225
+
2226
+ /**
2227
+ * This allows you to extend the set of trigger mappings available. E.g.:
2228
+ *
2229
+ * $tooltipProvider.setTriggers( 'openTrigger': 'closeTrigger' );
2230
+ */
2231
+ this.setTriggers = function setTriggers ( triggers ) {
2232
+ angular.extend( triggerMap, triggers );
2233
+ };
2234
+
2235
+ /**
2236
+ * This is a helper function for translating camel-case to snake-case.
2237
+ */
2238
+ function snake_case(name){
2239
+ var regexp = /[A-Z]/g;
2240
+ var separator = '-';
2241
+ return name.replace(regexp, function(letter, pos) {
2242
+ return (pos ? separator : '') + letter.toLowerCase();
2243
+ });
2244
+ }
2245
+
2246
+ /**
2247
+ * Returns the actual instance of the $tooltip service.
2248
+ * TODO support multiple triggers
2249
+ */
2250
+ this.$get = [ '$window', '$compile', '$timeout', '$parse', '$document', '$position', '$interpolate', function ( $window, $compile, $timeout, $parse, $document, $position, $interpolate ) {
2251
+ return function $tooltip ( type, prefix, defaultTriggerShow ) {
2252
+ var options = angular.extend( {}, defaultOptions, globalOptions );
2253
+
2254
+ /**
2255
+ * Returns an object of show and hide triggers.
2256
+ *
2257
+ * If a trigger is supplied,
2258
+ * it is used to show the tooltip; otherwise, it will use the `trigger`
2259
+ * option passed to the `$tooltipProvider.options` method; else it will
2260
+ * default to the trigger supplied to this directive factory.
2261
+ *
2262
+ * The hide trigger is based on the show trigger. If the `trigger` option
2263
+ * was passed to the `$tooltipProvider.options` method, it will use the
2264
+ * mapped trigger from `triggerMap` or the passed trigger if the map is
2265
+ * undefined; otherwise, it uses the `triggerMap` value of the show
2266
+ * trigger; else it will just use the show trigger.
2267
+ */
2268
+ function getTriggers ( trigger ) {
2269
+ var show = trigger || options.trigger || defaultTriggerShow;
2270
+ var hide = triggerMap[show] || show;
2271
+ return {
2272
+ show: show,
2273
+ hide: hide
2274
+ };
2275
+ }
2276
+
2277
+ var directiveName = snake_case( type );
2278
+
2279
+ var startSym = $interpolate.startSymbol();
2280
+ var endSym = $interpolate.endSymbol();
2281
+ var template =
2282
+ '<'+ directiveName +'-popup '+
2283
+ 'title="'+startSym+'tt_title'+endSym+'" '+
2284
+ 'content="'+startSym+'tt_content'+endSym+'" '+
2285
+ 'placement="'+startSym+'tt_placement'+endSym+'" '+
2286
+ 'animation="tt_animation()" '+
2287
+ 'is-open="tt_isOpen"'+
2288
+ '>'+
2289
+ '</'+ directiveName +'-popup>';
2290
+
2291
+ return {
2292
+ restrict: 'EA',
2293
+ scope: true,
2294
+ link: function link ( scope, element, attrs ) {
2295
+ var tooltip = $compile( template )( scope );
2296
+ var transitionTimeout;
2297
+ var popupTimeout;
2298
+ var $body;
2299
+ var appendToBody = angular.isDefined( options.appendToBody ) ? options.appendToBody : false;
2300
+ var triggers = getTriggers( undefined );
2301
+ var hasRegisteredTriggers = false;
2302
+
2303
+ // By default, the tooltip is not open.
2304
+ // TODO add ability to start tooltip opened
2305
+ scope.tt_isOpen = false;
2306
+
2307
+ function toggleTooltipBind () {
2308
+ if ( ! scope.tt_isOpen ) {
2309
+ showTooltipBind();
2310
+ } else {
2311
+ hideTooltipBind();
2312
+ }
2313
+ }
2314
+
2315
+ // Show the tooltip with delay if specified, otherwise show it immediately
2316
+ function showTooltipBind() {
2317
+ if ( scope.tt_popupDelay ) {
2318
+ popupTimeout = $timeout( show, scope.tt_popupDelay );
2319
+ } else {
2320
+ scope.$apply( show );
2321
+ }
2322
+ }
2323
+
2324
+ function hideTooltipBind () {
2325
+ scope.$apply(function () {
2326
+ hide();
2327
+ });
2328
+ }
2329
+
2330
+ // Show the tooltip popup element.
2331
+ function show() {
2332
+ var position,
2333
+ ttWidth,
2334
+ ttHeight,
2335
+ ttPosition;
2336
+
2337
+ // Don't show empty tooltips.
2338
+ if ( ! scope.tt_content ) {
2339
+ return;
2340
+ }
2341
+
2342
+ // If there is a pending remove transition, we must cancel it, lest the
2343
+ // tooltip be mysteriously removed.
2344
+ if ( transitionTimeout ) {
2345
+ $timeout.cancel( transitionTimeout );
2346
+ }
2347
+
2348
+ // Set the initial positioning.
2349
+ tooltip.css({ top: 0, left: 0, display: 'block' });
2350
+
2351
+ // Now we add it to the DOM because need some info about it. But it's not
2352
+ // visible yet anyway.
2353
+ if ( appendToBody ) {
2354
+ $body = $body || $document.find( 'body' );
2355
+ $body.append( tooltip );
2356
+ } else {
2357
+ element.after( tooltip );
2358
+ }
2359
+
2360
+ // Get the position of the directive element.
2361
+ position = appendToBody ? $position.offset( element ) : $position.position( element );
2362
+
2363
+ // Get the height and width of the tooltip so we can center it.
2364
+ ttWidth = tooltip.prop( 'offsetWidth' );
2365
+ ttHeight = tooltip.prop( 'offsetHeight' );
2366
+
2367
+ // Calculate the tooltip's top and left coordinates to center it with
2368
+ // this directive.
2369
+ switch ( scope.tt_placement ) {
2370
+ case 'right':
2371
+ ttPosition = {
2372
+ top: position.top + position.height / 2 - ttHeight / 2,
2373
+ left: position.left + position.width
2374
+ };
2375
+ break;
2376
+ case 'bottom':
2377
+ ttPosition = {
2378
+ top: position.top + position.height,
2379
+ left: position.left + position.width / 2 - ttWidth / 2
2380
+ };
2381
+ break;
2382
+ case 'left':
2383
+ ttPosition = {
2384
+ top: position.top + position.height / 2 - ttHeight / 2,
2385
+ left: position.left - ttWidth
2386
+ };
2387
+ break;
2388
+ default:
2389
+ ttPosition = {
2390
+ top: position.top - ttHeight,
2391
+ left: position.left + position.width / 2 - ttWidth / 2
2392
+ };
2393
+ break;
2394
+ }
2395
+
2396
+ ttPosition.top += 'px';
2397
+ ttPosition.left += 'px';
2398
+
2399
+ // Now set the calculated positioning.
2400
+ tooltip.css( ttPosition );
2401
+
2402
+ // And show the tooltip.
2403
+ scope.tt_isOpen = true;
2404
+ }
2405
+
2406
+ // Hide the tooltip popup element.
2407
+ function hide() {
2408
+ // First things first: we don't show it anymore.
2409
+ scope.tt_isOpen = false;
2410
+
2411
+ //if tooltip is going to be shown after delay, we must cancel this
2412
+ $timeout.cancel( popupTimeout );
2413
+
2414
+ // And now we remove it from the DOM. However, if we have animation, we
2415
+ // need to wait for it to expire beforehand.
2416
+ // FIXME: this is a placeholder for a port of the transitions library.
2417
+ if ( angular.isDefined( scope.tt_animation ) && scope.tt_animation() ) {
2418
+ transitionTimeout = $timeout( function () { tooltip.remove(); }, 500 );
2419
+ } else {
2420
+ tooltip.remove();
2421
+ }
2422
+ }
2423
+
2424
+ /**
2425
+ * Observe the relevant attributes.
2426
+ */
2427
+ attrs.$observe( type, function ( val ) {
2428
+ scope.tt_content = val;
2429
+ });
2430
+
2431
+ attrs.$observe( prefix+'Title', function ( val ) {
2432
+ scope.tt_title = val;
2433
+ });
2434
+
2435
+ attrs.$observe( prefix+'Placement', function ( val ) {
2436
+ scope.tt_placement = angular.isDefined( val ) ? val : options.placement;
2437
+ });
2438
+
2439
+ attrs.$observe( prefix+'Animation', function ( val ) {
2440
+ scope.tt_animation = angular.isDefined( val ) ? $parse( val ) : function(){ return options.animation; };
2441
+ });
2442
+
2443
+ attrs.$observe( prefix+'PopupDelay', function ( val ) {
2444
+ var delay = parseInt( val, 10 );
2445
+ scope.tt_popupDelay = ! isNaN(delay) ? delay : options.popupDelay;
2446
+ });
2447
+
2448
+ attrs.$observe( prefix+'Trigger', function ( val ) {
2449
+
2450
+ if (hasRegisteredTriggers) {
2451
+ element.unbind( triggers.show, showTooltipBind );
2452
+ element.unbind( triggers.hide, hideTooltipBind );
2453
+ }
2454
+
2455
+ triggers = getTriggers( val );
2456
+
2457
+ if ( triggers.show === triggers.hide ) {
2458
+ element.bind( triggers.show, toggleTooltipBind );
2459
+ } else {
2460
+ element.bind( triggers.show, showTooltipBind );
2461
+ element.bind( triggers.hide, hideTooltipBind );
2462
+ }
2463
+
2464
+ hasRegisteredTriggers = true;
2465
+ });
2466
+
2467
+ attrs.$observe( prefix+'AppendToBody', function ( val ) {
2468
+ appendToBody = angular.isDefined( val ) ? $parse( val )( scope ) : appendToBody;
2469
+ });
2470
+
2471
+ // if a tooltip is attached to <body> we need to remove it on
2472
+ // location change as its parent scope will probably not be destroyed
2473
+ // by the change.
2474
+ if ( appendToBody ) {
2475
+ scope.$on('$locationChangeSuccess', function closeTooltipOnLocationChangeSuccess () {
2476
+ if ( scope.tt_isOpen ) {
2477
+ hide();
2478
+ }
2479
+ });
2480
+ }
2481
+
2482
+ // Make sure tooltip is destroyed and removed.
2483
+ scope.$on('$destroy', function onDestroyTooltip() {
2484
+ if ( scope.tt_isOpen ) {
2485
+ hide();
2486
+ } else {
2487
+ tooltip.remove();
2488
+ }
2489
+ });
2490
+ }
2491
+ };
2492
+ };
2493
+ }];
2494
+ })
2495
+
2496
+ .constant('tooltipConfig', {
2497
+ templateUrl: 'template/tooltip/tooltip-popup.html',
2498
+ htmlUnsafeTemplateUrl: 'template/tooltip/tooltip-html-unsafe-popup.html'
2499
+ })
2500
+
2501
+ .directive( 'tooltipPopup', ['tooltipConfig', function (tooltipConfig) {
2502
+ return {
2503
+ restrict: 'E',
2504
+ replace: true,
2505
+ scope: { content: '@', placement: '@', animation: '&', isOpen: '&' },
2506
+ templateUrl: tooltipConfig.templateUrl
2507
+ };
2508
+ }])
2509
+
2510
+ .directive( 'tooltip', [ '$tooltip', function ( $tooltip ) {
2511
+ return $tooltip( 'tooltip', 'tooltip', 'mouseenter' );
2512
+ }])
2513
+
2514
+ .directive( 'tooltipHtmlUnsafePopup', ['tooltipConfig', function (tooltipConfig) {
2515
+ return {
2516
+ restrict: 'E',
2517
+ replace: true,
2518
+ scope: { content: '@', placement: '@', animation: '&', isOpen: '&' },
2519
+ templateUrl: tooltipConfig.htmlUnsafeTemplateUrl
2520
+ };
2521
+ }])
2522
+
2523
+ .directive( 'tooltipHtmlUnsafe', [ '$tooltip', function ( $tooltip ) {
2524
+ return $tooltip( 'tooltipHtmlUnsafe', 'tooltip', 'mouseenter' );
2525
+ }]);
2526
+
2527
+ /**
2528
+ * The following features are still outstanding: popup delay, animation as a
2529
+ * function, placement as a function, inside, support for more triggers than
2530
+ * just mouse enter/leave, html popovers, and selector delegatation.
2531
+ */
2532
+ angular.module( 'ui.bootstrap.popover', [ 'ui.bootstrap.tooltip' ] )
2533
+ .constant('popoverConfig', {
2534
+ templateUrl: 'template/popover/popover.html'
2535
+ })
2536
+
2537
+ .directive( 'popoverPopup', ['popoverConfig', function (popoverConfig) {
2538
+ return {
2539
+ restrict: 'EA',
2540
+ replace: true,
2541
+ scope: { title: '@', content: '@', placement: '@', animation: '&', isOpen: '&' },
2542
+ templateUrl: popoverConfig.templateUrl
2543
+ };
2544
+ }])
2545
+ .directive( 'popover', [ '$compile', '$timeout', '$parse', '$window', '$tooltip', function ( $compile, $timeout, $parse, $window, $tooltip ) {
2546
+ return $tooltip( 'popover', 'popover', 'click' );
2547
+ }]);
2548
+
2549
+
2550
+ angular.module('ui.bootstrap.progressbar', ['ui.bootstrap.transition'])
2551
+
2552
+ .constant('progressConfig', {
2553
+ animate: true,
2554
+ autoType: false,
2555
+ stackedTypes: ['success', 'info', 'warning', 'danger'],
2556
+ templateUrl: 'template/progressbar/progress.html',
2557
+ barTemplateUrl: 'template/progressbar/bar.html'
2558
+ })
2559
+
2560
+ .controller('ProgressBarController', ['$scope', '$attrs', 'progressConfig', function($scope, $attrs, progressConfig) {
2561
+
2562
+ // Whether bar transitions should be animated
2563
+ var animate = angular.isDefined($attrs.animate) ? $scope.$eval($attrs.animate) : progressConfig.animate;
2564
+ var autoType = angular.isDefined($attrs.autoType) ? $scope.$eval($attrs.autoType) : progressConfig.autoType;
2565
+ var stackedTypes = angular.isDefined($attrs.stackedTypes) ? $scope.$eval('[' + $attrs.stackedTypes + ']') : progressConfig.stackedTypes;
2566
+
2567
+ // Create bar object
2568
+ this.makeBar = function(newBar, oldBar, index) {
2569
+ var newValue = (angular.isObject(newBar)) ? newBar.value : (newBar || 0);
2570
+ var oldValue = (angular.isObject(oldBar)) ? oldBar.value : (oldBar || 0);
2571
+ var type = (angular.isObject(newBar) && angular.isDefined(newBar.type)) ? newBar.type : (autoType) ? getStackedType(index || 0) : null;
2572
+
2573
+ return {
2574
+ from: oldValue,
2575
+ to: newValue,
2576
+ type: type,
2577
+ animate: animate
2578
+ };
2579
+ };
2580
+
2581
+ function getStackedType(index) {
2582
+ return stackedTypes[index];
2583
+ }
2584
+
2585
+ this.addBar = function(bar) {
2586
+ $scope.bars.push(bar);
2587
+ $scope.totalPercent += bar.to;
2588
+ };
2589
+
2590
+ this.clearBars = function() {
2591
+ $scope.bars = [];
2592
+ $scope.totalPercent = 0;
2593
+ };
2594
+ this.clearBars();
2595
+ }])
2596
+
2597
+ .directive('progress', ['progressConfig', function(progressConfig) {
2598
+ return {
2599
+ restrict: 'EA',
2600
+ replace: true,
2601
+ controller: 'ProgressBarController',
2602
+ scope: {
2603
+ value: '=percent',
2604
+ onFull: '&',
2605
+ onEmpty: '&'
2606
+ },
2607
+ templateUrl: progressConfig.templateUrl,
2608
+ link: function(scope, element, attrs, controller) {
2609
+ scope.$watch('value', function(newValue, oldValue) {
2610
+ controller.clearBars();
2611
+
2612
+ if (angular.isArray(newValue)) {
2613
+ // Stacked progress bar
2614
+ for (var i=0, n=newValue.length; i < n; i++) {
2615
+ controller.addBar(controller.makeBar(newValue[i], oldValue[i], i));
2616
+ }
2617
+ } else {
2618
+ // Simple bar
2619
+ controller.addBar(controller.makeBar(newValue, oldValue));
2620
+ }
2621
+ }, true);
2622
+
2623
+ // Total percent listeners
2624
+ scope.$watch('totalPercent', function(value) {
2625
+ if (value >= 100) {
2626
+ scope.onFull();
2627
+ } else if (value <= 0) {
2628
+ scope.onEmpty();
2629
+ }
2630
+ }, true);
2631
+ }
2632
+ };
2633
+ }])
2634
+
2635
+ .directive('progressbar', ['$transition', 'progressConfig', function($transition, progressConfig) {
2636
+ return {
2637
+ restrict: 'EA',
2638
+ replace: true,
2639
+ scope: {
2640
+ width: '=',
2641
+ old: '=',
2642
+ type: '=',
2643
+ animate: '='
2644
+ },
2645
+ templateUrl: progressConfig.barTemplateUrl,
2646
+ link: function(scope, element) {
2647
+ scope.$watch('width', function(value) {
2648
+ if (scope.animate) {
2649
+ element.css('width', scope.old + '%');
2650
+ $transition(element, {width: value + '%'});
2651
+ } else {
2652
+ element.css('width', value + '%');
2653
+ }
2654
+ });
2655
+ }
2656
+ };
2657
+ }]);
2658
+ angular.module('ui.bootstrap.rating', [])
2659
+
2660
+ .constant('ratingConfig', {
2661
+ max: 5,
2662
+ stateOn: null,
2663
+ stateOff: null,
2664
+ templateUrl: 'template/rating/rating.html'
2665
+ })
2666
+
2667
+ .controller('RatingController', ['$scope', '$attrs', '$parse', 'ratingConfig', function($scope, $attrs, $parse, ratingConfig) {
2668
+
2669
+ this.maxRange = angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : ratingConfig.max;
2670
+ this.stateOn = angular.isDefined($attrs.stateOn) ? $scope.$parent.$eval($attrs.stateOn) : ratingConfig.stateOn;
2671
+ this.stateOff = angular.isDefined($attrs.stateOff) ? $scope.$parent.$eval($attrs.stateOff) : ratingConfig.stateOff;
2672
+
2673
+ this.createDefaultRange = function(len) {
2674
+ var defaultStateObject = {
2675
+ stateOn: this.stateOn,
2676
+ stateOff: this.stateOff
2677
+ };
2678
+
2679
+ var states = new Array(len);
2680
+ for (var i = 0; i < len; i++) {
2681
+ states[i] = defaultStateObject;
2682
+ }
2683
+ return states;
2684
+ };
2685
+
2686
+ this.normalizeRange = function(states) {
2687
+ for (var i = 0, n = states.length; i < n; i++) {
2688
+ states[i].stateOn = states[i].stateOn || this.stateOn;
2689
+ states[i].stateOff = states[i].stateOff || this.stateOff;
2690
+ }
2691
+ return states;
2692
+ };
2693
+
2694
+ // Get objects used in template
2695
+ $scope.range = angular.isDefined($attrs.ratingStates) ? this.normalizeRange(angular.copy($scope.$parent.$eval($attrs.ratingStates))): this.createDefaultRange(this.maxRange);
2696
+
2697
+ $scope.rate = function(value) {
2698
+ if ( $scope.readonly || $scope.value === value) {
2699
+ return;
2700
+ }
2701
+
2702
+ $scope.value = value;
2703
+ };
2704
+
2705
+ $scope.enter = function(value) {
2706
+ if ( ! $scope.readonly ) {
2707
+ $scope.val = value;
2708
+ }
2709
+ $scope.onHover({value: value});
2710
+ };
2711
+
2712
+ $scope.reset = function() {
2713
+ $scope.val = angular.copy($scope.value);
2714
+ $scope.onLeave();
2715
+ };
2716
+
2717
+ $scope.$watch('value', function(value) {
2718
+ $scope.val = value;
2719
+ });
2720
+
2721
+ $scope.readonly = false;
2722
+ if ($attrs.readonly) {
2723
+ $scope.$parent.$watch($parse($attrs.readonly), function(value) {
2724
+ $scope.readonly = !!value;
2725
+ });
2726
+ }
2727
+ }])
2728
+
2729
+ .directive('rating', ['ratingConfig', function(ratingConfig) {
2730
+ return {
2731
+ restrict: 'EA',
2732
+ scope: {
2733
+ value: '=',
2734
+ onHover: '&',
2735
+ onLeave: '&'
2736
+ },
2737
+ controller: 'RatingController',
2738
+ templateUrl: ratingConfig.templateUrl,
2739
+ replace: true
2740
+ };
2741
+ }]);
2742
+
2743
+
2744
+ /**
2745
+ * @ngdoc overview
2746
+ * @name ui.bootstrap.tabs
2747
+ *
2748
+ * @description
2749
+ * AngularJS version of the tabs directive.
2750
+ */
2751
+
2752
+ angular.module('ui.bootstrap.tabs', [])
2753
+
2754
+ .directive('tabs', function() {
2755
+ return function() {
2756
+ throw new Error("The `tabs` directive is deprecated, please migrate to `tabset`. Instructions can be found at http://github.com/angular-ui/bootstrap/tree/master/CHANGELOG.md");
2757
+ };
2758
+ })
2759
+
2760
+ .controller('TabsetController', ['$scope', function TabsetCtrl($scope) {
2761
+ var ctrl = this,
2762
+ tabs = ctrl.tabs = $scope.tabs = [];
2763
+
2764
+ ctrl.select = function(tab) {
2765
+ angular.forEach(tabs, function(tab) {
2766
+ tab.active = false;
2767
+ });
2768
+ tab.active = true;
2769
+ };
2770
+
2771
+ ctrl.addTab = function addTab(tab) {
2772
+ tabs.push(tab);
2773
+ if (tabs.length === 1 || tab.active) {
2774
+ ctrl.select(tab);
2775
+ }
2776
+ };
2777
+
2778
+ ctrl.removeTab = function removeTab(tab) {
2779
+ var index = tabs.indexOf(tab);
2780
+ //Select a new tab if the tab to be removed is selected
2781
+ if (tab.active && tabs.length > 1) {
2782
+ //If this is the last tab, select the previous tab. else, the next tab.
2783
+ var newActiveIndex = index == tabs.length - 1 ? index - 1 : index + 1;
2784
+ ctrl.select(tabs[newActiveIndex]);
2785
+ }
2786
+ tabs.splice(index, 1);
2787
+ };
2788
+ }])
2789
+
2790
+ .constant('tabsConfig', {
2791
+ tabsetTemplateUrl: 'template/tabs/tabset.html',
2792
+ tabTemplateUrl: 'template/tabs/tab.html',
2793
+ titlesTemplateUrl: 'template/tabs/tabset-titles.html'
2794
+ })
2795
+
2796
+ /**
2797
+ * @ngdoc directive
2798
+ * @name ui.bootstrap.tabs.directive:tabset
2799
+ * @restrict EA
2800
+ *
2801
+ * @description
2802
+ * Tabset is the outer container for the tabs directive
2803
+ *
2804
+ * @param {boolean=} vertical Whether or not to use vertical styling for the tabs.
2805
+ * @param {string=} direction What direction the tabs should be rendered. Available:
2806
+ * 'right', 'left', 'below'.
2807
+ *
2808
+ * @example
2809
+ <example module="ui.bootstrap">
2810
+ <file name="index.html">
2811
+ <tabset>
2812
+ <tab heading="Vertical Tab 1"><b>First</b> Content!</tab>
2813
+ <tab heading="Vertical Tab 2"><i>Second</i> Content!</tab>
2814
+ </tabset>
2815
+ <hr />
2816
+ <tabset vertical="true">
2817
+ <tab heading="Vertical Tab 1"><b>First</b> Vertical Content!</tab>
2818
+ <tab heading="Vertical Tab 2"><i>Second</i> Vertical Content!</tab>
2819
+ </tabset>
2820
+ </file>
2821
+ </example>
2822
+ */
2823
+ .directive('tabset', ['tabsConfig', function(tabsConfig) {
2824
+ return {
2825
+ restrict: 'EA',
2826
+ transclude: true,
2827
+ replace: true,
2828
+ require: '^tabset',
2829
+ scope: {},
2830
+ controller: 'TabsetController',
2831
+ templateUrl: tabsConfig.tabsetTemplateUrl,
2832
+ compile: function(elm, attrs, transclude) {
2833
+ return function(scope, element, attrs, tabsetCtrl) {
2834
+ scope.vertical = angular.isDefined(attrs.vertical) ? scope.$parent.$eval(attrs.vertical) : false;
2835
+ scope.type = angular.isDefined(attrs.type) ? scope.$parent.$eval(attrs.type) : 'tabs';
2836
+ scope.direction = angular.isDefined(attrs.direction) ? scope.$parent.$eval(attrs.direction) : 'top';
2837
+ scope.tabsAbove = (scope.direction != 'below');
2838
+ tabsetCtrl.$scope = scope;
2839
+ tabsetCtrl.$transcludeFn = transclude;
2840
+ };
2841
+ }
2842
+ };
2843
+ }])
2844
+
2845
+ /**
2846
+ * @ngdoc directive
2847
+ * @name ui.bootstrap.tabs.directive:tab
2848
+ * @restrict EA
2849
+ *
2850
+ * @param {string=} heading The visible heading, or title, of the tab. Set HTML headings with {@link ui.bootstrap.tabs.directive:tabHeading tabHeading}.
2851
+ * @param {string=} select An expression to evaluate when the tab is selected.
2852
+ * @param {boolean=} active A binding, telling whether or not this tab is selected.
2853
+ * @param {boolean=} disabled A binding, telling whether or not this tab is disabled.
2854
+ *
2855
+ * @description
2856
+ * Creates a tab with a heading and content. Must be placed within a {@link ui.bootstrap.tabs.directive:tabset tabset}.
2857
+ *
2858
+ * @example
2859
+ <example module="ui.bootstrap">
2860
+ <file name="index.html">
2861
+ <div ng-controller="TabsDemoCtrl">
2862
+ <button class="btn btn-small" ng-click="items[0].active = true">
2863
+ Select item 1, using active binding
2864
+ </button>
2865
+ <button class="btn btn-small" ng-click="items[1].disabled = !items[1].disabled">
2866
+ Enable/disable item 2, using disabled binding
2867
+ </button>
2868
+ <br />
2869
+ <tabset>
2870
+ <tab heading="Tab 1">First Tab</tab>
2871
+ <tab select="alertMe()">
2872
+ <tab-heading><i class="icon-bell"></i> Alert me!</tab-heading>
2873
+ Second Tab, with alert callback and html heading!
2874
+ </tab>
2875
+ <tab ng-repeat="item in items"
2876
+ heading="{{item.title}}"
2877
+ disabled="item.disabled"
2878
+ active="item.active">
2879
+ {{item.content}}
2880
+ </tab>
2881
+ </tabset>
2882
+ </div>
2883
+ </file>
2884
+ <file name="script.js">
2885
+ function TabsDemoCtrl($scope) {
2886
+ $scope.items = [
2887
+ { title:"Dynamic Title 1", content:"Dynamic Item 0" },
2888
+ { title:"Dynamic Title 2", content:"Dynamic Item 1", disabled: true }
2889
+ ];
2890
+
2891
+ $scope.alertMe = function() {
2892
+ setTimeout(function() {
2893
+ alert("You've selected the alert tab!");
2894
+ });
2895
+ };
2896
+ };
2897
+ </file>
2898
+ </example>
2899
+ */
2900
+
2901
+ /**
2902
+ * @ngdoc directive
2903
+ * @name ui.bootstrap.tabs.directive:tabHeading
2904
+ * @restrict EA
2905
+ *
2906
+ * @description
2907
+ * Creates an HTML heading for a {@link ui.bootstrap.tabs.directive:tab tab}. Must be placed as a child of a tab element.
2908
+ *
2909
+ * @example
2910
+ <example module="ui.bootstrap">
2911
+ <file name="index.html">
2912
+ <tabset>
2913
+ <tab>
2914
+ <tab-heading><b>HTML</b> in my titles?!</tab-heading>
2915
+ And some content, too!
2916
+ </tab>
2917
+ <tab>
2918
+ <tab-heading><i class="icon-heart"></i> Icon heading?!?</tab-heading>
2919
+ That's right.
2920
+ </tab>
2921
+ </tabset>
2922
+ </file>
2923
+ </example>
2924
+ */
2925
+ .directive('tab', ['$parse', 'tabsConfig', function($parse, tabsConfig) {
2926
+ return {
2927
+ require: '^tabset',
2928
+ restrict: 'EA',
2929
+ replace: true,
2930
+ templateUrl: tabsConfig.tabTemplateUrl,
2931
+ transclude: true,
2932
+ scope: {
2933
+ heading: '@',
2934
+ onSelect: '&select', //This callback is called in contentHeadingTransclude
2935
+ //once it inserts the tab's content into the dom
2936
+ onDeselect: '&deselect'
2937
+ },
2938
+ controller: function() {
2939
+ //Empty controller so other directives can require being 'under' a tab
2940
+ },
2941
+ compile: function(elm, attrs, transclude) {
2942
+ return function postLink(scope, elm, attrs, tabsetCtrl) {
2943
+ var getActive, setActive;
2944
+ if (attrs.active) {
2945
+ getActive = $parse(attrs.active);
2946
+ setActive = getActive.assign;
2947
+ scope.$parent.$watch(getActive, function updateActive(value, oldVal) {
2948
+ // Avoid re-initializing scope.active as it is already initialized
2949
+ // below. (watcher is called async during init with value ===
2950
+ // oldVal)
2951
+ if (value !== oldVal) {
2952
+ scope.active = !!value;
2953
+ }
2954
+ });
2955
+ scope.active = getActive(scope.$parent);
2956
+ } else {
2957
+ setActive = getActive = angular.noop;
2958
+ }
2959
+
2960
+ scope.$watch('active', function(active) {
2961
+ // Note this watcher also initializes and assigns scope.active to the
2962
+ // attrs.active expression.
2963
+ setActive(scope.$parent, active);
2964
+ if (active) {
2965
+ tabsetCtrl.select(scope);
2966
+ scope.onSelect();
2967
+ } else {
2968
+ scope.onDeselect();
2969
+ }
2970
+ });
2971
+
2972
+ scope.disabled = false;
2973
+ if ( attrs.disabled ) {
2974
+ scope.$parent.$watch($parse(attrs.disabled), function(value) {
2975
+ scope.disabled = !! value;
2976
+ });
2977
+ }
2978
+
2979
+ scope.select = function() {
2980
+ if ( ! scope.disabled ) {
2981
+ scope.active = true;
2982
+ }
2983
+ };
2984
+
2985
+ tabsetCtrl.addTab(scope);
2986
+ scope.$on('$destroy', function() {
2987
+ tabsetCtrl.removeTab(scope);
2988
+ });
2989
+
2990
+
2991
+ //We need to transclude later, once the content container is ready.
2992
+ //when this link happens, we're inside a tab heading.
2993
+ scope.$transcludeFn = transclude;
2994
+ };
2995
+ }
2996
+ };
2997
+ }])
2998
+
2999
+ .directive('tabHeadingTransclude', [function() {
3000
+ return {
3001
+ restrict: 'A',
3002
+ require: '^tab',
3003
+ link: function(scope, elm, attrs, tabCtrl) {
3004
+ scope.$watch('headingElement', function updateHeadingElement(heading) {
3005
+ if (heading) {
3006
+ elm.html('');
3007
+ elm.append(heading);
3008
+ }
3009
+ });
3010
+ }
3011
+ };
3012
+ }])
3013
+
3014
+ .directive('tabContentTransclude', function() {
3015
+ return {
3016
+ restrict: 'A',
3017
+ require: '^tabset',
3018
+ link: function(scope, elm, attrs) {
3019
+ var tab = scope.$eval(attrs.tabContentTransclude);
3020
+
3021
+ //Now our tab is ready to be transcluded: both the tab heading area
3022
+ //and the tab content area are loaded. Transclude 'em both.
3023
+ tab.$transcludeFn(tab.$parent, function(contents) {
3024
+ angular.forEach(contents, function(node) {
3025
+ if (isTabHeading(node)) {
3026
+ //Let tabHeadingTransclude know.
3027
+ tab.headingElement = node;
3028
+ } else {
3029
+ elm.append(node);
3030
+ }
3031
+ });
3032
+ });
3033
+ }
3034
+ };
3035
+ function isTabHeading(node) {
3036
+ return node.tagName && (
3037
+ node.hasAttribute('tab-heading') ||
3038
+ node.hasAttribute('data-tab-heading') ||
3039
+ node.tagName.toLowerCase() === 'tab-heading' ||
3040
+ node.tagName.toLowerCase() === 'data-tab-heading'
3041
+ );
3042
+ }
3043
+ })
3044
+
3045
+ .directive('tabsetTitles', ['tabsConfig', function(tabsConfig) {
3046
+ return {
3047
+ restrict: 'A',
3048
+ require: '^tabset',
3049
+ templateUrl: tabsConfig.titlesTemplateUrl,
3050
+ replace: true,
3051
+ link: function(scope, elm, attrs, tabsetCtrl) {
3052
+ if (!scope.$eval(attrs.tabsetTitles)) {
3053
+ elm.remove();
3054
+ } else {
3055
+ //now that tabs location has been decided, transclude the tab titles in
3056
+ tabsetCtrl.$transcludeFn(tabsetCtrl.$scope.$parent, function(node) {
3057
+ elm.append(node);
3058
+ });
3059
+ }
3060
+ }
3061
+ };
3062
+ }]);
3063
+
3064
+ angular.module('ui.bootstrap.timepicker', [])
3065
+
3066
+ .constant('timepickerConfig', {
3067
+ hourStep: 1,
3068
+ minuteStep: 1,
3069
+ showMeridian: true,
3070
+ meridians: ['AM', 'PM'],
3071
+ readonlyInput: false,
3072
+ mousewheel: true,
3073
+ templateUrl: 'template/timepicker/timepicker.html'
3074
+ })
3075
+
3076
+ .directive('timepicker', ['$parse', '$log', 'timepickerConfig', function ($parse, $log, timepickerConfig) {
3077
+ return {
3078
+ restrict: 'EA',
3079
+ require:'?^ngModel',
3080
+ replace: true,
3081
+ scope: {},
3082
+ templateUrl: timepickerConfig.templateUrl,
3083
+ link: function(scope, element, attrs, ngModel) {
3084
+ if ( !ngModel ) {
3085
+ return; // do nothing if no ng-model
3086
+ }
3087
+
3088
+ var selected = new Date(), meridians = timepickerConfig.meridians;
3089
+
3090
+ var hourStep = timepickerConfig.hourStep;
3091
+ if (attrs.hourStep) {
3092
+ scope.$parent.$watch($parse(attrs.hourStep), function(value) {
3093
+ hourStep = parseInt(value, 10);
3094
+ });
3095
+ }
3096
+
3097
+ var minuteStep = timepickerConfig.minuteStep;
3098
+ if (attrs.minuteStep) {
3099
+ scope.$parent.$watch($parse(attrs.minuteStep), function(value) {
3100
+ minuteStep = parseInt(value, 10);
3101
+ });
3102
+ }
3103
+
3104
+ // 12H / 24H mode
3105
+ scope.showMeridian = timepickerConfig.showMeridian;
3106
+ if (attrs.showMeridian) {
3107
+ scope.$parent.$watch($parse(attrs.showMeridian), function(value) {
3108
+ scope.showMeridian = !!value;
3109
+
3110
+ if ( ngModel.$error.time ) {
3111
+ // Evaluate from template
3112
+ var hours = getHoursFromTemplate(), minutes = getMinutesFromTemplate();
3113
+ if (angular.isDefined( hours ) && angular.isDefined( minutes )) {
3114
+ selected.setHours( hours );
3115
+ refresh();
3116
+ }
3117
+ } else {
3118
+ updateTemplate();
3119
+ }
3120
+ });
3121
+ }
3122
+
3123
+ // Get scope.hours in 24H mode if valid
3124
+ function getHoursFromTemplate ( ) {
3125
+ var hours = parseInt( scope.hours, 10 );
3126
+ var valid = ( scope.showMeridian ) ? (hours > 0 && hours < 13) : (hours >= 0 && hours < 24);
3127
+ if ( !valid ) {
3128
+ return undefined;
3129
+ }
3130
+
3131
+ if ( scope.showMeridian ) {
3132
+ if ( hours === 12 ) {
3133
+ hours = 0;
3134
+ }
3135
+ if ( scope.meridian === meridians[1] ) {
3136
+ hours = hours + 12;
3137
+ }
3138
+ }
3139
+ return hours;
3140
+ }
3141
+
3142
+ function getMinutesFromTemplate() {
3143
+ var minutes = parseInt(scope.minutes, 10);
3144
+ return ( minutes >= 0 && minutes < 60 ) ? minutes : undefined;
3145
+ }
3146
+
3147
+ function pad( value ) {
3148
+ return ( angular.isDefined(value) && value.toString().length < 2 ) ? '0' + value : value;
3149
+ }
3150
+
3151
+ // Input elements
3152
+ var inputs = element.find('input'), hoursInputEl = inputs.eq(0), minutesInputEl = inputs.eq(1);
3153
+
3154
+ // Respond on mousewheel spin
3155
+ var mousewheel = (angular.isDefined(attrs.mousewheel)) ? scope.$eval(attrs.mousewheel) : timepickerConfig.mousewheel;
3156
+ if ( mousewheel ) {
3157
+
3158
+ var isScrollingUp = function(e) {
3159
+ if (e.originalEvent) {
3160
+ e = e.originalEvent;
3161
+ }
3162
+ //pick correct delta variable depending on event
3163
+ var delta = (e.wheelDelta) ? e.wheelDelta : -e.deltaY;
3164
+ return (e.detail || delta > 0);
3165
+ };
3166
+
3167
+ hoursInputEl.bind('mousewheel wheel', function(e) {
3168
+ scope.$apply( (isScrollingUp(e)) ? scope.incrementHours() : scope.decrementHours() );
3169
+ e.preventDefault();
3170
+ });
3171
+
3172
+ minutesInputEl.bind('mousewheel wheel', function(e) {
3173
+ scope.$apply( (isScrollingUp(e)) ? scope.incrementMinutes() : scope.decrementMinutes() );
3174
+ e.preventDefault();
3175
+ });
3176
+ }
3177
+
3178
+ scope.readonlyInput = (angular.isDefined(attrs.readonlyInput)) ? scope.$eval(attrs.readonlyInput) : timepickerConfig.readonlyInput;
3179
+ if ( ! scope.readonlyInput ) {
3180
+
3181
+ var invalidate = function(invalidHours, invalidMinutes) {
3182
+ ngModel.$setViewValue( null );
3183
+ ngModel.$setValidity('time', false);
3184
+ if (angular.isDefined(invalidHours)) {
3185
+ scope.invalidHours = invalidHours;
3186
+ }
3187
+ if (angular.isDefined(invalidMinutes)) {
3188
+ scope.invalidMinutes = invalidMinutes;
3189
+ }
3190
+ };
3191
+
3192
+ scope.updateHours = function() {
3193
+ var hours = getHoursFromTemplate();
3194
+
3195
+ if ( angular.isDefined(hours) ) {
3196
+ selected.setHours( hours );
3197
+ refresh( 'h' );
3198
+ } else {
3199
+ invalidate(true);
3200
+ }
3201
+ };
3202
+
3203
+ hoursInputEl.bind('blur', function(e) {
3204
+ if ( !scope.validHours && scope.hours < 10) {
3205
+ scope.$apply( function() {
3206
+ scope.hours = pad( scope.hours );
3207
+ });
3208
+ }
3209
+ });
3210
+
3211
+ scope.updateMinutes = function() {
3212
+ var minutes = getMinutesFromTemplate();
3213
+
3214
+ if ( angular.isDefined(minutes) ) {
3215
+ selected.setMinutes( minutes );
3216
+ refresh( 'm' );
3217
+ } else {
3218
+ invalidate(undefined, true);
3219
+ }
3220
+ };
3221
+
3222
+ minutesInputEl.bind('blur', function(e) {
3223
+ if ( !scope.invalidMinutes && scope.minutes < 10 ) {
3224
+ scope.$apply( function() {
3225
+ scope.minutes = pad( scope.minutes );
3226
+ });
3227
+ }
3228
+ });
3229
+ } else {
3230
+ scope.updateHours = angular.noop;
3231
+ scope.updateMinutes = angular.noop;
3232
+ }
3233
+
3234
+ ngModel.$render = function() {
3235
+ var date = ngModel.$modelValue ? new Date( ngModel.$modelValue ) : null;
3236
+
3237
+ if ( isNaN(date) ) {
3238
+ ngModel.$setValidity('time', false);
3239
+ $log.error('Timepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.');
3240
+ } else {
3241
+ if ( date ) {
3242
+ selected = date;
3243
+ }
3244
+ makeValid();
3245
+ updateTemplate();
3246
+ }
3247
+ };
3248
+
3249
+ // Call internally when we know that model is valid.
3250
+ function refresh( keyboardChange ) {
3251
+ makeValid();
3252
+ ngModel.$setViewValue( new Date(selected) );
3253
+ updateTemplate( keyboardChange );
3254
+ }
3255
+
3256
+ function makeValid() {
3257
+ ngModel.$setValidity('time', true);
3258
+ scope.invalidHours = false;
3259
+ scope.invalidMinutes = false;
3260
+ }
3261
+
3262
+ function updateTemplate( keyboardChange ) {
3263
+ var hours = selected.getHours(), minutes = selected.getMinutes();
3264
+
3265
+ if ( scope.showMeridian ) {
3266
+ hours = ( hours === 0 || hours === 12 ) ? 12 : hours % 12; // Convert 24 to 12 hour system
3267
+ }
3268
+ scope.hours = keyboardChange === 'h' ? hours : pad(hours);
3269
+ scope.minutes = keyboardChange === 'm' ? minutes : pad(minutes);
3270
+ scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1];
3271
+ }
3272
+
3273
+ function addMinutes( minutes ) {
3274
+ var dt = new Date( selected.getTime() + minutes * 60000 );
3275
+ selected.setHours( dt.getHours(), dt.getMinutes() );
3276
+ refresh();
3277
+ }
3278
+
3279
+ scope.incrementHours = function() {
3280
+ addMinutes( hourStep * 60 );
3281
+ };
3282
+ scope.decrementHours = function() {
3283
+ addMinutes( - hourStep * 60 );
3284
+ };
3285
+ scope.incrementMinutes = function() {
3286
+ addMinutes( minuteStep );
3287
+ };
3288
+ scope.decrementMinutes = function() {
3289
+ addMinutes( - minuteStep );
3290
+ };
3291
+ scope.toggleMeridian = function() {
3292
+ addMinutes( 12 * 60 * (( selected.getHours() < 12 ) ? 1 : -1) );
3293
+ };
3294
+ }
3295
+ };
3296
+ }]);
3297
+
3298
+ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap.bindHtml'])
3299
+
3300
+ /**
3301
+ * A helper service that can parse typeahead's syntax (string provided by users)
3302
+ * Extracted to a separate service for ease of unit testing
3303
+ */
3304
+ .factory('typeaheadParser', ['$parse', function ($parse) {
3305
+
3306
+ // 00000111000000000000022200000000000000003333333333333330000000000044000
3307
+ var TYPEAHEAD_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/;
3308
+
3309
+ return {
3310
+ parse:function (input) {
3311
+
3312
+ var match = input.match(TYPEAHEAD_REGEXP), modelMapper, viewMapper, source;
3313
+ if (!match) {
3314
+ throw new Error(
3315
+ "Expected typeahead specification in form of '_modelValue_ (as _label_)? for _item_ in _collection_'" +
3316
+ " but got '" + input + "'.");
3317
+ }
3318
+
3319
+ return {
3320
+ itemName:match[3],
3321
+ source:$parse(match[4]),
3322
+ viewMapper:$parse(match[2] || match[1]),
3323
+ modelMapper:$parse(match[1])
3324
+ };
3325
+ }
3326
+ };
3327
+ }])
3328
+
3329
+ .directive('typeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$position', 'typeaheadParser',
3330
+ function ($compile, $parse, $q, $timeout, $document, $position, typeaheadParser) {
3331
+
3332
+ var HOT_KEYS = [9, 13, 27, 38, 40];
3333
+
3334
+ return {
3335
+ require:'ngModel',
3336
+ link:function (originalScope, element, attrs, modelCtrl) {
3337
+
3338
+ //SUPPORTED ATTRIBUTES (OPTIONS)
3339
+
3340
+ //minimal no of characters that needs to be entered before typeahead kicks-in
3341
+ var minSearch = originalScope.$eval(attrs.typeaheadMinLength) || 1;
3342
+
3343
+ //minimal wait time after last character typed before typehead kicks-in
3344
+ var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0;
3345
+
3346
+ //should it restrict model values to the ones selected from the popup only?
3347
+ var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false;
3348
+
3349
+ //binding to a variable that indicates if matches are being retrieved asynchronously
3350
+ var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop;
3351
+
3352
+ //a callback executed when a match is selected
3353
+ var onSelectCallback = $parse(attrs.typeaheadOnSelect);
3354
+
3355
+ var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined;
3356
+
3357
+ //INTERNAL VARIABLES
3358
+
3359
+ //model setter executed upon match selection
3360
+ var $setModelValue = $parse(attrs.ngModel).assign;
3361
+
3362
+ //expressions used by typeahead
3363
+ var parserResult = typeaheadParser.parse(attrs.typeahead);
3364
+
3365
+
3366
+ //pop-up element used to display matches
3367
+ var popUpEl = angular.element('<typeahead-popup></typeahead-popup>');
3368
+ popUpEl.attr({
3369
+ matches: 'matches',
3370
+ active: 'activeIdx',
3371
+ select: 'select(activeIdx)',
3372
+ query: 'query',
3373
+ position: 'position'
3374
+ });
3375
+ //custom item template
3376
+ if (angular.isDefined(attrs.typeaheadTemplateUrl)) {
3377
+ popUpEl.attr('template-url', attrs.typeaheadTemplateUrl);
3378
+ }
3379
+
3380
+ //create a child scope for the typeahead directive so we are not polluting original scope
3381
+ //with typeahead-specific data (matches, query etc.)
3382
+ var scope = originalScope.$new();
3383
+ originalScope.$on('$destroy', function(){
3384
+ scope.$destroy();
3385
+ });
3386
+
3387
+ var resetMatches = function() {
3388
+ scope.matches = [];
3389
+ scope.activeIdx = -1;
3390
+ };
3391
+
3392
+ var getMatchesAsync = function(inputValue) {
3393
+
3394
+ var locals = {$viewValue: inputValue};
3395
+ isLoadingSetter(originalScope, true);
3396
+ $q.when(parserResult.source(scope, locals)).then(function(matches) {
3397
+
3398
+ //it might happen that several async queries were in progress if a user were typing fast
3399
+ //but we are interested only in responses that correspond to the current view value
3400
+ if (inputValue === modelCtrl.$viewValue) {
3401
+ if (matches.length > 0) {
3402
+
3403
+ scope.activeIdx = 0;
3404
+ scope.matches.length = 0;
3405
+
3406
+ //transform labels
3407
+ for(var i=0; i<matches.length; i++) {
3408
+ locals[parserResult.itemName] = matches[i];
3409
+ scope.matches.push({
3410
+ label: parserResult.viewMapper(scope, locals),
3411
+ model: matches[i]
3412
+ });
3413
+ }
3414
+
3415
+ scope.query = inputValue;
3416
+ //position pop-up with matches - we need to re-calculate its position each time we are opening a window
3417
+ //with matches as a pop-up might be absolute-positioned and position of an input might have changed on a page
3418
+ //due to other elements being rendered
3419
+ scope.position = $position.position(element);
3420
+ scope.position.top = scope.position.top + element.prop('offsetHeight');
3421
+
3422
+ } else {
3423
+ resetMatches();
3424
+ }
3425
+ isLoadingSetter(originalScope, false);
3426
+ }
3427
+ }, function(){
3428
+ resetMatches();
3429
+ isLoadingSetter(originalScope, false);
3430
+ });
3431
+ };
3432
+
3433
+ resetMatches();
3434
+
3435
+ //we need to propagate user's query so we can higlight matches
3436
+ scope.query = undefined;
3437
+
3438
+ //Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later
3439
+ var timeoutPromise;
3440
+
3441
+ //plug into $parsers pipeline to open a typeahead on view changes initiated from DOM
3442
+ //$parsers kick-in on all the changes coming from the view as well as manually triggered by $setViewValue
3443
+ modelCtrl.$parsers.unshift(function (inputValue) {
3444
+
3445
+ resetMatches();
3446
+ if (inputValue && inputValue.length >= minSearch) {
3447
+ if (waitTime > 0) {
3448
+ if (timeoutPromise) {
3449
+ $timeout.cancel(timeoutPromise);//cancel previous timeout
3450
+ }
3451
+ timeoutPromise = $timeout(function () {
3452
+ getMatchesAsync(inputValue);
3453
+ }, waitTime);
3454
+ } else {
3455
+ getMatchesAsync(inputValue);
3456
+ }
3457
+ }
3458
+
3459
+ if (isEditable) {
3460
+ return inputValue;
3461
+ } else {
3462
+ modelCtrl.$setValidity('editable', false);
3463
+ return undefined;
3464
+ }
3465
+ });
3466
+
3467
+ modelCtrl.$formatters.push(function (modelValue) {
3468
+
3469
+ var candidateViewValue, emptyViewValue;
3470
+ var locals = {};
3471
+
3472
+ if (inputFormatter) {
3473
+
3474
+ locals['$model'] = modelValue;
3475
+ return inputFormatter(originalScope, locals);
3476
+
3477
+ } else {
3478
+
3479
+ //it might happen that we don't have enough info to properly render input value
3480
+ //we need to check for this situation and simply return model value if we can't apply custom formatting
3481
+ locals[parserResult.itemName] = modelValue;
3482
+ candidateViewValue = parserResult.viewMapper(originalScope, locals);
3483
+ locals[parserResult.itemName] = undefined;
3484
+ emptyViewValue = parserResult.viewMapper(originalScope, locals);
3485
+
3486
+ return candidateViewValue!== emptyViewValue ? candidateViewValue : modelValue;
3487
+ }
3488
+ });
3489
+
3490
+ scope.select = function (activeIdx) {
3491
+ //called from within the $digest() cycle
3492
+ var locals = {};
3493
+ var model, item;
3494
+
3495
+ locals[parserResult.itemName] = item = scope.matches[activeIdx].model;
3496
+ model = parserResult.modelMapper(originalScope, locals);
3497
+ $setModelValue(originalScope, model);
3498
+ modelCtrl.$setValidity('editable', true);
3499
+
3500
+ onSelectCallback(originalScope, {
3501
+ $item: item,
3502
+ $model: model,
3503
+ $label: parserResult.viewMapper(originalScope, locals)
3504
+ });
3505
+
3506
+ resetMatches();
3507
+
3508
+ //return focus to the input element if a mach was selected via a mouse click event
3509
+ element[0].focus();
3510
+ };
3511
+
3512
+ //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27)
3513
+ element.bind('keydown', function (evt) {
3514
+
3515
+ //typeahead is open and an "interesting" key was pressed
3516
+ if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) {
3517
+ return;
3518
+ }
3519
+
3520
+ evt.preventDefault();
3521
+
3522
+ if (evt.which === 40) {
3523
+ scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length;
3524
+ scope.$digest();
3525
+
3526
+ } else if (evt.which === 38) {
3527
+ scope.activeIdx = (scope.activeIdx ? scope.activeIdx : scope.matches.length) - 1;
3528
+ scope.$digest();
3529
+
3530
+ } else if (evt.which === 13 || evt.which === 9) {
3531
+ scope.$apply(function () {
3532
+ scope.select(scope.activeIdx);
3533
+ });
3534
+
3535
+ } else if (evt.which === 27) {
3536
+ evt.stopPropagation();
3537
+
3538
+ resetMatches();
3539
+ scope.$digest();
3540
+ }
3541
+ });
3542
+
3543
+ // Keep reference to click handler to unbind it.
3544
+ var dismissClickHandler = function (evt) {
3545
+ if (element[0] !== evt.target) {
3546
+ resetMatches();
3547
+ scope.$digest();
3548
+ }
3549
+ };
3550
+
3551
+ $document.bind('click', dismissClickHandler);
3552
+
3553
+ originalScope.$on('$destroy', function(){
3554
+ $document.unbind('click', dismissClickHandler);
3555
+ });
3556
+
3557
+ element.after($compile(popUpEl)(scope));
3558
+ }
3559
+ };
3560
+
3561
+ }])
3562
+
3563
+ .constant('typeaheadConfig', {
3564
+ popupTemplateUrl: 'template/typeahead/typeahead-popup.html',
3565
+ matchTemplateUrl: 'template/typeahead/typeahead-match.html'
3566
+ })
3567
+
3568
+ .directive('typeaheadPopup', ['typeaheadConfig', function (typeaheadConfig) {
3569
+ return {
3570
+ restrict:'E',
3571
+ scope:{
3572
+ matches:'=',
3573
+ query:'=',
3574
+ active:'=',
3575
+ position:'=',
3576
+ select:'&'
3577
+ },
3578
+ replace:true,
3579
+ templateUrl: typeaheadConfig.popupTemplateUrl,
3580
+ link:function (scope, element, attrs) {
3581
+
3582
+ scope.templateUrl = attrs.templateUrl;
3583
+
3584
+ scope.isOpen = function () {
3585
+ return scope.matches.length > 0;
3586
+ };
3587
+
3588
+ scope.isActive = function (matchIdx) {
3589
+ return scope.active == matchIdx;
3590
+ };
3591
+
3592
+ scope.selectActive = function (matchIdx) {
3593
+ scope.active = matchIdx;
3594
+ };
3595
+
3596
+ scope.selectMatch = function (activeIdx) {
3597
+ scope.select({activeIdx:activeIdx});
3598
+ };
3599
+ }
3600
+ };
3601
+ }])
3602
+
3603
+ .directive('typeaheadMatch', ['$http', '$templateCache', '$compile', '$parse', 'typeaheadConfig', function ($http, $templateCache, $compile, $parse, typeaheadConfig) {
3604
+ return {
3605
+ restrict:'E',
3606
+ scope:{
3607
+ index:'=',
3608
+ match:'=',
3609
+ query:'='
3610
+ },
3611
+ link:function (scope, element, attrs) {
3612
+ var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || typeaheadConfig.matchTemplateUrl;
3613
+ $http.get(tplUrl, {cache: $templateCache}).success(function(tplContent){
3614
+ element.replaceWith($compile(tplContent.trim())(scope));
3615
+ });
3616
+ }
3617
+ };
3618
+ }])
3619
+
3620
+ .filter('typeaheadHighlight', function() {
3621
+
3622
+ function escapeRegexp(queryToEscape) {
3623
+ return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1");
3624
+ }
3625
+
3626
+ return function(matchItem, query) {
3627
+ return query ? matchItem.replace(new RegExp(escapeRegexp(query), 'gi'), '<strong>$&</strong>') : matchItem;
3628
+ };
3629
+ });