laces 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (106) hide show
  1. data/CONTRIBUTING.md +38 -0
  2. data/Gemfile +3 -0
  3. data/Gemfile.lock +122 -0
  4. data/LICENSE +21 -0
  5. data/README.md +5 -0
  6. data/Rakefile +8 -0
  7. data/bin/laces +16 -0
  8. data/features/creating_a_heroku_app.feature +9 -0
  9. data/features/rake_clean.feature +21 -0
  10. data/features/skipping_clearance.feature +13 -0
  11. data/features/step_definitions/gem_steps.rb +5 -0
  12. data/features/step_definitions/heroku_steps.rb +3 -0
  13. data/features/step_definitions/shell_steps.rb +55 -0
  14. data/features/support/bin/heroku +5 -0
  15. data/features/support/env.rb +15 -0
  16. data/features/support/fake_heroku.rb +21 -0
  17. data/laces-0.0.1.gem +0 -0
  18. data/laces.gemspec +35 -0
  19. data/lib/laces/actions.rb +35 -0
  20. data/lib/laces/app_builder.rb +237 -0
  21. data/lib/laces/generators/app_generator.rb +111 -0
  22. data/lib/laces/version.rb +3 -0
  23. data/templates/.DS_Store +0 -0
  24. data/templates/Gemfile_template +76 -0
  25. data/templates/HEROKU_README.md +66 -0
  26. data/templates/Procfile +1 -0
  27. data/templates/README.md +81 -0
  28. data/templates/app/assets/imgs/glyphicons-halflings-white.png +0 -0
  29. data/templates/app/assets/imgs/glyphicons-halflings.png +0 -0
  30. data/templates/app/assets/javascripts/admin.coffee +20 -0
  31. data/templates/app/assets/javascripts/application.coffee +21 -0
  32. data/templates/app/assets/javascripts/lib/actinology.coffee +47 -0
  33. data/templates/app/assets/javascripts/lib/analytics.js +11 -0
  34. data/templates/app/assets/javascripts/lib/auth_token_sync.js +17 -0
  35. data/templates/app/assets/javascripts/lib/backbone-ui.js +2455 -0
  36. data/templates/app/assets/javascripts/lib/backbone.coffee +27 -0
  37. data/templates/app/assets/javascripts/lib/backbone/collection.js +249 -0
  38. data/templates/app/assets/javascripts/lib/backbone/events.js +64 -0
  39. data/templates/app/assets/javascripts/lib/backbone/helpers.js +68 -0
  40. data/templates/app/assets/javascripts/lib/backbone/history.js +144 -0
  41. data/templates/app/assets/javascripts/lib/backbone/model.js +291 -0
  42. data/templates/app/assets/javascripts/lib/backbone/router.coffee +45 -0
  43. data/templates/app/assets/javascripts/lib/backbone/sync.coffee +38 -0
  44. data/templates/app/assets/javascripts/lib/backbone/view.js +150 -0
  45. data/templates/app/assets/javascripts/lib/backbone_extended.coffee +276 -0
  46. data/templates/app/assets/javascripts/lib/bootstrap.js +1836 -0
  47. data/templates/app/assets/javascripts/lib/inflection.js +658 -0
  48. data/templates/app/assets/javascripts/lib/jquery-ui.js +343 -0
  49. data/templates/app/assets/javascripts/lib/jquery.hotkeys.js +102 -0
  50. data/templates/app/assets/javascripts/lib/jquery.js +4 -0
  51. data/templates/app/assets/javascripts/lib/milk.js.coffee +265 -0
  52. data/templates/app/assets/javascripts/lib/raw.js +143 -0
  53. data/templates/app/assets/javascripts/lib/strftime.js +732 -0
  54. data/templates/app/assets/javascripts/lib/throttle-debounce.js +251 -0
  55. data/templates/app/assets/javascripts/lib/timeago.js +148 -0
  56. data/templates/app/assets/javascripts/lib/underscore.js +28 -0
  57. data/templates/app/assets/styles/application.sass +21 -0
  58. data/templates/app/assets/styles/layouts/default.sass +15 -0
  59. data/templates/app/assets/styles/layouts/footer.sass +0 -0
  60. data/templates/app/assets/styles/layouts/forms.sass +34 -0
  61. data/templates/app/assets/styles/layouts/header.sass +0 -0
  62. data/templates/app/assets/styles/layouts/navigation.sass +0 -0
  63. data/templates/app/assets/styles/lib/backbone-ui.css +580 -0
  64. data/templates/app/assets/styles/lib/bootstrap.sass +4248 -0
  65. data/templates/app/assets/styles/pages/home.sass +0 -0
  66. data/templates/app/assets/styles/sessions/new.sass +0 -0
  67. data/templates/app/assets/styles/users/activate.sass +0 -0
  68. data/templates/app/assets/styles/users/new.sass +0 -0
  69. data/templates/app/assets/styles/users/suspended.sass +0 -0
  70. data/templates/app/controllers/app_controller.rb +14 -0
  71. data/templates/app/controllers/pages_controller.rb +2 -0
  72. data/templates/app/controllers/sessions_controller.rb +2 -0
  73. data/templates/app/controllers/templating_controller.rb +31 -0
  74. data/templates/app/controllers/users_controller.rb +2 -0
  75. data/templates/app/helpers/app_helper.rb +94 -0
  76. data/templates/app/helpers/users_helper.rb +53 -0
  77. data/templates/app/models/user.rb +9 -0
  78. data/templates/app/views/devise/confirmations/new.haml +9 -0
  79. data/templates/app/views/devise/passwords/edit.haml +11 -0
  80. data/templates/app/views/devise/passwords/new.haml +9 -0
  81. data/templates/app/views/devise/registrations/edit.haml +13 -0
  82. data/templates/app/views/devise/registrations/new.haml +8 -0
  83. data/templates/app/views/devise/sessions/_form.haml +7 -0
  84. data/templates/app/views/devise/sessions/new.haml +5 -0
  85. data/templates/app/views/devise/unlocks/new.haml +7 -0
  86. data/templates/app/views/layouts/_ascii.haml +0 -0
  87. data/templates/app/views/layouts/_column.haml +0 -0
  88. data/templates/app/views/layouts/_content.haml +1 -0
  89. data/templates/app/views/layouts/_extra.haml +0 -0
  90. data/templates/app/views/layouts/_footer.haml +9 -0
  91. data/templates/app/views/layouts/_head.haml +13 -0
  92. data/templates/app/views/layouts/_header.haml +6 -0
  93. data/templates/app/views/layouts/application.html.haml +16 -0
  94. data/templates/app/views/pages/home.haml +1 -0
  95. data/templates/config/app.yml +29 -0
  96. data/templates/config/application.erb +28 -0
  97. data/templates/config/database.yml +32 -0
  98. data/templates/config/initializers/devise.rb +232 -0
  99. data/templates/config/initializers/rabl_init.rb +4 -0
  100. data/templates/config/initializers/setup_mail.rb +13 -0
  101. data/templates/config/initializers/wrap_parameters.rb +12 -0
  102. data/templates/db/migrate/user_migration.rb +36 -0
  103. data/templates/laces_gitignore +9 -0
  104. data/templates/lib/development_mail_interceptor.rb +7 -0
  105. data/templates/lib/templating.rb +40 -0
  106. metadata +225 -0
@@ -0,0 +1 @@
1
+ web: bundle exec thin start -p $PORT
@@ -0,0 +1,81 @@
1
+ Rails app
2
+ =========
3
+
4
+ This is a Rails 3.1 app running on Ruby 1.9.2 and deployed to Heroku's Cedar stack. It has an RSpec and Cucumber test suite which should be run before commiting to the master branch.
5
+
6
+ Laptop setup
7
+ ------------
8
+
9
+ Use our laptop script to get Homebrew, Postgres, Redis, RVM, Ruby 1.9.2, and Bundler:
10
+
11
+ https://github.com/thoughtbot/laptop
12
+
13
+ Use our dotfiles to get commands like `git up` and `git down` for a clean git history:
14
+
15
+ https://github.com/thoughtbot/dotfiles
16
+
17
+ Get the code:
18
+
19
+ git clone git@github.com:your-account/your-app.git
20
+
21
+ Set up the app:
22
+
23
+ cd your-app
24
+ bundle
25
+ bake db:create
26
+ bake db:migrate
27
+
28
+ Running tests
29
+ -------------
30
+
31
+ Run the whole test suite with:
32
+
33
+ bake
34
+
35
+ Run individual specs like:
36
+
37
+ s spec/models/user_spec.rb
38
+
39
+ Run individual features like:
40
+
41
+ cuc features/visitor_signs_in.feature
42
+
43
+ Tab complete to make it even faster!
44
+
45
+ When a spec or feature file has many specs in them, you sometimes want to run just what you're working on. In that case, specify a line number:
46
+
47
+ s spec/models/user_spec.rb:8
48
+ cuc features/visitor_signs_in.feature:105
49
+
50
+ Development process
51
+ -------------------
52
+
53
+ To run the app in development mode, use Foreman, which was installed from the `laptop` script:
54
+
55
+ foreman start
56
+
57
+ It will pick up on the Procfile and use Thin as the app server instead of Webrick, which will also be used by Heroku's Cedar stack.
58
+
59
+ git pull --rebase
60
+ grb create feature-branch
61
+ bake
62
+
63
+ This creates a new branch for your feature. Name it something relevant. Run the tests to make sure everything's passing. Then, implement the feature.
64
+
65
+ bake
66
+ git add -A
67
+ git commit -m "my awesome feature"
68
+ git push origin feature-branch
69
+
70
+ Open up the Github repo, change into your feature-branch branch. Press the "Pull request" button. It should automatically choose the commits that are different between master and your feature-branch. Create a pull request and share the link in Campfire with the team. When someone else gives you the thumbs-up, you can merge into master:
71
+
72
+ git up
73
+ git down
74
+ git push origin master
75
+
76
+ For more details and screenshots of the feature branch code review process, read [this blog post](http://robots.thoughtbot.com/post/2831837714/feature-branch-code-reviews).
77
+
78
+ Most importantly
79
+ ----------------
80
+
81
+ Have fun!
@@ -0,0 +1,20 @@
1
+ #= require lib/jquery
2
+ #= require lib/jquery-ui
3
+ #= require lib/raw
4
+ #= require lib/timeago
5
+ #= require lib/inflection
6
+ #= require lib/underscore
7
+ #= require lib/backbone
8
+ #= require lib/auth_token_sync
9
+ #= require lib/backbone_extended
10
+ #= require lib/milk
11
+ #= require template
12
+
13
+ #= require_tree admin/models
14
+ #= require_tree admin/collections
15
+ #= require_tree admin/routers
16
+ #= require_tree admin/views
17
+ #= require_tree admin/helpers
18
+
19
+ $ ->
20
+ console.log 'admin'
@@ -0,0 +1,21 @@
1
+ #= require lib/jquery
2
+ #= require lib/jquery-ui
3
+ #= require lib/raw
4
+ #= require lib/timeago
5
+ #= require lib/inflection
6
+ #= require lib/underscore
7
+ #= require lib/backbone
8
+ #= require lib/auth_token_sync
9
+ #= require lib/backbone_extended
10
+ #= require lib/milk
11
+ #= require lib/bootstrap
12
+ #= require template
13
+
14
+ #= require_tree ./models
15
+ #= require_tree ./collections
16
+ #= require_tree ./routers
17
+ #= require_tree ./views
18
+ #= require_tree ./helpers
19
+
20
+ $ ->
21
+ console.log 'application'
@@ -0,0 +1,47 @@
1
+ window.Track=
2
+ defaults: {}
3
+ elements:(els, event, category, name, options)->
4
+ els.on "#{event}.track", (ev)=>
5
+ el = @target ev
6
+ options = $.extend({}, @defaults, options)
7
+ options.action = ev.type
8
+ @event category, name, options
9
+ #els.addClass 'tracked'
10
+ visit:(category, name, options)->
11
+ options = $.extend({}, @defaults, options)
12
+ options.action = 'visit'
13
+ @event category, name, options
14
+ event:(category, name, options)->
15
+ options = $.extend({}, @defaults, options)
16
+ funnel = true
17
+ funnel = options.funnel if options.funnel?
18
+ if options.action?
19
+ name = "#{options.action}_#{name}"
20
+ @gaq_funnel_event category, name if funnel
21
+ @gaq_event category, name, options.properties
22
+ @kmq_event category, name, options.properties
23
+ target:(ev)->
24
+ el = ev.target || ev.srcElement
25
+ el = el.parentNode if el.nodeType == 3
26
+ el
27
+ gaq_funnel_event:(category, name)->
28
+ _gaq ?= []
29
+ url = "/_event/#{category}/#{name}"
30
+ _gaq.push ['_trackPageview', url]
31
+ gaq_event:(category, name, properties)->
32
+ _gaq ?= []
33
+ gaq_attrs = ['_trackEvent', category, name]
34
+ gaq_attrs.push JSON.stringify(properties) if properties?
35
+ _gaq.push gaq_attrs
36
+ kmq_event:(category, name, properties)->
37
+ _kmq ?= []
38
+ kmq_attrs = ['record', "#{category} - #{name}"]
39
+ kmq_attrs.push properties if properties?
40
+ _kmq.push kmq_attrs
41
+
42
+
43
+ (($)->
44
+ $.fn.track = (event, category, action, options)->
45
+ Track.elements @, event, category, action, options
46
+ @
47
+ ) jQuery
@@ -0,0 +1,11 @@
1
+ var _gaq = _gaq || [];
2
+
3
+ _gaq.push(['_setAccount', 'UA-17858131-2']);
4
+ _gaq.push(['_trackPageview']);
5
+
6
+ (function() {
7
+ var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
8
+ ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
9
+ var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
10
+ })();
11
+
@@ -0,0 +1,17 @@
1
+ /* alias away the sync method */
2
+ Backbone._sync = Backbone.sync;
3
+
4
+ /* define a new sync method */
5
+ Backbone.sync = function(method, model, success, error) {
6
+ /* only need a token for non-get requests */
7
+ if (method == 'create' || method == 'update' || method == 'delete') {
8
+ /* grab the token from the meta tag rails embeds */
9
+ var auth_options = {};
10
+ auth_options[$("meta[name='csrf-param']").attr('content')] =
11
+ $("meta[name='csrf-token']").attr('content');
12
+ /* set it as a model attribute without triggering events */
13
+ model.set(auth_options, {silent: true});
14
+ }
15
+ /* proxy the call to the old sync method */
16
+ return Backbone._sync(method, model, success, error);
17
+ }
@@ -0,0 +1,2455 @@
1
+ (function(context) {
2
+ // ensure backbone and jquery are available
3
+ if(typeof Backbone === 'undefined') alert('backbone environment not loaded') ;
4
+ if(typeof $ === 'undefined') alert('jquery environment not loaded');
5
+
6
+
7
+ // define our Backbone.UI namespace
8
+ Backbone.UI = Backbone.UI || {
9
+ KEYS : {
10
+ KEY_BACKSPACE: 8,
11
+ KEY_TAB: 9,
12
+ KEY_RETURN: 13,
13
+ KEY_ESC: 27,
14
+ KEY_LEFT: 37,
15
+ KEY_UP: 38,
16
+ KEY_RIGHT: 39,
17
+ KEY_DOWN: 40,
18
+ KEY_DELETE: 46,
19
+ KEY_HOME: 36,
20
+ KEY_END: 35,
21
+ KEY_PAGEUP: 33,
22
+ KEY_PAGEDOWN: 34,
23
+ KEY_INSERT: 45
24
+ },
25
+
26
+ setSkin : function(skin) {
27
+ if(!!Backbone.UI.currentSkin) {
28
+ $(document.body).removeClass('skin_' + Backbone.UI.currentSkin);
29
+ }
30
+ $(document.body).addClass('skin_' + skin);
31
+ Backbone.UI.currentSkin = skin;
32
+ },
33
+
34
+ noop : function(){},
35
+
36
+ IS_MOBILE :
37
+ document.ontouchstart !== undefined ||
38
+ document.ontouchstart === null
39
+ };
40
+
41
+ _(Backbone.View.prototype).extend({
42
+ // resolves the appropriate content from the given choices
43
+ resolveContent : function(model, content) {
44
+ model = _(model).exists() ? model : this.model;
45
+ content = _(content).exists() ? content : this.options.content;
46
+ var hasModelProperty = _(model).exists() && _(content).exists();
47
+ return _(content).isFunction() ? content(model) :
48
+ hasModelProperty && _(model[content]).isFunction() ? model[content]() :
49
+ hasModelProperty && _(_(model).resolveProperty(content)).isFunction() ? _(model).resolveProperty(content)(model) :
50
+ hasModelProperty ? _(model).resolveProperty(content) : content;
51
+ },
52
+
53
+ mixin : function(objects) {
54
+ var options = _(this.options).clone();
55
+
56
+ _(objects).each(function(object) {
57
+ $.extend(true, this, object);
58
+ }, this);
59
+
60
+ $.extend(true, this.options, options);
61
+ }
62
+ });
63
+
64
+ // Add some utility methods to underscore
65
+ _.mixin({
66
+ // produces a natural language description of the given
67
+ // index in the given list
68
+ nameForIndex : function(list, index) {
69
+ return list.length === 1 ? 'first last' :
70
+ index === 0 ? 'first' :
71
+ index === list.length - 1 ?
72
+ 'last' : 'middle';
73
+ },
74
+
75
+ exists : function(object) {
76
+ return !_(object).isNull() && !_(object).isUndefined();
77
+ },
78
+
79
+ // resolves the value of the given property on the given
80
+ // object.
81
+ resolveProperty : function(object, property) {
82
+ var result = null;
83
+ if(_(property).exists() && _(property).isString()) {
84
+ var parts = property.split('.');
85
+ _(parts).each(function(part) {
86
+ if(_(object).exists()) {
87
+ var target = result || object;
88
+ result = _(target.get).isFunction() ? target.get(part) : target[part];
89
+ }
90
+ });
91
+ }
92
+
93
+ return result;
94
+ },
95
+
96
+ // sets the given value for the given property on the given
97
+ // object.
98
+ setProperty : function(object, property, value, silent) {
99
+ if(!property) return;
100
+
101
+ var parts = property.split('.');
102
+ _(parts.slice(0, parts.length - 2)).each(function(part) {
103
+ if(!_(object).isNull() && !_(object).isUndefined()){
104
+ object = _(object.get).isFunction() ? object.get(part) : object[part];
105
+ }
106
+ });
107
+
108
+ if(!!object) {
109
+ if(_(object.set).isFunction()) {
110
+ var attributes = {};
111
+ attributes[property] = value;
112
+ object.set(attributes, {silent : silent});
113
+ }
114
+ else {
115
+ object[property] = value;
116
+ }
117
+ }
118
+ }
119
+ });
120
+
121
+ var _alignCoords = function(el, anchor, pos, xFudge, yFudge) {
122
+ el = $(el);
123
+ anchor = $(anchor);
124
+ pos = pos || '';
125
+
126
+ // Get anchor bounds (document relative)
127
+ var bOffset = anchor.offset();
128
+ var bDim = {width : anchor.width(), height : anchor.height()};
129
+
130
+ // Get element dimensions
131
+ var elbOffset = el.offset();
132
+ var elbDim = {width : el.width(), height : el.height()};
133
+
134
+ // Determine align coords (document-relative)
135
+ var x,y;
136
+ if (pos.indexOf('-left') >= 0) {
137
+ x = bOffset.left;
138
+ } else if (pos.indexOf('left') >= 0) {
139
+ x = bOffset.left - elbDim.width;
140
+ } else if (pos.indexOf('-right') >= 0) {
141
+ x = (bOffset.left + bDim.width) - elbDim.width;
142
+ } else if (pos.indexOf('right') >= 0) {
143
+ x = bOffset.left + bDim.width;
144
+ } else { // Default = centered
145
+ x = bOffset.left + (bDim.width - elbDim.width)/2;
146
+ }
147
+
148
+ if (pos.indexOf('-top') >= 0) {
149
+ y = bOffset.top;
150
+ } else if (pos.indexOf('top') >= 0) {
151
+ y = bOffset.top - elbDim.height;
152
+ } else if (pos.indexOf('-bottom') >= 0) {
153
+ y = (bOffset.top + bDim.height) - elbDim.height;
154
+ } else if (pos.indexOf('bottom') >= 0) {
155
+ y = bOffset.top + bDim.height;
156
+ } else { // Default = centered
157
+ y = bOffset.top + (bDim.height - elbDim.height)/2;
158
+ }
159
+
160
+ // Check for constrainment (default true)
161
+ var constraint = true;
162
+ if (pos.indexOf('no-constraint') >= 0) constraint = false;
163
+
164
+ // Add fudge factors
165
+ x += xFudge || 0;
166
+ y += yFudge || 0;
167
+
168
+ // Create bounds rect/constrain to viewport
169
+ //var nb = new zen.util.Rect(x,y,elb.width,elb.height);
170
+ //if (constraint) nb = nb.constrainTo(zen.util.Dom.getViewport());
171
+
172
+ // Convert to offsetParent coordinates
173
+ //if(el.offsetParent()) {
174
+ //var ob = $(el.offsetParent).getOffset();
175
+ //nb.translate(-ob.left, -ob.top);
176
+ //}
177
+
178
+ // Return rect, constrained to viewport
179
+ return {x : x, y : y};
180
+ };
181
+
182
+
183
+ // Add some utility methods to JQuery
184
+ _($.fn).extend({
185
+ // aligns each element releative to the given anchor
186
+ alignTo : function(anchor, pos, xFudge, yFudge, container) {
187
+ _.each(this, function(el) {
188
+ var rehide = false;
189
+ // in order for alignTo to work properly the element needs to be visible
190
+ // if it's hidden show it off screen so it can be positioned
191
+ if(el.style.display === 'none') {
192
+ rehide=true;
193
+ $(el).css({position:'absolute',top:'-10000px', left:'-10000px', display:'block'});
194
+ }
195
+
196
+ var o = _alignCoords(el, anchor, pos, xFudge, yFudge);
197
+ $(el).css({
198
+ position:'absolute',
199
+ left: Math.round(o.x) + 'px',
200
+ top: Math.round(o.y) + 'px'
201
+ });
202
+
203
+ if(rehide) $(el).hide();
204
+ });
205
+ },
206
+
207
+ // Hides each element the next time the user clicks the mouse or presses a
208
+ // key. This is a one-shot action - once the element is hidden, all
209
+ // related event handlers are removed.
210
+ autohide : function(options) {
211
+ _.each(this, function(el) {
212
+ options = _.extend({
213
+ leaveOpen : false,
214
+ hideCallback : false,
215
+ ignoreInputs: false,
216
+ ignoreKeys : [],
217
+ leaveOpenTargets : []
218
+ }, options || {});
219
+
220
+ el._autoignore = true;
221
+ setTimeout(function() {
222
+ el._autoignore = false; $(el).removeAttr('_autoignore');
223
+ }, 0);
224
+
225
+ if (!el._autohider) {
226
+ el._autohider = _.bind(function(e) {
227
+
228
+ var target = e.target;
229
+ if(!$(el).is(':visible')) return;
230
+
231
+ if (options.ignoreInputs && (/input|textarea|select|option/i).test(target.nodeName)) return;
232
+ //if (el._autoignore || (options.leaveOpen && Element.partOf(e.target, el)))
233
+ if(el._autoignore) return;
234
+ // pass in a list of keys to ignore as autohide triggers
235
+ if(e.type && e.type.match(/keypress/) && _.include(options.ignoreKeys, e.keyCode)) return;
236
+
237
+ // allows you to provide an array of elements that should not trigger autohiding.
238
+ // This is useful for doing thigns like a flyout menu from a pulldown
239
+ if(options.leaveOpenTargets) {
240
+ var ancestor = _(options.leaveOpenTargets).find(function(t) {
241
+ return e.target === t || $(e.target).closest($(t)).length > 0;
242
+ });
243
+ if(!!ancestor) return;
244
+ }
245
+
246
+ var proceed = (options.hideCallback) ? options.hideCallback(el) : true;
247
+ if (!proceed) return;
248
+
249
+ $(el).hide();
250
+ $(document).bind('click', el._autohider);
251
+ $(document).bind('keypress', el._autohider);
252
+ el._autohider = null;
253
+ }, this);
254
+
255
+ $(document).bind('click', el._autohider);
256
+ $(document).bind('keypress', el._autohider);
257
+ }
258
+ });
259
+ }
260
+ });
261
+ }(this));
262
+ (function(){
263
+ window.Backbone.UI.Button = Backbone.View.extend({
264
+ options : {
265
+ tagName : 'a',
266
+
267
+ // true will disable the button
268
+ // (muted non-clickable)
269
+ disabled : false,
270
+
271
+ // true will activate the button
272
+ // (depressed and non-clickable)
273
+ active : false,
274
+
275
+ hasBorder : true,
276
+
277
+ // A callback to invoke when the button is clicked
278
+ onClick : null,
279
+
280
+ // renders this button as an input type=submit element as opposed to an anchor.
281
+ isSubmit : false
282
+ },
283
+
284
+ initialize : function() {
285
+ this.mixin([Backbone.UI.HasModel]);
286
+
287
+ _(this).bindAll('render');
288
+
289
+ $(this.el).addClass('button');
290
+
291
+ // if we're running in a mobile environment, the 'click' event
292
+ // isn't quite translated correctly
293
+ if(Backbone.UI.IS_MOBILE) {
294
+ $(this.el).bind('touchstart', _(function(e) {
295
+ $(this.el).addClass('active');
296
+
297
+ Backbone.UI._activeButton = this;
298
+ var bodyUpListener = $(document.body).bind('touchend', function(e) {
299
+ if(Backbone.UI._activeButton) {
300
+ if(e.target === Backbone.UI._activeButton.el || $(e.target).closest('.button.active').length > 0) {
301
+ if(Backbone.UI._activeButton.options.onClick) Backbone.UI._activeButton.options.onClick(e);
302
+ }
303
+ $(Backbone.UI._activeButton.el).removeClass('active');
304
+ }
305
+
306
+ Backbone.UI._activeButton = null;
307
+ $(document.body).unbind('touchend', bodyUpListener);
308
+ });
309
+
310
+ return false;
311
+ }).bind(this));
312
+ }
313
+
314
+ else {
315
+ $(this.el).bind('click', _(function(e) {
316
+ if(!this.options.disabled && !this.options.active && this.options.onClick) {
317
+ this.options.onClick(e);
318
+ }
319
+ return false;
320
+ }).bind(this));
321
+ }
322
+ },
323
+
324
+ render : function() {
325
+ var labelText = this.resolveContent();
326
+
327
+ this._observeModel(this.render);
328
+
329
+ $(this.el).empty();
330
+ $(this.el).toggleClass('has_border', this.options.hasBorder);
331
+
332
+ if(this.options.isSubmit) {
333
+ $.el.input({
334
+ type : 'submit',
335
+ value : ''
336
+ }).appendTo(this.el);
337
+ }
338
+
339
+ this.el.appendChild($.el.span({className : 'label'}, labelText));
340
+
341
+ // add appropriate class names
342
+ this.setEnabled(!this.options.disabled);
343
+ this.setActive(this.options.active);
344
+
345
+ return this;
346
+ },
347
+
348
+ // sets the enabled state of the button
349
+ setEnabled : function(enabled) {
350
+ if(enabled) {
351
+ this.el.href = '#';
352
+ } else {
353
+ this.el.removeAttribute('href');
354
+ }
355
+ this.options.disabled = !enabled;
356
+ $(this.el)[enabled ? 'removeClass' : 'addClass']('disabled');
357
+ },
358
+
359
+ // sets the active state of the button
360
+ setActive : function(active) {
361
+ this.options.active = active;
362
+ $(this.el)[active ? 'addClass' : 'removeClass']('active');
363
+ }
364
+ });
365
+ }());
366
+
367
+ (function() {
368
+
369
+ var monthNames = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
370
+ var dayNames = ['s', 'm', 't', 'w', 't', 'f', 's'];
371
+
372
+ var isLeapYear = function(year) {
373
+ return ((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0);
374
+ };
375
+
376
+ var daysInMonth = function(date) {
377
+ return [31, (isLeapYear(date.getYear()) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][date.getMonth()];
378
+ };
379
+
380
+ var formatDateHeading = function(date) {
381
+ return monthNames[date.getMonth()] + ' ' + date.getFullYear();
382
+ };
383
+
384
+ var isSameMonth = function(date1, date2) {
385
+ return date1.getFullYear() === date2.getFullYear() &&
386
+ date1.getMonth() === date2.getMonth();
387
+ };
388
+
389
+ window.Backbone.UI.Calendar = Backbone.View.extend({
390
+ options : {
391
+ // the selected calendar date
392
+ date : null,
393
+
394
+ // the week's start day (0 = Sunday, 1 = Monday, etc.)
395
+ weekStart : 0,
396
+
397
+ // a callback to invoke when a new date selection is made. The selected date
398
+ // will be passed in as the first argument
399
+ onSelect : null
400
+ },
401
+
402
+ date : null,
403
+
404
+ initialize : function() {
405
+ $(this.el).addClass('calendar');
406
+ _(this).bindAll('render');
407
+ },
408
+
409
+ render : function() {
410
+ if(_(this.model).exists() && _(this.options.content).exists()) {
411
+ this.date = this.resolveContent();
412
+ var key = 'change:' + this.options.content;
413
+ this.model.unbind(key, this.render);
414
+ this.model.bind(key, this.render);
415
+ }
416
+
417
+ else {
418
+ this.date = this.date || this.options.date || new Date();
419
+ }
420
+
421
+ this._renderDate(this.date);
422
+
423
+ return this;
424
+ },
425
+
426
+ _selectDate : function(date) {
427
+ this.date = date;
428
+ if(_(this.model).exists() && _(this.options.content).exists()) {
429
+
430
+ // we only want to set the bound property's date portion
431
+ var boundDate = this.resolveContent();
432
+ var updatedDate = new Date(boundDate.getTime());
433
+ updatedDate.setMonth(date.getMonth());
434
+ updatedDate.setDate(date.getDate());
435
+ updatedDate.setFullYear(date.getFullYear());
436
+
437
+ _(this.model).setProperty(this.options.content, updatedDate);
438
+ }
439
+ this.render();
440
+ if(_(this.options.onSelect).isFunction()) {
441
+ this.options.onSelect(date);
442
+ }
443
+ return false;
444
+ },
445
+
446
+ _renderDate : function(date, e) {
447
+ if(e) e.stopPropagation();
448
+ $(this.el).empty();
449
+
450
+ var nextMonth = new Date(date.getFullYear(), date.getMonth() + 1);
451
+ var lastMonth = new Date(date.getFullYear(), date.getMonth() - 1);
452
+ var monthStartDay = (new Date(date.getFullYear(), date.getMonth(), 1).getDay());
453
+ var inactiveBeforeDays = monthStartDay - this.options.weekStart - 1;
454
+ var daysInThisMonth = daysInMonth(date);
455
+ var today = new Date();
456
+ var inCurrentMonth = isSameMonth(today, date);
457
+ var inSelectedMonth = !!this.date && isSameMonth(this.date, date);
458
+
459
+ var daysRow = $.el.tr({className : 'row days'});
460
+ var names = dayNames.slice(this.options.weekStart).concat(
461
+ dayNames.slice(0, this.options.weekStart));
462
+ for(var i=0; i<names.length; i++) {
463
+ $.el.td(names[i]).appendTo(daysRow);
464
+ }
465
+
466
+ var tbody, table = $.el.table(
467
+ $.el.thead(
468
+ $.el.th(
469
+ $.el.a({className : 'go_back', onclick : _(this._renderDate).bind(this, lastMonth)}, '\u2039')),
470
+ $.el.th({className : 'title', colspan : 5},
471
+ $.el.div(formatDateHeading(date))),
472
+ $.el.th(
473
+ $.el.a({className : 'go_forward', onclick : _(this._renderDate).bind(this, nextMonth)}, '\u203a'))),
474
+ tbody = $.el.tbody(daysRow));
475
+
476
+ var day = inactiveBeforeDays >= 0 ? daysInMonth(lastMonth) - inactiveBeforeDays : 1;
477
+ var daysRendered = 0;
478
+ for(var rowIndex=0; rowIndex<6 ; rowIndex++) {
479
+
480
+ var row = $.el.tr({
481
+ className : 'row' + (rowIndex === 0 ? ' first' : rowIndex === 4 ? ' last' : '')
482
+ });
483
+
484
+ for(var colIndex=0; colIndex<7; colIndex++) {
485
+ var inactive = daysRendered <= inactiveBeforeDays ||
486
+ daysRendered > inactiveBeforeDays + daysInThisMonth;
487
+
488
+ var callback = _(this._selectDate).bind(
489
+ this, new Date(date.getFullYear(), date.getMonth(), day));
490
+
491
+ var className = 'cell' + (inactive ? ' inactive' : '') +
492
+ (colIndex === 0 ? ' first' : colIndex === 6 ? ' last' : '') +
493
+ (inCurrentMonth && !inactive && day === today.getDate() ? ' today' : '') +
494
+ (inSelectedMonth && !inactive && day === this.date.getDate() ? ' selected' : '');
495
+
496
+ $.el.td({ className : className },
497
+ inactive ?
498
+ $.el.div({ className : 'day' }, day) :
499
+ $.el.a({ className : 'day', onClick : callback }, day)).appendTo(row);
500
+
501
+ day = (rowIndex === 0 && colIndex === inactiveBeforeDays) ||
502
+ (rowIndex > 0 && day === daysInThisMonth) ? 1 : day + 1;
503
+
504
+ daysRendered++;
505
+ }
506
+
507
+ row.appendTo(tbody);
508
+ }
509
+
510
+ this.el.appendChild(table);
511
+
512
+ return false;
513
+ }
514
+ });
515
+ }());
516
+ (function(){
517
+ window.Backbone.UI.Checkbox = Backbone.View.extend({
518
+
519
+ options : {
520
+ tagName : 'a',
521
+
522
+ // The property of the model describing the label that
523
+ // should be placed next to the checkbox
524
+ labelContent : null,
525
+
526
+ // enables / disables the checkbox
527
+ disabled : false
528
+ },
529
+
530
+ initialize : function() {
531
+ this.mixin([Backbone.UI.HasModel]);
532
+ _(this).bindAll('render');
533
+
534
+ $(this.el).click(_(this._onClick).bind(this));
535
+ $(this.el).attr({href : '#'});
536
+ $(this.el).addClass('checkbox');
537
+ if(this.options.name){
538
+ $(this.el).addClass(this.options.name);
539
+ }
540
+ },
541
+
542
+ render : function() {
543
+
544
+ this._observeModel(this.render);
545
+
546
+ $(this.el).empty();
547
+
548
+ this.checked = this.checked || this.resolveContent();
549
+ var mark = $.el.div({className : 'checkmark'});
550
+ if(this.checked) {
551
+ mark.appendChild($.el.div({className : 'checkmark_fill'}));
552
+ }
553
+
554
+ var labelText = this.resolveContent(this.model, this.options.labelContent) || this.options.labelContent;
555
+ this._label = $.el.div({className : 'label'}, labelText);
556
+ $('a',this._label).click(function(e){
557
+ e.stopPropagation();
558
+ });
559
+ this.el.appendChild(mark);
560
+ this.el.appendChild(this._label);
561
+ this.el.appendChild($.el.br({style : 'clear:both'}));
562
+
563
+ return this;
564
+ },
565
+
566
+ _onClick : function() {
567
+ if (this.options.disabled) {
568
+ return false;
569
+ }
570
+
571
+ this.checked = !this.checked;
572
+ if(_(this.model).exists() && _(this.options.content).exists()) {
573
+ _(this.model).setProperty(this.options.content, this.checked);
574
+ }
575
+
576
+ else {
577
+ this.render();
578
+ }
579
+
580
+ return false;
581
+ }
582
+ });
583
+ }());
584
+ (function(){
585
+ window.Backbone.UI.CollectionView = Backbone.View.extend({
586
+ options : {
587
+ // The Backbone.Collection instance the view is bound to
588
+ model : null,
589
+
590
+ // The Backbone.View class responsible for rendering a single item in the collection
591
+ itemView : null,
592
+
593
+ // A string, element, or function describing what should be displayed
594
+ // when the list is empty.
595
+ emptyContent : null,
596
+
597
+ // A callback to invoke when a row is clicked. The associated model will be
598
+ // passed as the first argument.
599
+ onItemClick : Backbone.UI.noop,
600
+
601
+ // The maximum height in pixels that this table show grow to. If the
602
+ // content exceeds this height, it will become scrollable.
603
+ maxHeight : null
604
+ },
605
+
606
+ itemViews : {},
607
+
608
+ _emptyContent : null,
609
+
610
+ // must be over-ridden to describe how an item is rendered
611
+ _renderItem : Backbone.UI.noop,
612
+
613
+ initialize : function() {
614
+ if(this.model) {
615
+ this.model.bind('add', _.bind(this._onItemAdded, this));
616
+ this.model.bind('change', _.bind(this._onItemChanged, this));
617
+ this.model.bind('remove', _.bind(this._onItemRemoved, this));
618
+ this.model.bind('refresh', _.bind(this.render, this));
619
+ this.model.bind('reset', _.bind(this.render, this));
620
+ }
621
+ },
622
+
623
+ _onItemAdded : function(model, list, options) {
624
+ // first check if we've already rendered an item for this model
625
+ if(!!this.itemViews[model.cid]) {
626
+ return;
627
+ }
628
+
629
+ // remove empty content if it exists
630
+ if(!!this._emptyContent) {
631
+ if(!!this._emptyContent.parentNode) this._emptyContent.parentNode.removeChild(this._emptyContent);
632
+ this._emptyContent = null;
633
+ }
634
+
635
+ // render the new item
636
+ var properIndex = list.indexOf(model);
637
+ var el = this._renderItem(model, properIndex);
638
+
639
+ // insert it into the DOM position that matches it's position in the model
640
+ var anchorNode = this.collectionEl.childNodes[properIndex];
641
+ this.collectionEl.insertBefore(el, _(anchorNode).isUndefined() ? null : anchorNode);
642
+
643
+ // update the first / last class names
644
+ this._updateClassNames();
645
+ },
646
+
647
+ _onItemChanged : function(model) {
648
+ var view = this.itemViews[model.cid];
649
+ // re-render the individual item view if it's a backbone view
650
+ if(!!view && view.el && view.el.parentNode) {
651
+ view.render();
652
+ this._ensureProperPosition(view);
653
+ }
654
+
655
+ // otherwise, we re-render the entire collection
656
+ else {
657
+ this.render();
658
+ }
659
+ },
660
+
661
+ _onItemRemoved : function(model) {
662
+ var view = this.itemViews[model.cid];
663
+ var liOrTrElement = view.el.parentNode;
664
+ if(!!view && !!liOrTrElement && !!liOrTrElement.parentNode) {
665
+ liOrTrElement.parentNode.removeChild(liOrTrElement);
666
+ }
667
+ delete(this.itemViews[model.cid]);
668
+
669
+ // update the first / last class names
670
+ this._updateClassNames();
671
+ },
672
+
673
+ _updateClassNames : function() {
674
+ var children = this.collectionEl.childNodes;
675
+ if(children.length > 0) {
676
+ _(children).each(function(child) {
677
+ $(child).removeClass('first');
678
+ $(child).removeClass('last');
679
+ });
680
+ $(children[0]).addClass('first');
681
+ $(children[children.length - 1]).addClass('last');
682
+ }
683
+ },
684
+
685
+ _ensureProperPosition : function(view) {
686
+ if(_(this.model.comparator).isFunction()) {
687
+ this.model.sort({silent : true});
688
+ var itemEl = view.el.parentNode;
689
+ var currentIndex = _(this.collectionEl.childNodes).indexOf(itemEl, true);
690
+ var properIndex = this.model.indexOf(view.model);
691
+ if(currentIndex !== properIndex) {
692
+ itemEl.parentNode.removeChild(itemEl);
693
+ var refNode = this.collectionEl.childNodes[properIndex];
694
+ if(refNode) {
695
+ this.collectionEl.insertBefore(itemEl, refNode);
696
+ }
697
+ else {
698
+ this.collectionEl.appendChild(itemEl);
699
+ }
700
+ }
701
+ }
702
+ }
703
+ });
704
+ }());
705
+
706
+ (function(){
707
+ window.Backbone.UI.DatePicker = Backbone.View.extend({
708
+
709
+ options : {
710
+ // a moment.js format : http://momentjs.com/docs/#/display/format
711
+ format : 'MM/DD/YYYY',
712
+ date : null,
713
+ name : null,
714
+ onChange : null
715
+ },
716
+
717
+ initialize : function() {
718
+ $(this.el).addClass('date_picker');
719
+
720
+ this._calendar = new Backbone.UI.Calendar({
721
+ className : 'date_picker_calendar',
722
+ model : this.model,
723
+ property : this.options.content,
724
+ onSelect : _(this._selectDate).bind(this)
725
+ });
726
+ $(this._calendar.el).hide();
727
+ document.body.appendChild(this._calendar.el);
728
+
729
+ $(this._calendar.el).autohide({
730
+ ignoreInputs : true,
731
+ leaveOpenTargets : [this._calendar.el]
732
+ });
733
+
734
+ // listen for model changes
735
+ if(!!this.model && this.options.content) {
736
+ this.model.bind('change:' + this.options.content, _(this.render).bind(this));
737
+ }
738
+ },
739
+
740
+ render : function() {
741
+ $(this.el).empty();
742
+
743
+ this._textField = new Backbone.UI.TextField({
744
+ name : this.options.name
745
+ }).render();
746
+
747
+ $(this._textField.input).click(_(this._showCalendar).bind(this));
748
+ $(this._textField.input).keyup(_(this._dateEdited).bind(this));
749
+
750
+ this.el.appendChild(this._textField.el);
751
+
752
+ this._selectedDate = (!!this.model && !!this.options.content) ?
753
+ this.resolveContent() : this.options.date;
754
+
755
+ if(!!this._selectedDate) {
756
+ this._calendar.options.selectedDate = this._selectedDate;
757
+ var dateString = moment(this._selectedDate).format(this.options.format);
758
+ this._textField.setValue(dateString);
759
+ }
760
+ this._calendar.render();
761
+
762
+ return this;
763
+ },
764
+
765
+ setEnabled : function(enabled) {
766
+ this._textField.setEnabled(enabled);
767
+ },
768
+
769
+ getValue : function() {
770
+ return this._selectedDate;
771
+ },
772
+
773
+ setValue : function(date) {
774
+ this._selectedDate = date;
775
+ var dateString = moment(date).format(this.options.format);
776
+ this._textField.setValue(dateString);
777
+ this._dateEdited();
778
+ },
779
+
780
+ _showCalendar : function() {
781
+ $(this._calendar.el).show();
782
+ $(this._calendar.el).alignTo(this._textField.el, 'bottom -left', 0, 2);
783
+ },
784
+
785
+ _hideCalendar : function() {
786
+ $(this._calendar.el).hide();
787
+ },
788
+
789
+ _selectDate : function(date) {
790
+ var month = date.getMonth() + 1;
791
+ if(month < 10) month = '0' + month;
792
+
793
+ var day = date.getDate();
794
+ if(day < 10) day = '0' + day;
795
+
796
+ var dateString = moment(date).format(this.options.format);
797
+ this._textField.setValue(dateString);
798
+ this._dateEdited();
799
+ this._hideCalendar();
800
+
801
+ return false;
802
+ },
803
+
804
+ _dateEdited : function(e) {
805
+ var newDate = moment(this._textField.getValue(), this.options.format);
806
+ this._selectedDate = newDate.toDate();
807
+
808
+ // if the enter key was pressed or we've invoked this method manually,
809
+ // we hide the calendar and re-format our date
810
+ if(!e || e.keyCode === Backbone.UI.KEYS.KEY_RETURN) {
811
+ var newValue = moment(newDate).format(this.options.format);
812
+ this._textField.setValue(newValue);
813
+ this._hideCalendar();
814
+
815
+ // update our bound model (but only the date portion)
816
+ if(!!this.model && this.options.content) {
817
+ var boundDate = this.resolveContent() || new Date();
818
+ var updatedDate = new Date(boundDate.getTime());
819
+ updatedDate.setMonth(newDate.month());
820
+ updatedDate.setDate(newDate.date());
821
+ updatedDate.setFullYear(newDate.year());
822
+ _(this.model).setProperty(this.options.content, updatedDate);
823
+ }
824
+
825
+ if(_(this.options.onChange).isFunction()) {
826
+ this.options.onChange(newValue);
827
+ }
828
+ }
829
+ }
830
+ });
831
+ }());
832
+ (function() {
833
+ Backbone.UI.DragSession = function(options) {
834
+ this.options = _.extend({
835
+ // A mouse(move/down) event
836
+ dragEvent : null,
837
+
838
+ //The document in which the drag session should occur
839
+ scope : null,
840
+
841
+ //Sent when the session is ends up being a sloppy mouse click
842
+ onClick: Backbone.UI.noop,
843
+
844
+ // Sent when a drag session starts for real
845
+ // (after the mouse has moved SLOP pixels)
846
+ onStart: Backbone.UI.noop,
847
+
848
+ // Sent for each mouse move event that occurs during the drag session
849
+ onMove: Backbone.UI.noop,
850
+
851
+ // Sent when the session stops normally (the mouse was released)
852
+ onStop: Backbone.UI.noop,
853
+
854
+ // Sent when the session is aborted (ESC key pressed)
855
+ onAbort: Backbone.UI.noop,
856
+
857
+ // Sent when the drag session finishes, regardless of
858
+ // whether it stopped normally or was aborted.
859
+ onDone: Backbone.UI.noop
860
+ }, options);
861
+
862
+ if(Backbone.UI.DragSession.currentSession) {
863
+ // Abort any existing drag session. While this should never happen in
864
+ // theory, in practice it happens a fair bit (e.g. if a mouseup occurs
865
+ // outside the document). So we don't complain about.
866
+ Backbone.UI.DragSession.currentSession.abort();
867
+ }
868
+
869
+ this._doc = this.options.scope || document;
870
+
871
+ this._handleEvent = _.bind(this._handleEvent, this);
872
+ this._handleEvent(this.options.dragEvent);
873
+
874
+ // Activate handlers
875
+ this._activate(true);
876
+
877
+ this.options.dragEvent.stopPropagation();
878
+
879
+ /**
880
+ * currentSession The currently active drag session.
881
+ */
882
+ Backbone.UI.DragSession.currentSession = this;
883
+ };
884
+
885
+ // add class methods
886
+ _.extend(Backbone.UI.DragSession, {
887
+ SLOP : 2,
888
+
889
+ BASIC_DRAG_CLASSNAME: 'dragging',
890
+
891
+ // Enable basic draggable element behavior for absolutely positioned elements.
892
+ // scope: The window/document to enable dragging on. Default is current document.
893
+ // container: a container element to constrain dragging within
894
+ // shield: if true the draggable will use a shield iframe useful for
895
+ // covering controls that bleed through zindex layers
896
+ enableBasicDragSupport : function(scope, container, shield) {
897
+ var d = scope ? (scope.document || scope) : document;
898
+ if (d._basicDragSupportEnabled) return;
899
+ d._basicDragSupportEnabled = true;
900
+ // Enable "draggable"/"grabbable" classes
901
+ $(d).bind('mousedown', function(e) {
902
+ var el = e.target;
903
+
904
+ // Ignore clicks that happen on anything the user might want to
905
+ // interact with input elements
906
+ var IGNORE = /(input|textarea|button|select|option)/i;
907
+ if (IGNORE.exec(el.nodeName)) return;
908
+
909
+ // Find the element to drag
910
+ if (!el.hasClassName) return; // flash objects don't support this method
911
+ // and should not be draggable
912
+ // this fixes a problem in Shareflow in IE7
913
+ // with the upload button
914
+ var del = el.hasClassName('draggable') ? el : el.up('.draggable');
915
+ del = del ? del.up('.draggable-container') || del : null;
916
+
917
+ if (del) {
918
+ // Get the allowable bounds to drag w/in
919
+ // if (container) container = $(container);
920
+ // var vp = container ? container.getBounds() : zen.util.Dom.getViewport(del.ownerDocument);
921
+ //var vp = zen.util.Dom.getViewport(del.ownerDocument);
922
+ var elb = del.getBounds();
923
+
924
+ // Create a new drag session
925
+ var activeElement = document.activeElement;
926
+ var ds = new Backbone.UI.DragSession({
927
+ dragEvent : e,
928
+ scope : del.ownerDocument,
929
+ onStart : function(ds) {
930
+ if (activeElement && activeElement.blur) activeElement.blur();
931
+ ds.pos = del.positionedOffset();
932
+ $(del).addClass(Backbone.UI.DragSession.BASIC_DRAG_CLASSNAME);
933
+ },
934
+ onMove : function(ds) {
935
+ //elb.moveTo(ds.pos.left + ds.dx, ds.pos.top + ds.dy).constrainTo(vp);
936
+ del.style.left = elb.x + 'px';
937
+ del.style.top = elb.y + 'px';
938
+ },
939
+ onDone : function(ds) {
940
+ if (activeElement && activeElement.focus) activeElement.focus();
941
+ del.removeClassName(Backbone.UI.DragSession.BASIC_DRAG_CLASSNAME);
942
+ }
943
+ });
944
+ }
945
+ });
946
+ }
947
+ });
948
+
949
+ // add instance methods
950
+ _.extend(Backbone.UI.DragSession.prototype, {
951
+
952
+ // Fire the onStop event and stop the drag session.
953
+ stop: function() {
954
+ this._stop();
955
+ },
956
+
957
+ // Fire the onAbort event and stop the drag session.
958
+ abort: function() {
959
+ this._stop(true);
960
+ },
961
+
962
+ // Activate the session by registering/unregistering event handlers
963
+ _activate: function(flag) {
964
+ var f = flag ? 'bind' : 'unbind';
965
+ $(this._doc)[f]('mousemove', this._handleEvent);
966
+ $(this._doc)[f]('mouseup', this._handleEvent);
967
+ $(this._doc)[f]('keyup', this._handleEvent);
968
+ },
969
+
970
+ // All-in-one event handler for managing a drag session
971
+ _handleEvent: function(e) {
972
+ e.stopPropagation();
973
+ e.preventDefault();
974
+
975
+ this.x = e.pageX;
976
+ this.y = e.pageY;
977
+
978
+ if (e.type === 'mousedown') {
979
+ // Absolute X of initial mouse down*/
980
+ this.xStart = this.x;
981
+
982
+ // Absolute Y of initial mouse down
983
+ this.yStart = this.y;
984
+ }
985
+
986
+ // X-coord relative to initial mouse down
987
+ this.dx = this.x - this.xStart;
988
+
989
+ // Y-coord relative to initial mouse down
990
+ this.dy = this.y - this.yStart;
991
+
992
+ switch (e.type) {
993
+ case 'mousemove':
994
+ if (!this._dragging) {
995
+ // Sloppy click?
996
+ if(this.dx * this.dx + this.dy * this.dy >= Backbone.UI.DragSession.SLOP * Backbone.UI.DragSession.SLOP) {
997
+ this._dragging = true;
998
+ this.options.onStart(this, e);
999
+ }
1000
+ } else {
1001
+ this.options.onMove(this, e);
1002
+ }
1003
+ break;
1004
+ case 'mouseup':
1005
+ if (!this._dragging) {
1006
+ this.options.onClick(this, e);
1007
+ } else {
1008
+ this.stop();
1009
+ }
1010
+ //this._stop();
1011
+ break;
1012
+ case 'keyup':
1013
+ if (e.keyCode !== Backbone.UI.KEYS.KEY_ESC) return;
1014
+ this.abort();
1015
+ break;
1016
+ default:
1017
+ return;
1018
+ }
1019
+ },
1020
+
1021
+ // Stop the drag session
1022
+ _stop: function(abort) {
1023
+ Backbone.UI.DragSession.currentSession = null;
1024
+
1025
+ // Deactivate handlers
1026
+ this._activate(false);
1027
+
1028
+ if (this._dragging) {
1029
+ if (abort) {
1030
+ this.options.onAbort(this);
1031
+ } else {
1032
+ this.options.onStop(this);
1033
+ }
1034
+ this.options.onDone(this);
1035
+ }
1036
+ }
1037
+ });
1038
+ }());
1039
+
1040
+ // A mixin for dealing with collection alternatives
1041
+ (function(){
1042
+ Backbone.UI.HasAlternativeProperty = {
1043
+ options : {
1044
+ // The collection of items representing alternative choices
1045
+ alternatives : null,
1046
+
1047
+ // The property of the individual choice represent the the label to be displayed
1048
+ altLabelContent : null,
1049
+
1050
+ // The property of the individual choice that represents the value to be stored
1051
+ // in the bound model's property. Omit this option if you'd like the choice
1052
+ // object itself to represent the value.
1053
+ altValueContent : null
1054
+ },
1055
+
1056
+ _determineSelectedItem : function() {
1057
+ var item;
1058
+
1059
+ // if a bound property has been given, we attempt to resolve it
1060
+ if(_(this.model).exists() && _(this.options.content).exists()) {
1061
+ item = _(this.model).resolveProperty(this.options.content);
1062
+
1063
+ // if a value property is given, we further resolve our selected item
1064
+ if(_(this.options.altValueContent).exists()) {
1065
+ var otherItem = _(this._collectionArray()).detect(function(collectionItem) {
1066
+ return (collectionItem.attributes || collectionItem)[this.options.altValueContent] === item;
1067
+ }, this);
1068
+ if(!_(otherItem).isUndefined()) item = otherItem;
1069
+ }
1070
+ }
1071
+
1072
+ return item || this.options.selectedItem;
1073
+ },
1074
+
1075
+ _setSelectedItem : function(item, silent) {
1076
+ this.selectedValue = item;
1077
+ this.selectedItem = item;
1078
+
1079
+ if(_(this.model).exists() && _(this.options.content).exists()) {
1080
+ this.selectedValue = this._valueForItem(item);
1081
+ _(this.model).setProperty(this.options.content, this.selectedValue, silent);
1082
+ }
1083
+ },
1084
+
1085
+ _valueForItem : function(item) {
1086
+ return _(this.options.altValueContent).exists() ?
1087
+ _(item).resolveProperty(this.options.altValueContent) :
1088
+ item;
1089
+ },
1090
+
1091
+ _collectionArray : function() {
1092
+ return _(this.options.alternatives).exists() ?
1093
+ this.options.alternatives.models || this.options.alternatives : [];
1094
+ },
1095
+
1096
+ _observeCollection : function(callback) {
1097
+ if(_(this.options.alternatives).exists() && _(this.options.alternatives.bind).exists()) {
1098
+ var key = 'change';
1099
+ this.options.alternatives.unbind(key, callback);
1100
+ this.options.alternatives.bind(key, callback);
1101
+ }
1102
+ }
1103
+ };
1104
+ }());
1105
+
1106
+ // A mixin for those views that are model bound
1107
+ (function(){
1108
+ Backbone.UI.HasModel = {
1109
+
1110
+ options : {
1111
+ // The Backbone.Model instance the view is bound to
1112
+ model : null,
1113
+
1114
+ // The property of the bound model this component should render / update.
1115
+ // If a function is given, it will be invoked with the model and will
1116
+ // expect an element to be returned. If no model is present, this
1117
+ // property may be a string or function describing the content to be rendered
1118
+ content : null
1119
+ },
1120
+
1121
+ _observeModel : function(callback) {
1122
+ if(_(this.model).exists() && _(this.model.unbind).isFunction()) {
1123
+ _(['content', 'labelContent']).each(function(prop) {
1124
+ var key = this.options[prop];
1125
+ if(_(key).exists()) {
1126
+ key = 'change:' + key;
1127
+ this.model.unbind(key, callback);
1128
+ this.model.bind(key, callback);
1129
+ }
1130
+ }, this);
1131
+ }
1132
+ }
1133
+ };
1134
+ }());
1135
+
1136
+ (function(){
1137
+ window.Backbone.UI.Label = Backbone.View.extend({
1138
+ options : {
1139
+ tagName : 'span'
1140
+ },
1141
+
1142
+ initialize : function() {
1143
+ this.mixin([Backbone.UI.HasModel]);
1144
+
1145
+ _(this).bindAll('render');
1146
+
1147
+ $(this.el).addClass('label');
1148
+
1149
+ },
1150
+
1151
+ render : function() {
1152
+ var labelText = this.resolveContent();
1153
+
1154
+ this._observeModel(this.render);
1155
+
1156
+ $(this.el).empty();
1157
+
1158
+ // insert label
1159
+ this.el.appendChild(document.createTextNode(labelText));
1160
+
1161
+ return this;
1162
+ }
1163
+
1164
+ });
1165
+ }());
1166
+
1167
+ (function(){
1168
+ window.Backbone.UI.Link = Backbone.View.extend({
1169
+ options : {
1170
+ tagName : 'a',
1171
+
1172
+ // disables the link (non-clickable)
1173
+ disabled : false,
1174
+
1175
+ // A callback to invoke when the link is clicked
1176
+ onClick : null
1177
+ },
1178
+
1179
+ initialize : function() {
1180
+ this.mixin([Backbone.UI.HasModel]);
1181
+
1182
+ _(this).bindAll('render');
1183
+
1184
+ $(this.el).addClass('link');
1185
+
1186
+ $(this.el).bind('click', _(function(e) {
1187
+ if(!this.options.disabled && this.options.onClick) {
1188
+ this.options.onClick(e);
1189
+ }
1190
+ return false;
1191
+ }).bind(this));
1192
+ },
1193
+
1194
+ render : function() {
1195
+ var labelText = this.resolveContent();
1196
+
1197
+ this._observeModel(this.render);
1198
+
1199
+ $(this.el).empty();
1200
+
1201
+ // insert label
1202
+ this.el.appendChild($.el.span({className : 'label'}, labelText));
1203
+
1204
+ // add appropriate class names
1205
+ this.setEnabled(!this.options.disabled);
1206
+
1207
+ return this;
1208
+ },
1209
+
1210
+ // sets the enabled state of the button
1211
+ setEnabled : function(enabled) {
1212
+ if(enabled) {
1213
+ this.el.href = '#';
1214
+ } else {
1215
+ this.el.removeAttribute('href');
1216
+ }
1217
+ this.options.disabled = !enabled;
1218
+ $(this.el)[enabled ? 'removeClass' : 'addClass']('disabled');
1219
+ }
1220
+ });
1221
+ }());
1222
+
1223
+ (function(){
1224
+ window.Backbone.UI.List = Backbone.UI.CollectionView.extend({
1225
+ options : {
1226
+ // A Backbone.View implementation describing how to render a particular
1227
+ // item in the collection. For simple use cases, you can pass a String
1228
+ // instead which will be interpreted as the property of the model to display.
1229
+ itemView : null
1230
+ },
1231
+
1232
+ initialize : function() {
1233
+ Backbone.UI.CollectionView.prototype.initialize.call(this, arguments);
1234
+ $(this.el).addClass('list');
1235
+ },
1236
+
1237
+ render : function() {
1238
+ $(this.el).empty();
1239
+ this.itemViews = {};
1240
+
1241
+ this.collectionEl = $.el.ul();
1242
+
1243
+ // if the collection is empty, we render the empty content
1244
+ if(!_(this.model).exists() || this.model.length === 0) {
1245
+ this._emptyContent = _(this.options.emptyContent).isFunction() ?
1246
+ this.options.emptyContent() : this.options.emptyContent;
1247
+ this._emptyContent = $.el.li(this._emptyContent);
1248
+
1249
+ if(!!this._emptyContent) {
1250
+ this.collectionEl.appendChild(this._emptyContent);
1251
+ }
1252
+ }
1253
+
1254
+ // otherwise, we render each row
1255
+ else {
1256
+ _(this.model.models).each(function(model, index) {
1257
+ var item = this._renderItem(model, index);
1258
+ this.collectionEl.appendChild(item);
1259
+ }, this);
1260
+ }
1261
+
1262
+ // wrap the list in a scroller
1263
+ if(_(this.options.maxHeight).exists()) {
1264
+ var style = 'max-height:' + this.options.maxHeight + 'px';
1265
+ var scroller = new Backbone.UI.Scroller({
1266
+ content : $.el.div({style : style}, this.collectionEl)
1267
+ }).render();
1268
+
1269
+ this.el.appendChild(scroller.el);
1270
+ }
1271
+ else {
1272
+ this.el.appendChild(this.collectionEl);
1273
+ }
1274
+
1275
+ this._updateClassNames();
1276
+
1277
+ return this;
1278
+ },
1279
+
1280
+ // renders an item for the given model, at the given index
1281
+ _renderItem : function(model, index) {
1282
+ var content;
1283
+ if(_(this.options.itemView).exists()) {
1284
+
1285
+ if(_(this.options.itemView).isString()) {
1286
+ content = this.resolveContent(model, this.options.itemView);
1287
+ }
1288
+
1289
+ else {
1290
+ var view = new this.options.itemView({
1291
+ model : model
1292
+ });
1293
+ view.render();
1294
+ this.itemViews[model.cid] = view;
1295
+ content = view.el;
1296
+ }
1297
+ }
1298
+
1299
+ var item = $.el.li(content);
1300
+
1301
+ // bind the item click callback if given
1302
+ if(this.options.onItemClick) {
1303
+ $(item).click(_(this.options.onItemClick).bind(this, model));
1304
+ }
1305
+
1306
+ return item;
1307
+ }
1308
+ });
1309
+ }());
1310
+
1311
+ (function(){
1312
+ window.Backbone.UI.Menu = Backbone.View.extend({
1313
+
1314
+ options : {
1315
+ // an additional item to render at the top of the menu to
1316
+ // denote the lack of a selection
1317
+ emptyItem : null
1318
+ },
1319
+
1320
+ initialize : function() {
1321
+ this.mixin([Backbone.UI.HasModel, Backbone.UI.HasAlternativeProperty]);
1322
+
1323
+ _(this).bindAll('render');
1324
+
1325
+ $(this.el).addClass('menu');
1326
+
1327
+ this._textField = new Backbone.UI.TextField().render();
1328
+ },
1329
+
1330
+ scroller : null,
1331
+
1332
+ render : function() {
1333
+ $(this.el).empty();
1334
+
1335
+ this._observeModel(this.render);
1336
+ this._observeCollection(this.render);
1337
+
1338
+ // create a new list of items
1339
+ var list = $.el.ul();
1340
+
1341
+ // add entry for the empty model if it exists
1342
+ if(!!this.options.emptyItem) {
1343
+ this._addItemToMenu(list, this.options.emptyItem);
1344
+ }
1345
+
1346
+ var selectedItem = this._determineSelectedItem();
1347
+
1348
+ _(this._collectionArray()).each(function(item) {
1349
+ var selectedValue = this._valueForItem(selectedItem);
1350
+ var itemValue = this._valueForItem(item);
1351
+ this._addItemToMenu(list, item, _(selectedValue).isEqual(itemValue));
1352
+ }, this);
1353
+
1354
+ // wrap them up in a scroller
1355
+ this.scroller = new Backbone.UI.Scroller({
1356
+ content : list
1357
+ }).render();
1358
+
1359
+ // Prevent scroll events from percolating out to the enclosing doc
1360
+ $(this.scroller.el).bind('mousewheel', function(){return false;});
1361
+ $(this.scroller.el).addClass('menu_scroller');
1362
+
1363
+ this.el.appendChild(this.scroller.el);
1364
+
1365
+ return this;
1366
+ },
1367
+
1368
+ scrollToSelectedItem : function() {
1369
+ var pos = !this._selectedAnchor ? 0 :
1370
+ $(this._selectedAnchor.parentNode).position().top - 10;
1371
+ this.scroller.setScrollPosition(pos);
1372
+ },
1373
+
1374
+ width : function() {
1375
+ return $(this.scroller.el).innerWidth();
1376
+ },
1377
+
1378
+ // Adds the given item (creating a new li element)
1379
+ // to the given menu ul element
1380
+ _addItemToMenu : function(menu, item, select) {
1381
+ var anchor = $.el.a({href : '#'});
1382
+
1383
+ var liElement = $.el.li(anchor);
1384
+ $.el.span(this._labelForItem(item) || '\u00a0').appendTo(anchor);
1385
+
1386
+ var clickFunction = _.bind(function(e, silent) {
1387
+ if(!!this._selectedAnchor) $(this._selectedAnchor).removeClass('selected');
1388
+
1389
+ this._setSelectedItem(_(item).isEqual(this.options.emptyItem) ? null : item, silent);
1390
+ this._selectedAnchor = anchor;
1391
+ $(anchor).addClass('selected');
1392
+
1393
+ if(_(this.options.onChange).isFunction()) this.options.onChange(item);
1394
+ return false;
1395
+ }, this);
1396
+
1397
+ $(anchor).click(clickFunction);
1398
+
1399
+ if(select) clickFunction(null, true);
1400
+
1401
+ menu.appendChild(liElement);
1402
+ },
1403
+
1404
+ _labelForItem : function(item) {
1405
+ return !_(item).exists() ? this.options.placeholder :
1406
+ this.resolveContent(item, this.options.altLabelContent);
1407
+ }
1408
+ });
1409
+ }());
1410
+ (function(){
1411
+ window.Backbone.UI.Pulldown = Backbone.View.extend({
1412
+ options : {
1413
+ // text to place in the pulldown button before a
1414
+ // selection has been made
1415
+ placeholder : 'Select...',
1416
+
1417
+ // If true, the menu will be aligned to the right side
1418
+ alignRight : false,
1419
+
1420
+ // A callback to invoke with a particular item when that item is
1421
+ // selected from the pulldown menu.
1422
+ onChange : Backbone.UI.noop,
1423
+
1424
+ // A callback to invoke when the pulldown menu is shown, passing the
1425
+ // button click event.
1426
+ onMenuShow : Backbone.UI.noop,
1427
+
1428
+ // A callback to invoke when the pulldown menu is hidden, if the menu was hidden
1429
+ // as a result of a second click on the pulldown button, the button click event
1430
+ // will be passed.
1431
+ onMenuHide : Backbone.UI.noop,
1432
+
1433
+ // an additional item to render at the top of the menu to
1434
+ // denote the lack of a selection
1435
+ emptyItem : null
1436
+ },
1437
+
1438
+ initialize : function() {
1439
+ $(this.el).addClass('pulldown');
1440
+
1441
+ var onChange = this.options.onChange;
1442
+ delete(this.options.onChange);
1443
+ var menuOptions = _(this.options).extend({
1444
+ onChange : _(function(item){
1445
+ this._onItemSelected(item);
1446
+ if(_(onChange).isFunction()) onChange(item);
1447
+ }).bind(this)
1448
+ });
1449
+
1450
+ this._menu = new Backbone.UI.Menu(menuOptions).render();
1451
+ $(this._menu.el).autohide({
1452
+ ignoreKeys : [Backbone.UI.KEYS.KEY_UP, Backbone.UI.KEYS.KEY_DOWN],
1453
+ ignoreInputs : false,
1454
+ hideCallback : _.bind(this._onAutoHide, this)
1455
+ });
1456
+ $(this._menu.el).hide();
1457
+ document.body.appendChild(this._menu.el);
1458
+
1459
+ // observe model changes
1460
+ if(_(this.model).exists() && _(this.model.bind).isFunction()) {
1461
+ this.model.unbind('change', _(this.render).bind(this));
1462
+
1463
+ // observe model changes
1464
+ if(_(this.options.content).exists()) {
1465
+ this.model.bind('change:' + this.options.content, _(this.render).bind(this));
1466
+ }
1467
+ }
1468
+
1469
+ // observe collection changes
1470
+ if(_(this.options.alternatives).exists() && _(this.options.alternatives.bind).isFunction()) {
1471
+ this.options.alternatives.unbind('all', _(this.render).bind(this));
1472
+ this.options.alternatives.bind('all', _(this.render).bind(this));
1473
+ }
1474
+ },
1475
+
1476
+ // public accessors
1477
+ button : null,
1478
+
1479
+ render : function() {
1480
+ $(this.el).empty();
1481
+
1482
+ var item = this._menu.selectedItem;
1483
+ var label = this._labelForItem(item);
1484
+ this.button = new Backbone.UI.Button({
1485
+ className : 'pulldown_button',
1486
+ model : {label : this._labelForItem(item)},
1487
+ content : 'label',
1488
+ onClick : _.bind(this.showMenu, this)
1489
+ }).render();
1490
+ this.el.appendChild(this.button.el);
1491
+
1492
+ return this;
1493
+ },
1494
+
1495
+ setEnabled : function(enabled) {
1496
+ if(this.button) this.button.setEnabled(enabled);
1497
+ },
1498
+
1499
+ _labelForItem : function(item) {
1500
+ return !_(item).exists() ? this.options.placeholder :
1501
+ this.resolveContent(item, this.options.altLabelContent);
1502
+ },
1503
+
1504
+ // sets the selected item
1505
+ setSelectedItem : function(item) {
1506
+ this._setSelectedItem(item);
1507
+ this.button.options.label = this._labelForItem(item);
1508
+ this.button.render();
1509
+ },
1510
+
1511
+ // Forces the menu to hide
1512
+ hideMenu : function(event) {
1513
+ $(this._menu.el).hide();
1514
+ if(this.options.onMenuHide) this.options.onMenuHide(event);
1515
+ },
1516
+
1517
+ //forces the menu to show
1518
+ showMenu : function(e) {
1519
+ var anchor = this.button.el;
1520
+ var showOnTop = $(window).height() - ($(anchor).offset().top - document.body.scrollTop) < 150;
1521
+ var position = (this.options.alignRight ? '-right' : '-left') + (showOnTop ? 'top' : ' bottom');
1522
+ $(this._menu.el).alignTo(anchor, position, 0, 1);
1523
+ $(this._menu.el).show();
1524
+
1525
+ this._menuWidth = this._menuWidth || this._menu.width();
1526
+ var buttonWidth = $(this.button.el).innerWidth();
1527
+ $(this._menu.el).css({width : Math.max(this._menuWidth, buttonWidth)});
1528
+ if(this.options.onMenuShow) this.options.onMenuShow(e);
1529
+ this._menu.scrollToSelectedItem();
1530
+ },
1531
+
1532
+ _onItemSelected : function(item) {
1533
+ if(!!this.button) {
1534
+ $(this.el).removeClass('placeholder');
1535
+ this.button.model = {label : this._labelForItem(item)};
1536
+ this.button.render();
1537
+ this.hideMenu();
1538
+ }
1539
+ },
1540
+
1541
+ // notify of the menu hiding
1542
+ _onAutoHide : function() {
1543
+ if(this.options.onMenuHide) this.options.onMenuHide();
1544
+ return true;
1545
+ }
1546
+
1547
+ });
1548
+ }());
1549
+ (function(){
1550
+ window.Backbone.UI.RadioGroup = Backbone.View.extend({
1551
+
1552
+ options : {
1553
+ // A callback to invoke with the selected item whenever the selection changes
1554
+ onChange : Backbone.UI.noop
1555
+ },
1556
+
1557
+ initialize : function() {
1558
+ this.mixin([Backbone.UI.HasModel, Backbone.UI.HasAlternativeProperty]);
1559
+ _(this).bindAll('render');
1560
+
1561
+ $(this.el).addClass('radio_group');
1562
+ if(this.options.name){
1563
+ $(this.el).addClass(this.options.name);
1564
+ }
1565
+ },
1566
+
1567
+ // public accessors
1568
+ selectedItem : null,
1569
+
1570
+ render : function() {
1571
+
1572
+ $(this.el).empty();
1573
+
1574
+ this._observeModel(this.render);
1575
+ this._observeCollection(this.render);
1576
+
1577
+ this.selectedItem = this._determineSelectedItem() || this.selectedItem;
1578
+
1579
+ var ul = $.el.ul();
1580
+ var selectedValue = this._valueForItem(this.selectedItem);
1581
+ _(this._collectionArray()).each(function(item) {
1582
+
1583
+ var selected = selectedValue === this._valueForItem(item);
1584
+
1585
+ var label = this.resolveContent(item, this.options.altLabelContent);
1586
+ if(label.nodeType === 1) {
1587
+ $('a',label).click(function(e){
1588
+ e.stopPropagation();
1589
+ });
1590
+ }
1591
+
1592
+ var li = $.el.li(
1593
+ $.el.a({className : 'choice' + (selected ? ' selected' : '')},
1594
+ $.el.div({className : 'mark' + (selected ? ' selected' : '')},
1595
+ selected ? '\u25cf' : '\u00a0')));
1596
+
1597
+ // insert label into li then add to ul
1598
+ $.el.div({className : 'label'}, label).appendTo(li);
1599
+ ul.appendChild(li);
1600
+
1601
+ $(li).bind('click', _.bind(this._onChange, this, item));
1602
+
1603
+ }, this);
1604
+ this.el.appendChild(ul);
1605
+
1606
+ return this;
1607
+ },
1608
+
1609
+ _onChange : function(item) {
1610
+ this._setSelectedItem(item);
1611
+ this.render();
1612
+
1613
+ if(_(this.options.onChange).isFunction()) this.options.onChange(item);
1614
+ return false;
1615
+ }
1616
+ });
1617
+ }());
1618
+ (function(){
1619
+
1620
+ window.Backbone.UI.Scroller = Backbone.View.extend({
1621
+ options : {
1622
+ className : 'scroller',
1623
+
1624
+ // The content to be scrolled. This element should be
1625
+ // of a fixed height.
1626
+ content : null,
1627
+
1628
+ // The amount to scroll on each wheel click
1629
+ scrollAmount : 5,
1630
+
1631
+ // A callback to invoke when scrolling occurs
1632
+ onScroll : null
1633
+ },
1634
+
1635
+ initialize : function() {
1636
+ Backbone.UI.DragSession.enableBasicDragSupport();
1637
+ setInterval(_(this.update).bind(this), 40);
1638
+ },
1639
+
1640
+ render : function () {
1641
+ $(this.el).empty();
1642
+ $(this.el).addClass('scroller');
1643
+
1644
+ this._scrollContent = this.options.content;
1645
+ $(this._scrollContent).addClass('content');
1646
+
1647
+ this._knob = $.el.div({className : 'knob'},
1648
+ $.el.div({className : 'knob_top'}),
1649
+ $.el.div({className : 'knob_middle'}),
1650
+ $.el.div({className : 'knob_bottom'}));
1651
+
1652
+ this._tray = $.el.div({className : 'tray'});
1653
+ this._tray.appendChild(this._knob);
1654
+
1655
+ // for firefox on windows we need to wrap the scroller content in an overflow
1656
+ // auto div to avoid a rendering bug that causes artifacts on the screen when
1657
+ // the hidden content is scrolled...wsb
1658
+ this._scrollContentWrapper = $.el.div({className : 'content_wrapper'});
1659
+ this._scrollContentWrapper.appendChild(this._scrollContent);
1660
+
1661
+ this.el.appendChild(this._tray);
1662
+ this.el.appendChild(this._scrollContentWrapper);
1663
+
1664
+ // FF workaround: Set tabIndex so the user can click on the div to give
1665
+ // it focus (which allows us to capture the up/down/pageup/pagedown
1666
+ // keys). (And setting it to -1 keeps it out of the tab-navigation
1667
+ // chain)
1668
+ this.el.tabIndex = -1;
1669
+
1670
+ // observe events
1671
+ $(this._knob).bind('mousedown', _.bind(this._onKnobMouseDown, this));
1672
+ $(this._tray).bind('click', _.bind(this._onTrayClick, this));
1673
+ $(this.el).bind('mousewheel', _.bind(this._onMouseWheel, this));
1674
+ $(this.el).bind($.browser.msie ? 'keyup' : 'keypress',
1675
+ _.bind(this._onKeyPress, this));
1676
+
1677
+ // touch events if appropriates
1678
+ if(Backbone.UI.IS_MOBILE) {
1679
+ $(this._scrollContent).css({
1680
+ overflow : 'scroll',
1681
+ '-webkit-overflow-scrolling' : 'touch'
1682
+ });
1683
+ }
1684
+ $(this.el).addClass('disabled');
1685
+
1686
+ return this;
1687
+ },
1688
+
1689
+ // Returns the scroll position as a ratio of position relative to
1690
+ // overall content size. 0 = at top, 1 = at bottom.
1691
+ scrollRatio: function() {
1692
+ return this.scrollPosition()/(this._totalHeight - this._visibleHeight);
1693
+ },
1694
+
1695
+ setScrollRatio: function(ratio) {
1696
+ var overflow = (this._totalHeight - this._visibleHeight);
1697
+ ratio = Math.max(0, Math.min(overflow > 0 ? 1 : 0, ratio));
1698
+ var contentPos = ratio*overflow;
1699
+
1700
+ this._scrollContent.scrollTop = Math.round(contentPos);
1701
+
1702
+ if(this.options.onScroll) this.options.onScroll();
1703
+
1704
+ // FF workaround: with position relative set on the container (needed to
1705
+ // float the scrollbar properly), scrolling performance suh-hucks!
1706
+ // However updating the knob position in a timeout dramatically improves
1707
+ // matters. Don't ask me why!
1708
+ setTimeout(_.bind(this._updateKnobPosition, this), 10);
1709
+ this._updateKnobPosition();
1710
+ },
1711
+
1712
+ // Scrolls the content by the given amount
1713
+ scrollBy: function(amount) {
1714
+ this.setScrollPosition(this.scrollPosition() + amount);
1715
+ },
1716
+
1717
+ // Returns the actual scroll position
1718
+ scrollPosition: function() {
1719
+ return this._scrollContent.scrollTop;
1720
+ },
1721
+
1722
+ setScrollPosition: function(top) {
1723
+ this.update();
1724
+ var h = this._totalHeight - this._visibleHeight;
1725
+ this.setScrollRatio(h ? top/h : 0);
1726
+ this.update();
1727
+ },
1728
+
1729
+ // Scrolls to the end of the content
1730
+ scrollToEnd : function(){
1731
+ this.setScrollRatio(1);
1732
+ },
1733
+
1734
+ // updates and resizes the scrollbar if changes to the scroll
1735
+ update: function() {
1736
+ var visibleHeight = this._scrollContent.offsetHeight;
1737
+ var totalHeight = this._scrollContent.scrollHeight;
1738
+
1739
+ this.maxY = $(this._tray).height() - $(this._knob).height();
1740
+
1741
+ // if either the offset or scroll height has changed
1742
+ if(this._visibleHeight !== visibleHeight || this._totalHeight !== totalHeight) {
1743
+ this._disabled = totalHeight <= visibleHeight + 2;
1744
+ $(this.el).toggleClass('disabled', this._disabled || Backbone.UI.IS_MOBILE);
1745
+ this._visibleHeight = visibleHeight;
1746
+ this._totalHeight = totalHeight;
1747
+
1748
+ // if there's nothing to scroll, we disable the scroll bar
1749
+ if(this._totalHeight >= this._visibleHeight) {
1750
+ this._updateKnobSize();
1751
+ this.minY = 0;
1752
+ }
1753
+ }
1754
+ this._updateKnobPosition();
1755
+ this._updateKnobSize();
1756
+ },
1757
+
1758
+ // Set the position of the knob to reflect the current scroll position
1759
+ _updateKnobPosition: function() {
1760
+ var r = this.scrollRatio();
1761
+ var y = this.minY + (this.maxY-this.minY) * r;
1762
+ if (!isNaN(y)) this._knob.style.top = y + 'px';
1763
+ },
1764
+
1765
+ _updateKnobSize : function(){
1766
+ var knobSize = $(this._tray).height() * (this._visibleHeight/this._totalHeight);
1767
+ knobSize = knobSize > 20 ? knobSize : 20;
1768
+ $(this._knob).css({height : knobSize + 'px'});
1769
+ },
1770
+
1771
+ _knobRatio: function(top) {
1772
+ top = top || this._knob.offsetTop;
1773
+ top = Math.max(this.minY, Math.min(this.maxY, top));
1774
+ return (top-this.minY) / (this.maxY - this.minY);
1775
+ },
1776
+
1777
+ _onTrayClick: function(e) {
1778
+ e = e || event;
1779
+ if(e.target === this._tray) {
1780
+ var y = (e.layerY || e.y);
1781
+ if(!y) y = (e.originalEvent.layerY || e.originalEvent.y);
1782
+ y = y - this._knob.offsetHeight/2;
1783
+ this.setScrollRatio(this._knobRatio(y));
1784
+ }
1785
+ e.stopPropagation();
1786
+ },
1787
+
1788
+ _onKnobMouseDown : function(e) {
1789
+ this.el.focus();
1790
+ var ds = new Backbone.UI.DragSession({
1791
+ dragEvent : e,
1792
+ scope : this.el.ownerDocument,
1793
+
1794
+ onStart : _.bind(function(ds) {
1795
+ // Cache starting position of the knob
1796
+ ds.pos = this._knob.offsetTop;
1797
+ ds.scroller = this;
1798
+ $(this.el).addClass('dragging');
1799
+ }, this),
1800
+
1801
+ onMove : _.bind(function(ds) {
1802
+ var ratio = this._knobRatio(ds.pos + ds.dy);
1803
+ this.setScrollRatio(ratio);
1804
+ }, this),
1805
+
1806
+ onStop : _.bind(function(ds) {
1807
+ $(this.el).removeClass('dragging');
1808
+ }, this)
1809
+ });
1810
+ e.stopPropagation();
1811
+ },
1812
+
1813
+ _onMouseWheel: function(e, delta, deltaX, deltaY) {
1814
+ if(!this._disabled) {
1815
+ var step = this.options.scrollAmount;
1816
+ this.setScrollPosition(this.scrollPosition() - delta*step);
1817
+ e.preventDefault();
1818
+ return false;
1819
+ }
1820
+ },
1821
+
1822
+ _onKeyPress : function(e) {
1823
+ switch (e.keyCode) {
1824
+ case Backbone.UI.KEYS.KEY_DOWN:
1825
+ this.scrollBy(this.options.scrollAmount);
1826
+ break;
1827
+ case Backbone.UI.KEYS.KEY_UP:
1828
+ this.scrollBy(-this.options.scrollAmount);
1829
+ break;
1830
+ case Backbone.UI.KEYS.KEY_PAGEDOWN:
1831
+ this.scrollBy(this.options.scrollAmount);
1832
+ break;
1833
+ case Backbone.UI.KEYS.KEY_PAGEUP:
1834
+ this.scrollBy(-this.options.scrollAmount);
1835
+ break;
1836
+ case Backbone.UI.KEYS.KEY_HOME:
1837
+ this.setScrollRatio(0);
1838
+ break;
1839
+ case Backbone.UI.KEYS.KEY_END:
1840
+ this.setScrollRatio(1);
1841
+ break;
1842
+ default:
1843
+ return;
1844
+ }
1845
+ e.stopPropagation();
1846
+ e.preventDefault();
1847
+ }
1848
+ });
1849
+ }());
1850
+
1851
+
1852
+
1853
+ (function() {
1854
+ Backbone.UI.TabSet = Backbone.View.extend({
1855
+ options : {
1856
+ // Tabs to initially add to this tab set. Each entry may contain
1857
+ // a <code>label</code>, <code>content</code>, and <code>onActivate</code>
1858
+ // option.
1859
+ alternatives : [],
1860
+
1861
+ // The index of the tab to initially select
1862
+ selectedTab : 0
1863
+ },
1864
+
1865
+ initialize : function() {
1866
+ $(this.el).addClass('tab_set');
1867
+ },
1868
+
1869
+ render : function() {
1870
+ $(this.el).empty();
1871
+
1872
+ this._tabs = [];
1873
+ this._contents = [];
1874
+ this._callbacks = [];
1875
+ this._tabBar = $.el.div({className : 'tab_bar'});
1876
+ this._contentContainer = $.el.div({className : 'content_container'});
1877
+ this.el.appendChild(this._tabBar);
1878
+ this.el.appendChild(this._contentContainer);
1879
+
1880
+ for(var i=0; i<this.options.alternatives.length; i++) {
1881
+ this.addTab(this.options.alternatives[i]);
1882
+ }
1883
+
1884
+ if(this.options.selectedTab >= 0){
1885
+ this.activateTab(this.options.selectedTab);
1886
+ }
1887
+ else{
1888
+ $(this.el).addClass('no_selection');
1889
+ }
1890
+
1891
+ return this;
1892
+ },
1893
+
1894
+ addTab : function(tabOptions) {
1895
+ var tab = $.el.a({href : '#', className : 'tab'});
1896
+ if(tabOptions.className) $(tab).addClass(tabOptions.className);
1897
+
1898
+ var label = this.resolveContent(null, tabOptions.label);
1899
+ tab.appendChild(_(label).isString() ? document.createTextNode(label || '') : label);
1900
+
1901
+ this._tabBar.appendChild(tab);
1902
+ this._tabs.push(tab);
1903
+
1904
+ var content = !!tabOptions.content && !!tabOptions.content.nodeType ?
1905
+ tabOptions.content :
1906
+ $.el.div(tabOptions.content);
1907
+ this._contents.push(content);
1908
+ $(content).hide();
1909
+ this._contentContainer.appendChild(content);
1910
+
1911
+ // observe tab clicks
1912
+ var index = this._tabs.length - 1;
1913
+ $(tab).bind('click', _.bind(function() {
1914
+ this.activateTab(index);
1915
+ return false;
1916
+ }, this));
1917
+
1918
+ this._callbacks.push(tabOptions.onActivate || Backbone.UI.noop);
1919
+ },
1920
+
1921
+ activateTab : function(index) {
1922
+
1923
+ $(this.el).removeClass('no_selection');
1924
+
1925
+ // hide all content panels
1926
+ _(this._contents).each(function(content) {
1927
+ $(content).hide();
1928
+ });
1929
+
1930
+ // de-select all tabs
1931
+ _(this._tabs).each(function(tab) {
1932
+ $(tab).removeClass('selected');
1933
+ });
1934
+
1935
+ if(_(this._selectedIndex).exists()) {
1936
+ $(this.el).removeClass('index_' + this._selectedIndex);
1937
+ }
1938
+ $(this.el).addClass('index_' + index);
1939
+ this._selectedIndex = index;
1940
+
1941
+ // select the appropriate tab
1942
+ $(this._tabs[index]).addClass('selected');
1943
+
1944
+ // show the proper contents
1945
+ $(this._contents[index]).show();
1946
+
1947
+ this._callbacks[index]();
1948
+ }
1949
+ });
1950
+ }());
1951
+
1952
+ (function(){
1953
+ window.Backbone.UI.TableView = Backbone.UI.CollectionView.extend({
1954
+ options : {
1955
+ // Each column should contain a <code>title</code> property to
1956
+ // describe the column's heading, a <code>content</code> property to
1957
+ // declare which property the cell is bound to, an optional two-argument
1958
+ // <code>comparator</code> with which to sort each column if the
1959
+ // table is sortable, and an optional <code>width</code> property to
1960
+ // declare the width of the column in pixels.
1961
+ columns : [],
1962
+
1963
+ // A string, element, or function describing what should be displayed
1964
+ // when the table is empty.
1965
+ emptyContent : 'no entries',
1966
+
1967
+ // A callback to invoke when a row is clicked. If this callback
1968
+ // is present, the rows will highlight on hover.
1969
+ onItemClick : Backbone.UI.noop,
1970
+
1971
+ // Clicking on the column headers will sort the table. See
1972
+ // <code>comparator</code> property description on columns.
1973
+ // The table is sorted by the first column by default.
1974
+ sortable : false,
1975
+
1976
+ // A callback to invoke when the table is to be sorted. The callback will
1977
+ // be passed the <code>column</code> on which to sort.
1978
+ onSort : null
1979
+ },
1980
+
1981
+ initialize : function() {
1982
+ Backbone.UI.CollectionView.prototype.initialize.call(this, arguments);
1983
+ $(this.el).addClass('table_view');
1984
+ this._sortState = {reverse : true};
1985
+ },
1986
+
1987
+ render : function() {
1988
+ $(this.el).empty();
1989
+ this.itemViews = {};
1990
+
1991
+ var container = $.el.div({className : 'content'},
1992
+ this.collectionEl = $.el.table({
1993
+ cellPadding : '0',
1994
+ cellSpacing : '0'
1995
+ }));
1996
+
1997
+ $(this.el).toggleClass('clickable', this.options.onItemClick !== Backbone.UI.noop);
1998
+
1999
+ // generate a table row for our headings
2000
+ var headingRow = $.el.tr();
2001
+ var sortFirstColumn = false;
2002
+ var firstHeading = null;
2003
+ _(this.options.columns).each(_(function(column, index, list) {
2004
+ var label = _(column.title).isFunction() ? column.title() : column.title;
2005
+ var width = !!column.width ? parseInt(column.width, 10) + 5 : null;
2006
+ var style = width ? 'width:' + width + 'px; max-width:' + width + 'px; ' : '';
2007
+ style += this.options.sortable ? 'cursor: pointer; ' : '';
2008
+ column.comparator = _(column.comparator).isFunction() ? column.comparator : function(item1, item2) {
2009
+ return item1.get(column.content) < item2.get(column.content) ? -1 :
2010
+ item1.get(column.content) > item2.get(column.content) ? 1 : 0;
2011
+ };
2012
+ var firstSort = (sortFirstColumn && firstHeading === null);
2013
+ var sortHeader = this._sortState.content === column.content || firstSort;
2014
+ var sortLabel = $.el.div({
2015
+ className : 'glyph'
2016
+ }, sortHeader ? (this._sortState.reverse && !firstSort ? '\u25b2 ' : '\u25bc ') : '');
2017
+
2018
+ var onclick = this.options.sortable ? (_(this.options.onSort).isFunction() ?
2019
+ _(function(e) { this.options.onSort(column); }).bind(this) :
2020
+ _(function(e, silent) { this._sort(column, silent); }).bind(this)) : Backbone.UI.noop;
2021
+
2022
+ var th = $.el.th({
2023
+ className : _(list).nameForIndex(index),
2024
+ style : style,
2025
+ onclick : onclick
2026
+ },
2027
+ sortLabel,
2028
+ $.el.div({
2029
+ className : 'wrapper' + (sortHeader ? ' sorted' : '')
2030
+ }, label)).appendTo(headingRow);
2031
+
2032
+ if (firstHeading === null) firstHeading = th;
2033
+ }).bind(this));
2034
+ if (sortFirstColumn && !!firstHeading) {
2035
+ firstHeading.onclick(null, true);
2036
+ }
2037
+
2038
+ // Add the heading row to it's very own table so we can allow the
2039
+ // actual table to scroll with a fixed heading.
2040
+ this.el.appendChild($.el.table({
2041
+ className : 'heading',
2042
+ cellPadding : '0',
2043
+ cellSpacing : '0'
2044
+ }, $.el.thead(headingRow)));
2045
+
2046
+ // now we'll generate the body of the content table, with a row
2047
+ // for each model in the bound collection
2048
+ var tableBody = $.el.tbody();
2049
+ this.collectionEl.appendChild(tableBody);
2050
+
2051
+ // if the collection is empty, we render the empty content
2052
+ if(!_(this.model).exists() || this.model.length === 0) {
2053
+ this._emptyContent = _(this.options.emptyContent).isFunction() ?
2054
+ this.options.emptyContent() : this.options.emptyContent;
2055
+ this._emptyContent = $.el.tr($.el.td(this._emptyContent));
2056
+
2057
+ if(!!this._emptyContent) {
2058
+ tableBody.appendChild(this._emptyContent);
2059
+ }
2060
+ }
2061
+
2062
+ // otherwise, we render each row
2063
+ else {
2064
+ _(this.model.models).each(function(model, index) {
2065
+ var item = this._renderItem(model, index);
2066
+ tableBody.appendChild(item);
2067
+ }, this);
2068
+ }
2069
+
2070
+ // wrap the list in a scroller
2071
+ if(_(this.options.maxHeight).exists()) {
2072
+ var style = 'max-height:' + this.options.maxHeight + 'px';
2073
+ var scroller = new Backbone.UI.Scroller({
2074
+ content : $.el.div({style : style}, container)
2075
+ }).render();
2076
+
2077
+ this.el.appendChild(scroller.el);
2078
+ }
2079
+ else {
2080
+ this.el.appendChild(container);
2081
+ }
2082
+
2083
+ this._updateClassNames();
2084
+
2085
+ return this;
2086
+ },
2087
+
2088
+ _renderItem : function(model, index) {
2089
+ var row = $.el.tr();
2090
+
2091
+ // for each model, we walk through each column and generate the content
2092
+ _(this.options.columns).each(function(column, index, list) {
2093
+ var width = !!column.width ? parseInt(column.width, 10) + 5 : null;
2094
+ var style = width ? 'width:' + width + 'px; max-width:' + width + 'px': null;
2095
+ var content = this.resolveContent(model, column.content);
2096
+ row.appendChild($.el.td({
2097
+ className : _(list).nameForIndex(index),
2098
+ style : style
2099
+ }, $.el.div({className : 'wrapper', style : style}, content)));
2100
+ }, this);
2101
+
2102
+ // bind the item click callback if given
2103
+ if(this.options.onItemClick) {
2104
+ $(row).click(_(this.options.onItemClick).bind(this, model));
2105
+ }
2106
+
2107
+ this.itemViews[model.cid] = row;
2108
+ return row;
2109
+ },
2110
+
2111
+ _sort : function(column, silent) {
2112
+ this._sortState.reverse = !this._sortState.reverse;
2113
+ this._sortState.content = column.content;
2114
+ var comp = column.comparator;
2115
+ if (this._sortState.reverse) {
2116
+ comp = function(item1, item2) {
2117
+ return -column.comparator(item1, item2);
2118
+ };
2119
+ }
2120
+ this.model.comparator = comp;
2121
+ this.model.sort({silent : !!silent});
2122
+ }
2123
+ });
2124
+ }());
2125
+
2126
+ (function(){
2127
+ window.Backbone.UI.TextArea = Backbone.View.extend({
2128
+ options : {
2129
+ className : 'text_area',
2130
+
2131
+ // id to use on the actual textArea
2132
+ textAreaId : null,
2133
+
2134
+ // disables the text area
2135
+ disabled : false,
2136
+
2137
+ enableScrolling : true,
2138
+
2139
+ tabIndex : null
2140
+ },
2141
+
2142
+ // public accessors
2143
+ textArea : null,
2144
+
2145
+ initialize : function() {
2146
+ this.mixin([Backbone.UI.HasModel]);
2147
+
2148
+ $(this.el).addClass('text_area');
2149
+ if(this.options.name){
2150
+ $(this.el).addClass(this.options.name);
2151
+ }
2152
+ },
2153
+
2154
+ render : function() {
2155
+ var value = (this.textArea && this.textArea.value.length) > 0 ?
2156
+ this.textArea.value : this.resolveContent();
2157
+
2158
+ $(this.el).empty();
2159
+
2160
+ this.textArea = $.el.textarea({
2161
+ id : this.options.textAreaId,
2162
+ tabIndex : this.options.tabIndex,
2163
+ placeholder : this.options.placeholder}, value);
2164
+
2165
+ var content = this.textArea;
2166
+ if(this.options.enableScrolling) {
2167
+ this._scroller = new Backbone.UI.Scroller({
2168
+ content : this.textArea
2169
+ }).render();
2170
+ content = this._scroller.el;
2171
+ }
2172
+
2173
+ this.el.appendChild(content);
2174
+
2175
+ this.setEnabled(!this.options.disabled);
2176
+
2177
+ $(this.textArea).keyup(_.bind(function() {
2178
+ _.defer(_(this._updateModel).bind(this));
2179
+ }, this));
2180
+
2181
+ return this;
2182
+ },
2183
+
2184
+ getValue : function() {
2185
+ return this.textArea.value;
2186
+ },
2187
+
2188
+ setValue : function(value) {
2189
+ $(this.textArea).empty();
2190
+ this.textArea.value = value;
2191
+ this._updateModel();
2192
+ },
2193
+
2194
+ // sets the enabled state
2195
+ setEnabled : function(enabled) {
2196
+ if(enabled) {
2197
+ $(this.el).removeClass('disabled');
2198
+ } else {
2199
+ $(this.el).addClass('disabled');
2200
+ }
2201
+ this.textArea.disabled = !enabled;
2202
+ },
2203
+
2204
+ _updateModel : function() {
2205
+ _(this.model).setProperty(this.options.content, this.textArea.value);
2206
+ }
2207
+ });
2208
+ }());
2209
+ (function(){
2210
+ window.Backbone.UI.TextField = Backbone.View.extend({
2211
+ options : {
2212
+ // disables the input text
2213
+ disabled : false,
2214
+
2215
+ // The type of input (text, password, number, email, etc.)
2216
+ type : 'text',
2217
+
2218
+ // the value to use for both the name and id attribute
2219
+ // of the underlying input element
2220
+ name : null,
2221
+
2222
+ // the tab index to set on the underlying input field
2223
+ tabIndex : null,
2224
+
2225
+ // a callback to invoke when a key is pressed within the text field
2226
+ onKeyPress : Backbone.UI.noop,
2227
+
2228
+ // if given, the text field will limit it's character count
2229
+ maxLength : null
2230
+ },
2231
+
2232
+ // public accessors
2233
+ input : null,
2234
+
2235
+ initialize : function() {
2236
+ this.mixin([Backbone.UI.HasModel]);
2237
+ _(this).bindAll('_refreshValue');
2238
+
2239
+ $(this.el).addClass('text_field');
2240
+ if(this.options.name){
2241
+ $(this.el).addClass(this.options.name);
2242
+ }
2243
+
2244
+ this.input = $.el.input({maxLength : this.options.maxLength});
2245
+
2246
+ $(this.input).keyup(_.bind(function(e) {
2247
+ this._updateModel();
2248
+ if(_(this.options.onKeyPress).exists() && _(this.options.onKeyPress).isFunction()) {
2249
+ this.options.onKeyPress(e, this);
2250
+ }
2251
+ }, this));
2252
+
2253
+ this._observeModel(this._refreshValue);
2254
+ },
2255
+
2256
+ render : function() {
2257
+ var value = (this.input && this.input.value.length) > 0 ?
2258
+ this.input.value : this.resolveContent();
2259
+
2260
+ $(this.el).empty();
2261
+
2262
+ $(this.input).attr({
2263
+ type : this.options.type ? this.options.type : 'text',
2264
+ name : this.options.name,
2265
+ id : this.options.name,
2266
+ tabIndex : this.options.tabIndex,
2267
+ placeholder : this.options.placeholder,
2268
+ value : value});
2269
+
2270
+ // insert text_wrapper
2271
+ this.el.appendChild($.el.div({className : 'input_wrapper'}, this.input));
2272
+
2273
+ this.setEnabled(!this.options.disabled);
2274
+
2275
+ return this;
2276
+ },
2277
+
2278
+ getValue : function() {
2279
+ return this.input.value;
2280
+ },
2281
+
2282
+ setValue : function(value) {
2283
+ this.input.value = value;
2284
+ this._updateModel();
2285
+ },
2286
+
2287
+ // sets the enabled state
2288
+ setEnabled : function(enabled) {
2289
+ if(enabled) {
2290
+ $(this.el).removeClass('disabled');
2291
+ } else {
2292
+ $(this.el).addClass('disabled');
2293
+ }
2294
+ this.input.disabled = !enabled;
2295
+ },
2296
+
2297
+ _updateModel : function() {
2298
+ _(this.model).setProperty(this.options.content, this.input.value);
2299
+ },
2300
+
2301
+ _refreshValue : function() {
2302
+ var newValue = this.resolveContent();
2303
+ if(this.input && this.input.value !== newValue) {
2304
+ this.input.value = _(newValue).exists() ? newValue : null;
2305
+ }
2306
+ }
2307
+ });
2308
+ }());
2309
+
2310
+ (function(){
2311
+ window.Backbone.UI.TimePicker = Backbone.View.extend({
2312
+
2313
+ options : {
2314
+ // a moment.js format : http://momentjs.com/docs/#/display/format
2315
+ format : 'hh:mm a',
2316
+
2317
+ // minute interval to use for pulldown menu
2318
+ interval : 30,
2319
+
2320
+ // the name given to the text field's input element
2321
+ name : null,
2322
+
2323
+ // text field is disabled or enabled
2324
+ disabled : false
2325
+ },
2326
+
2327
+ initialize : function() {
2328
+ $(this.el).addClass('time_picker');
2329
+
2330
+ this._timeModel = {};
2331
+ this._menu = new Backbone.UI.Menu({
2332
+ model : this._timeModel,
2333
+ altLabelContent : 'label',
2334
+ altValueContent : 'label',
2335
+ content : 'value',
2336
+ onChange : _(this._onSelectTimeItem).bind(this)
2337
+ });
2338
+ $(this._menu.el).hide();
2339
+ $(this._menu.el).autohide({
2340
+ ignoreInputs : true
2341
+ });
2342
+ document.body.appendChild(this._menu.el);
2343
+
2344
+ // listen for model changes
2345
+ if(!!this.model && this.options.content) {
2346
+ this.model.bind('change:' + this.options.content, _(this.render).bind(this));
2347
+ }
2348
+ },
2349
+
2350
+ render : function() {
2351
+ $(this.el).empty();
2352
+
2353
+ this._textField = new Backbone.UI.TextField({
2354
+ name : this.options.name,
2355
+ disabled : this.options.disabled
2356
+ }).render();
2357
+ $(this._textField.input).click(_(this._showMenu).bind(this));
2358
+ $(this._textField.input).keyup(_(this._timeEdited).bind(this));
2359
+ this.el.appendChild(this._textField.el);
2360
+
2361
+ var date = this.resolveContent();
2362
+
2363
+ if(!!date) {
2364
+ var value = moment(date).format(this.options.format);
2365
+ this._textField.setValue(value);
2366
+ this._timeModel.value = value;
2367
+ this._selectedTime = date;
2368
+ }
2369
+
2370
+ this._menu.options.alternatives = this._collectTimes();
2371
+ this._menu.options.model = this._timeModel;
2372
+ this._menu.render();
2373
+
2374
+ return this;
2375
+ },
2376
+
2377
+ getValue : function() {
2378
+ return this._selectedTime;
2379
+ },
2380
+
2381
+ setValue : function(time) {
2382
+ this._selectedTime = time;
2383
+ var timeString = moment(time).format(this.options.format);
2384
+ this._textField.setValue(timeString);
2385
+ this._timeEdited();
2386
+
2387
+ this._menu.options.selectedValue = time;
2388
+ this._menu.render();
2389
+ },
2390
+
2391
+ setEnabled : function(enabled) {
2392
+ this.options.disabled = !enabled;
2393
+ this._textField.setEnabled(enabled);
2394
+ },
2395
+
2396
+ _collectTimes : function() {
2397
+ var collection = [];
2398
+ var d = moment().sod();
2399
+ var day = d.date();
2400
+
2401
+ while(d.date() === day) {
2402
+ collection.push({
2403
+ label : d.format(this.options.format),
2404
+ value : new Date(d)
2405
+ });
2406
+
2407
+ d.add('minutes', this.options.interval);
2408
+ }
2409
+
2410
+ return collection;
2411
+ },
2412
+
2413
+ _showMenu : function() {
2414
+ $(this._menu.el).alignTo(this._textField.el, 'bottom -left', 0, 2);
2415
+ $(this._menu.el).show();
2416
+ this._menu.scrollToSelectedItem();
2417
+ },
2418
+
2419
+ _hideMenu : function() {
2420
+ $(this._menu.el).hide();
2421
+ },
2422
+
2423
+ _onSelectTimeItem : function(item) {
2424
+ this._hideMenu();
2425
+ this._selectedTime = item.value;
2426
+ this._textField.setValue(moment(this._selectedTime).format(this.options.format));
2427
+ this._timeEdited();
2428
+ },
2429
+
2430
+ _timeEdited : function(e) {
2431
+ var newDate = moment(this._textField.getValue(), this.options.format);
2432
+
2433
+ // if the enter key was pressed or we've invoked this method manually,
2434
+ // we hide the calendar and re-format our date
2435
+ if(!e || e.keyCode === Backbone.UI.KEYS.KEY_RETURN) {
2436
+ var newValue = moment(newDate).format(this.options.format);
2437
+ this._textField.setValue(newValue);
2438
+ this._hideMenu();
2439
+
2440
+ // update our bound model (but only the date portion)
2441
+ if(!!this.model && this.options.content) {
2442
+ var boundDate = this.resolveContent();
2443
+ var updatedDate = new Date(boundDate);
2444
+ updatedDate.setHours(newDate.hours());
2445
+ updatedDate.setMinutes(newDate.minutes());
2446
+ _(this.model).setProperty(this.options.content, updatedDate);
2447
+ }
2448
+
2449
+ if(_(this.options.onChange).isFunction()) {
2450
+ this.options.onChange(newValue);
2451
+ }
2452
+ }
2453
+ }
2454
+ });
2455
+ }());