neat-rails 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +1 -0
- data/lib/neat/rails/engine.rb +7 -0
- data/lib/neat/rails/version.rb +5 -0
- data/lib/neat-rails.rb +7 -0
- data/neat-rails.gemspec +21 -0
- data/vendor/assets/javascripts/neat/collection_editor.coffee +215 -0
- data/vendor/assets/javascripts/neat/lib/delayed_action.js +49 -0
- data/vendor/assets/javascripts/neat/lib/inflect.js +825 -0
- data/vendor/assets/javascripts/neat/lib/jquery_extensions.coffee +15 -0
- data/vendor/assets/javascripts/neat/lib/paginated_list.js +204 -0
- data/vendor/assets/javascripts/neat/model_editor.coffee +107 -0
- data/vendor/assets/javascripts/neat.js +23 -0
- metadata +75 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0917bb29396d40820cd7ea76add02769a75ebf06
|
4
|
+
data.tar.gz: 215afdec3ef8a2ea45eed909282ce24b33cb573a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 881a5a3ee3673cf59dcb969c1fb92be12f0cc1851e238f7ab4842191ff28bd60667427c97d7479b5665ecd0b8e540412118e9aab8ee8f8741a843671f875a3c4
|
7
|
+
data.tar.gz: a383cfcdfcf454b35d9a5d2d4486a127f2482de763ae076b6472338e05eb3455bc89ece97427f6af4e0fcfe2e7891c753c72a5185608fa179751fce892f66e33
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Robert Lail
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Neat::Rails
|
2
|
+
|
3
|
+
It's like FreightTrain for Backbone. That's pretty neat!
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'neat-rails'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install neat-rails
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/lib/neat-rails.rb
ADDED
data/neat-rails.gemspec
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'neat/rails/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "neat-rails"
|
8
|
+
gem.version = Neat::Rails::VERSION
|
9
|
+
gem.authors = ["Bob Lail", "Luke Booth"]
|
10
|
+
gem.email = ["bob.lailfamily@gmail.com", "luke.booth@cph.org"]
|
11
|
+
gem.description = %q{It allows editing collections and models inline}
|
12
|
+
gem.summary = %q{It's like FreightTrain for Backbone. That's pretty neat!}
|
13
|
+
gem.homepage = ""
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
|
20
|
+
gem.add_development_dependency "rake"
|
21
|
+
end
|
@@ -0,0 +1,215 @@
|
|
1
|
+
KEYS = {
|
2
|
+
13: "RETURN",
|
3
|
+
27: "ESC",
|
4
|
+
38: "UP",
|
5
|
+
40: "DOWN"
|
6
|
+
}
|
7
|
+
|
8
|
+
# Classes that inherit from EditableCollectionView
|
9
|
+
# must define one property:
|
10
|
+
# resource
|
11
|
+
#
|
12
|
+
# Can optionally define
|
13
|
+
# modelView - by default this is set to {Resource}View
|
14
|
+
# template - by default this is set to #{resource}/index
|
15
|
+
# viewPath - by default this is set to resource
|
16
|
+
#
|
17
|
+
class window.Neat.CollectionEditor extends Backbone.View
|
18
|
+
sortedBy: 'name'
|
19
|
+
sortOrder: 'asc'
|
20
|
+
sortAliases: {}
|
21
|
+
templateOptions: {}
|
22
|
+
pageSize: 30
|
23
|
+
|
24
|
+
keyDownHandlers:
|
25
|
+
ESC: -> @viewInEdit?.cancelEdit()
|
26
|
+
RETURN: -> @viewInEdit?.save()
|
27
|
+
UP: -> @edit @prevView()
|
28
|
+
DOWN: -> @edit @nextView()
|
29
|
+
|
30
|
+
initialize: ->
|
31
|
+
@viewPath = @viewPath ? @resource
|
32
|
+
@singular = inflect.singularize(@resource) # e.g. "calendar"
|
33
|
+
@modelView = @modelView ? (()=>
|
34
|
+
viewName = inflect.camelize(@singular) + "View" # e.g. "CalendarView"
|
35
|
+
@debug "expects viewName to be #{viewName}"
|
36
|
+
window[viewName])() # e.g. window["CalendarView"]
|
37
|
+
|
38
|
+
@debug "looking for template at #{"#{@viewPath}/index"}"
|
39
|
+
@template = @template ? Neat.template["#{@viewPath}/index"]
|
40
|
+
|
41
|
+
@paginator = new window.Lail.PaginatedList [],
|
42
|
+
page_size: if window.Neat.forPrint then Infinity else @pageSize
|
43
|
+
always_show: false
|
44
|
+
@paginator.onPageChange _.bind(@renderPage, @)
|
45
|
+
|
46
|
+
@collection.bind 'reset', @render, @
|
47
|
+
@collection.bind 'add', @rerenderPage, @
|
48
|
+
@collection.bind 'remove', @rerenderPage, @
|
49
|
+
|
50
|
+
# We need to rerender the page if a model's attributes
|
51
|
+
# have changed just in case this would affect how the
|
52
|
+
# models are sorted.
|
53
|
+
#
|
54
|
+
# !todo: perhaps we can be smarter here and only listen
|
55
|
+
# for changes to the attribute the view is sorted on.
|
56
|
+
#
|
57
|
+
# We don't want to redraw the page every time a model
|
58
|
+
# has changed. If an old transaction is deleted, a very
|
59
|
+
# large number of more recent transactions' running
|
60
|
+
# balances could be updated very rapidly. Redrawing the
|
61
|
+
# page will slow things down dramatically.
|
62
|
+
#
|
63
|
+
# Instead, redraw the page 500ms after a model has changed.
|
64
|
+
#
|
65
|
+
# This allows us to wait for activity to die down
|
66
|
+
# and to redraw the page when it's more likely the system
|
67
|
+
# has settled into new state.
|
68
|
+
#
|
69
|
+
@delayedRerender = new Lail.DelayedAction(_.bind(@rerenderPage, @), delay: 500)
|
70
|
+
@collection.bind 'change', @delayedRerender.trigger
|
71
|
+
|
72
|
+
# If the view's headers are 'a' tags, this view will try
|
73
|
+
# to sort the collection using the header tags.
|
74
|
+
$(@el).delegate '.header a', 'click', _.bind(@sort, @)
|
75
|
+
$(@el).delegate '.editor', 'keydown', _.bind(@onKeyDown, @)
|
76
|
+
|
77
|
+
@views = []
|
78
|
+
@viewInEdit = null
|
79
|
+
@templateOptions = {}
|
80
|
+
|
81
|
+
repaginate: ->
|
82
|
+
@rerenderPage(1)
|
83
|
+
|
84
|
+
rerenderPage: (page)->
|
85
|
+
page = @paginator.getCurrentPage() unless _.isNumber(page)
|
86
|
+
sortField = @sortField(@sortedBy)
|
87
|
+
items = @collection.sortBy (model)->
|
88
|
+
val = model.get(sortField) || ''
|
89
|
+
if _.isString(val) then val.toLowerCase() else val
|
90
|
+
items.reverse() if @sortOrder == 'desc'
|
91
|
+
@paginator.init items, page
|
92
|
+
|
93
|
+
|
94
|
+
render: ->
|
95
|
+
$el = $(@el)
|
96
|
+
$el.html @template(collection: @collection)
|
97
|
+
$el.cssHover '.row.interactive'
|
98
|
+
|
99
|
+
@afterRender()
|
100
|
+
|
101
|
+
@updateSortStyle() if @sortedBy
|
102
|
+
|
103
|
+
@paginator.renderPaginationIn($el.find('.pagination'))
|
104
|
+
@repaginate()
|
105
|
+
@
|
106
|
+
|
107
|
+
afterRender: ->
|
108
|
+
@
|
109
|
+
|
110
|
+
renderPage: ->
|
111
|
+
alt = false
|
112
|
+
$ul = $(@el).find("##{@resource}").empty() # e.g. $('#calendars')
|
113
|
+
@views = []
|
114
|
+
self = @
|
115
|
+
|
116
|
+
$(@el).find('.extended-pagination').html(@paginator.renderExtendedPagination())
|
117
|
+
|
118
|
+
for model in @paginator.getCurrentSet()
|
119
|
+
view = new @modelView # e.g. window.CalendarView
|
120
|
+
resource: @resource
|
121
|
+
viewPath: @viewPath
|
122
|
+
model: model
|
123
|
+
templateOptions: @templateOptions
|
124
|
+
view.bind 'edit:begin', -> self.beforeEdit.call(self, @)
|
125
|
+
view.bind 'edit:end', -> self.afterEdit.call(self, @)
|
126
|
+
|
127
|
+
@views.push(view)
|
128
|
+
|
129
|
+
$el = $(view.render().el)
|
130
|
+
$el.toggleClass 'alt', !(alt = !alt)
|
131
|
+
$ul.append $el
|
132
|
+
@
|
133
|
+
|
134
|
+
|
135
|
+
|
136
|
+
beforeEdit: (view)->
|
137
|
+
if @viewInEdit
|
138
|
+
@debug "cancelling edit for ##{$(@viewInEdit.el).attr('id')} (#{@indexOfViewInEdit()})"
|
139
|
+
@viewInEdit.cancelEdit()
|
140
|
+
@viewInEdit = view
|
141
|
+
@debug "beginning edit for ##{$(@viewInEdit.el).attr('id')} (#{@indexOfViewInEdit()})"
|
142
|
+
|
143
|
+
afterEdit: (view)->
|
144
|
+
@viewInEdit = null if @viewInEdit == view
|
145
|
+
|
146
|
+
|
147
|
+
|
148
|
+
sort: (e)->
|
149
|
+
e.preventDefault()
|
150
|
+
e.stopImmediatePropagation()
|
151
|
+
sortBy = $(e.target).closest('a').attr('class').substring(@singular.length + 1)
|
152
|
+
@log "sort by #{sortBy} [#{@sortField(sortBy)}]"
|
153
|
+
if @sortedBy == sortBy
|
154
|
+
@sortOrder = if @sortOrder == 'asc' then 'desc' else 'asc'
|
155
|
+
else
|
156
|
+
@removeSortStyle @sortedBy
|
157
|
+
@sortedBy = sortBy
|
158
|
+
@repaginate()
|
159
|
+
@updateSortStyle()
|
160
|
+
false
|
161
|
+
|
162
|
+
removeSortStyle: (field)->
|
163
|
+
|
164
|
+
updateSortStyle: ()->
|
165
|
+
@removeSortStyle @sortedBy
|
166
|
+
|
167
|
+
getHeader: (field)->
|
168
|
+
$(@el).find(".header > .#{@singular}-#{field}")
|
169
|
+
|
170
|
+
sortField: (field)->
|
171
|
+
@sortAliases[field] ? field
|
172
|
+
|
173
|
+
|
174
|
+
|
175
|
+
onKeyDown: (e)->
|
176
|
+
keyName = @identifyKey(e.keyCode)
|
177
|
+
handler = @keyDownHandlers[keyName]
|
178
|
+
if handler && !@ignoreKeyEventsForTarget(e.target)
|
179
|
+
e.preventDefault()
|
180
|
+
handler.apply(@)
|
181
|
+
|
182
|
+
identifyKey: (code)->
|
183
|
+
KEYS[code]
|
184
|
+
|
185
|
+
ignoreKeyEventsForTarget: (target)->
|
186
|
+
# i.e. return true if target is in a dropdown control like Chosen
|
187
|
+
false
|
188
|
+
|
189
|
+
|
190
|
+
|
191
|
+
nextView: ->
|
192
|
+
@views[@indexOfViewInEdit() + 1]
|
193
|
+
|
194
|
+
prevView: ->
|
195
|
+
@views[@indexOfViewInEdit() - 1]
|
196
|
+
|
197
|
+
indexOfViewInEdit: ->
|
198
|
+
_.indexOf @views, @viewInEdit
|
199
|
+
|
200
|
+
edit: (view)->
|
201
|
+
if view
|
202
|
+
@viewInEdit?.save()
|
203
|
+
view.edit()
|
204
|
+
@viewInEdit = view
|
205
|
+
|
206
|
+
cancelEdit: ->
|
207
|
+
@viewInEdit?.cancelEdit()
|
208
|
+
|
209
|
+
|
210
|
+
|
211
|
+
debug: (o...)->
|
212
|
+
@log(o...) if Neat.debug
|
213
|
+
|
214
|
+
log: (o...)->
|
215
|
+
Neat.logger.log "[#{@viewPath}] ", o...
|
@@ -0,0 +1,49 @@
|
|
1
|
+
var Lail; if(!Lail) Lail={};
|
2
|
+
Lail.DelayedAction = function(callback, options) {
|
3
|
+
// !todo: paste John Resig's code
|
4
|
+
|
5
|
+
options = options || {};
|
6
|
+
var self = this,
|
7
|
+
delay = options.delay || 1000,
|
8
|
+
steps = options.steps || 10,
|
9
|
+
intervalPeriod = delay / steps,
|
10
|
+
counter = 0,
|
11
|
+
_params,
|
12
|
+
_interval;
|
13
|
+
|
14
|
+
this.trigger = function(params) {
|
15
|
+
_params = params;
|
16
|
+
restartCountdown();
|
17
|
+
}
|
18
|
+
|
19
|
+
function restartCountdown() {
|
20
|
+
counter = steps;
|
21
|
+
if(!_interval) {
|
22
|
+
_interval = setInterval(countdown, intervalPeriod);
|
23
|
+
}
|
24
|
+
}
|
25
|
+
|
26
|
+
function countdown() {
|
27
|
+
if(counter > 0) {
|
28
|
+
counter = counter - 1;
|
29
|
+
} else {
|
30
|
+
stopCountdown()
|
31
|
+
fireCallback();
|
32
|
+
}
|
33
|
+
}
|
34
|
+
|
35
|
+
function stopCountdown() {
|
36
|
+
if(_interval) {
|
37
|
+
clearInterval(_interval);
|
38
|
+
_interval = null;
|
39
|
+
}
|
40
|
+
}
|
41
|
+
|
42
|
+
function fireCallback() {
|
43
|
+
try {
|
44
|
+
callback(_params);
|
45
|
+
} catch(e) {
|
46
|
+
App.debug(e);
|
47
|
+
}
|
48
|
+
}
|
49
|
+
}
|