laces 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CONTRIBUTING.md +38 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +122 -0
- data/LICENSE +21 -0
- data/README.md +5 -0
- data/Rakefile +8 -0
- data/bin/laces +16 -0
- data/features/creating_a_heroku_app.feature +9 -0
- data/features/rake_clean.feature +21 -0
- data/features/skipping_clearance.feature +13 -0
- data/features/step_definitions/gem_steps.rb +5 -0
- data/features/step_definitions/heroku_steps.rb +3 -0
- data/features/step_definitions/shell_steps.rb +55 -0
- data/features/support/bin/heroku +5 -0
- data/features/support/env.rb +15 -0
- data/features/support/fake_heroku.rb +21 -0
- data/laces-0.0.1.gem +0 -0
- data/laces.gemspec +35 -0
- data/lib/laces/actions.rb +35 -0
- data/lib/laces/app_builder.rb +237 -0
- data/lib/laces/generators/app_generator.rb +111 -0
- data/lib/laces/version.rb +3 -0
- data/templates/.DS_Store +0 -0
- data/templates/Gemfile_template +76 -0
- data/templates/HEROKU_README.md +66 -0
- data/templates/Procfile +1 -0
- data/templates/README.md +81 -0
- data/templates/app/assets/imgs/glyphicons-halflings-white.png +0 -0
- data/templates/app/assets/imgs/glyphicons-halflings.png +0 -0
- data/templates/app/assets/javascripts/admin.coffee +20 -0
- data/templates/app/assets/javascripts/application.coffee +21 -0
- data/templates/app/assets/javascripts/lib/actinology.coffee +47 -0
- data/templates/app/assets/javascripts/lib/analytics.js +11 -0
- data/templates/app/assets/javascripts/lib/auth_token_sync.js +17 -0
- data/templates/app/assets/javascripts/lib/backbone-ui.js +2455 -0
- data/templates/app/assets/javascripts/lib/backbone.coffee +27 -0
- data/templates/app/assets/javascripts/lib/backbone/collection.js +249 -0
- data/templates/app/assets/javascripts/lib/backbone/events.js +64 -0
- data/templates/app/assets/javascripts/lib/backbone/helpers.js +68 -0
- data/templates/app/assets/javascripts/lib/backbone/history.js +144 -0
- data/templates/app/assets/javascripts/lib/backbone/model.js +291 -0
- data/templates/app/assets/javascripts/lib/backbone/router.coffee +45 -0
- data/templates/app/assets/javascripts/lib/backbone/sync.coffee +38 -0
- data/templates/app/assets/javascripts/lib/backbone/view.js +150 -0
- data/templates/app/assets/javascripts/lib/backbone_extended.coffee +276 -0
- data/templates/app/assets/javascripts/lib/bootstrap.js +1836 -0
- data/templates/app/assets/javascripts/lib/inflection.js +658 -0
- data/templates/app/assets/javascripts/lib/jquery-ui.js +343 -0
- data/templates/app/assets/javascripts/lib/jquery.hotkeys.js +102 -0
- data/templates/app/assets/javascripts/lib/jquery.js +4 -0
- data/templates/app/assets/javascripts/lib/milk.js.coffee +265 -0
- data/templates/app/assets/javascripts/lib/raw.js +143 -0
- data/templates/app/assets/javascripts/lib/strftime.js +732 -0
- data/templates/app/assets/javascripts/lib/throttle-debounce.js +251 -0
- data/templates/app/assets/javascripts/lib/timeago.js +148 -0
- data/templates/app/assets/javascripts/lib/underscore.js +28 -0
- data/templates/app/assets/styles/application.sass +21 -0
- data/templates/app/assets/styles/layouts/default.sass +15 -0
- data/templates/app/assets/styles/layouts/footer.sass +0 -0
- data/templates/app/assets/styles/layouts/forms.sass +34 -0
- data/templates/app/assets/styles/layouts/header.sass +0 -0
- data/templates/app/assets/styles/layouts/navigation.sass +0 -0
- data/templates/app/assets/styles/lib/backbone-ui.css +580 -0
- data/templates/app/assets/styles/lib/bootstrap.sass +4248 -0
- data/templates/app/assets/styles/pages/home.sass +0 -0
- data/templates/app/assets/styles/sessions/new.sass +0 -0
- data/templates/app/assets/styles/users/activate.sass +0 -0
- data/templates/app/assets/styles/users/new.sass +0 -0
- data/templates/app/assets/styles/users/suspended.sass +0 -0
- data/templates/app/controllers/app_controller.rb +14 -0
- data/templates/app/controllers/pages_controller.rb +2 -0
- data/templates/app/controllers/sessions_controller.rb +2 -0
- data/templates/app/controllers/templating_controller.rb +31 -0
- data/templates/app/controllers/users_controller.rb +2 -0
- data/templates/app/helpers/app_helper.rb +94 -0
- data/templates/app/helpers/users_helper.rb +53 -0
- data/templates/app/models/user.rb +9 -0
- data/templates/app/views/devise/confirmations/new.haml +9 -0
- data/templates/app/views/devise/passwords/edit.haml +11 -0
- data/templates/app/views/devise/passwords/new.haml +9 -0
- data/templates/app/views/devise/registrations/edit.haml +13 -0
- data/templates/app/views/devise/registrations/new.haml +8 -0
- data/templates/app/views/devise/sessions/_form.haml +7 -0
- data/templates/app/views/devise/sessions/new.haml +5 -0
- data/templates/app/views/devise/unlocks/new.haml +7 -0
- data/templates/app/views/layouts/_ascii.haml +0 -0
- data/templates/app/views/layouts/_column.haml +0 -0
- data/templates/app/views/layouts/_content.haml +1 -0
- data/templates/app/views/layouts/_extra.haml +0 -0
- data/templates/app/views/layouts/_footer.haml +9 -0
- data/templates/app/views/layouts/_head.haml +13 -0
- data/templates/app/views/layouts/_header.haml +6 -0
- data/templates/app/views/layouts/application.html.haml +16 -0
- data/templates/app/views/pages/home.haml +1 -0
- data/templates/config/app.yml +29 -0
- data/templates/config/application.erb +28 -0
- data/templates/config/database.yml +32 -0
- data/templates/config/initializers/devise.rb +232 -0
- data/templates/config/initializers/rabl_init.rb +4 -0
- data/templates/config/initializers/setup_mail.rb +13 -0
- data/templates/config/initializers/wrap_parameters.rb +12 -0
- data/templates/db/migrate/user_migration.rb +36 -0
- data/templates/laces_gitignore +9 -0
- data/templates/lib/development_mail_interceptor.rb +7 -0
- data/templates/lib/templating.rb +40 -0
- metadata +225 -0
data/templates/Procfile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
web: bundle exec thin start -p $PORT
|
data/templates/README.md
ADDED
@@ -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!
|
Binary file
|
Binary file
|
@@ -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
|
+
}());
|