cartilage 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.markdown +57 -0
- data/Rakefile +84 -0
- data/app/assets/images/cartilage/patterns/background.dark.png +0 -0
- data/app/assets/images/cartilage/patterns/background.dark.psd +0 -0
- data/app/assets/images/cartilage/patterns/background.light.png +0 -0
- data/app/assets/images/cartilage/patterns/background.light.psd +0 -0
- data/app/assets/images/cartilage/patterns/background.medium.png +0 -0
- data/app/assets/images/cartilage/patterns/background.medium.psd +0 -0
- data/app/assets/images/cartilage/patterns/background.png +0 -0
- data/app/assets/images/cartilage/patterns/background.psd +0 -0
- data/app/assets/javascripts/cartilage/application.js.coffee +45 -0
- data/app/assets/javascripts/cartilage/collections/segments.js.coffee +4 -0
- data/app/assets/javascripts/cartilage/core.js.coffee +8 -0
- data/app/assets/javascripts/cartilage/models/model.js.coffee +2 -0
- data/app/assets/javascripts/cartilage/models/segment.js.coffee +2 -0
- data/app/assets/javascripts/cartilage/views/bar_segment_view.js.coffee +58 -0
- data/app/assets/javascripts/cartilage/views/bar_view.js.coffee +85 -0
- data/app/assets/javascripts/cartilage/views/content_view.js.coffee +16 -0
- data/app/assets/javascripts/cartilage/views/image_view.js.coffee +76 -0
- data/app/assets/javascripts/cartilage/views/list_view.js.coffee +515 -0
- data/app/assets/javascripts/cartilage/views/list_view_item.js.coffee +85 -0
- data/app/assets/javascripts/cartilage/views/loading_indicator_view.js.coffee +147 -0
- data/app/assets/javascripts/cartilage/views/matrix_view.js.coffee +253 -0
- data/app/assets/javascripts/cartilage/views/matrix_view_item.js.coffee +5 -0
- data/app/assets/javascripts/cartilage/views/modal_view.js.coffee +26 -0
- data/app/assets/javascripts/cartilage/views/popover_view.js.coffee +212 -0
- data/app/assets/javascripts/cartilage/views/source_list_view.js.coffee +69 -0
- data/app/assets/javascripts/cartilage/views/source_list_view_item.js.coffee +5 -0
- data/app/assets/javascripts/cartilage/views/split_view.js.coffee +175 -0
- data/app/assets/javascripts/cartilage/views/usage_bar_view.js.coffee +5 -0
- data/app/assets/javascripts/cartilage/views/view.js.coffee +232 -0
- data/app/assets/javascripts/cartilage.js.coffee +18 -0
- data/app/assets/javascripts/extensions/console.js.coffee +9 -0
- data/app/assets/javascripts/extensions/constructor_name.js.coffee +10 -0
- data/app/assets/javascripts/extensions/properties.js.coffee +45 -0
- data/app/assets/javascripts/extensions/underscore.js.coffee +71 -0
- data/app/assets/stylesheets/cartilage/core.css.scss +432 -0
- data/app/assets/stylesheets/cartilage/mixins.css.scss +19 -0
- data/app/assets/stylesheets/cartilage/responsive.css.scss +192 -0
- data/app/assets/stylesheets/cartilage/variables.css.scss +113 -0
- data/app/assets/stylesheets/cartilage/views/list_view.css.scss +41 -0
- data/app/assets/stylesheets/cartilage/views/list_view_item.css.scss +47 -0
- data/app/assets/stylesheets/cartilage/views/loading_indicator_view.css.scss +50 -0
- data/app/assets/stylesheets/cartilage/views/matrix_view.css.scss +87 -0
- data/app/assets/stylesheets/cartilage/views/popover_view.css.scss +124 -0
- data/app/assets/stylesheets/cartilage/views/segmented_view.css.scss +98 -0
- data/app/assets/stylesheets/cartilage/views/source_list_view.css.scss +65 -0
- data/app/assets/stylesheets/cartilage/views/split_view.css.scss +80 -0
- data/app/assets/stylesheets/cartilage/views/usage_bar_view.css.scss +101 -0
- data/app/assets/stylesheets/cartilage/views/view.css.scss +13 -0
- data/app/assets/stylesheets/cartilage.css.scss +5 -0
- data/lib/cartilage/engine.rb +14 -0
- data/lib/cartilage/version.rb +3 -0
- data/lib/cartilage.rb +5 -0
- data/lib/tasks/cartilage_tasks.rake +4 -0
- data/test/cartilage_test.rb +7 -0
- data/test/dummy/Rakefile +7 -0
- data/test/dummy/app/assets/javascripts/application.js +9 -0
- data/test/dummy/app/assets/stylesheets/application.css +7 -0
- data/test/dummy/app/controllers/application_controller.rb +3 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/config/application.rb +45 -0
- data/test/dummy/config/boot.rb +10 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +30 -0
- data/test/dummy/config/environments/production.rb +60 -0
- data/test/dummy/config/environments/test.rb +42 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/inflections.rb +10 -0
- data/test/dummy/config/initializers/mime_types.rb +5 -0
- data/test/dummy/config/initializers/secret_token.rb +7 -0
- data/test/dummy/config/initializers/session_store.rb +8 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +5 -0
- data/test/dummy/config/routes.rb +58 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/db/readme +6 -0
- data/test/dummy/public/404.html +26 -0
- data/test/dummy/public/422.html +26 -0
- data/test/dummy/public/500.html +26 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/script/rails +6 -0
- data/test/framework/cartilage/application_test.js.coffee +14 -0
- data/test/framework/cartilage/views/bar_segment_view_test.js.coffee +5 -0
- data/test/framework/cartilage/views/bar_view_test.js.coffee +30 -0
- data/test/framework/cartilage/views/image_view_test.js.coffee +27 -0
- data/test/framework/cartilage/views/list_view_item_test.js.coffee +17 -0
- data/test/framework/cartilage/views/list_view_test.js.coffee +190 -0
- data/test/framework/cartilage/views/loading_indicator_view_test.js.coffee +5 -0
- data/test/framework/cartilage/views/matrix_view_item_test.js.coffee +5 -0
- data/test/framework/cartilage/views/matrix_view_test.js.coffee +5 -0
- data/test/framework/cartilage/views/popover_view_test.js.coffee +5 -0
- data/test/framework/cartilage/views/source_list_view_item_test.js.coffee +5 -0
- data/test/framework/cartilage/views/source_list_view_test.js.coffee +5 -0
- data/test/framework/cartilage/views/split_view_test.js.coffee +5 -0
- data/test/framework/cartilage/views/usage_bar_view_test.js.coffee +5 -0
- data/test/framework/cartilage/views/view_test.js.coffee +85 -0
- data/test/framework/extensions/properties_test.js.coffee +82 -0
- data/test/framework/extensions/underscore_test.js.coffee +73 -0
- data/test/framework/index.html +72 -0
- data/test/framework/vendor/backbone.js +1431 -0
- data/test/framework/vendor/coffee-script.js +8 -0
- data/test/framework/vendor/jquery.js +9440 -0
- data/test/framework/vendor/phantomjs-runner.js +196 -0
- data/test/framework/vendor/qunit.css +235 -0
- data/test/framework/vendor/qunit.js +1977 -0
- data/test/framework/vendor/run-qunit.js +81 -0
- data/test/framework/vendor/sinon-1.5.0.js +4142 -0
- data/test/framework/vendor/sinon-qunit-1.0.0.js +62 -0
- data/test/framework/vendor/underscore.js +1059 -0
- data/test/test_helper.rb +10 -0
- metadata +373 -0
@@ -0,0 +1,515 @@
|
|
1
|
+
#
|
2
|
+
# List View
|
3
|
+
#
|
4
|
+
# Manages a list of items as an unordered list (<ul/>), providing rich
|
5
|
+
# interaction support (selection, keyboard navigation, removal, reordering,
|
6
|
+
# etc).
|
7
|
+
#
|
8
|
+
|
9
|
+
#= require cartilage/views/list_view_item
|
10
|
+
|
11
|
+
class window.Cartilage.Views.ListView extends Cartilage.View
|
12
|
+
|
13
|
+
# Properties ---------------------------------------------------------------
|
14
|
+
|
15
|
+
#
|
16
|
+
# Whether or not items may be removed from the list view through keyboard
|
17
|
+
# actions (i.e. pressing delete while the item is selected).
|
18
|
+
#
|
19
|
+
@property "allowsRemove", default: no
|
20
|
+
|
21
|
+
#
|
22
|
+
# Whether or not selections of list view items can be made in the list
|
23
|
+
# view.
|
24
|
+
#
|
25
|
+
@property "allowsSelection", default: yes
|
26
|
+
|
27
|
+
#
|
28
|
+
# Whether or not the list view may clear its selection through user
|
29
|
+
# actions, such as clicking on the empty areas of a list view.
|
30
|
+
#
|
31
|
+
@property "allowsDeselection", default: yes
|
32
|
+
|
33
|
+
#
|
34
|
+
# Whether or not multiple list view items may be selected at once.
|
35
|
+
#
|
36
|
+
@property "allowsMultipleSelection", default: no
|
37
|
+
|
38
|
+
#
|
39
|
+
# Whether or not the user can drag list view items around to reorder them.
|
40
|
+
#
|
41
|
+
@property "allowsDragToReorder", default: no
|
42
|
+
|
43
|
+
#
|
44
|
+
# The default class for all list view items that are not explicitly
|
45
|
+
# initialized.
|
46
|
+
#
|
47
|
+
@property "itemView", default: Cartilage.Views.ListViewItem
|
48
|
+
|
49
|
+
#
|
50
|
+
# An array containing the view of each item rendered as a part of the list
|
51
|
+
#
|
52
|
+
@property "listItemViews", access: READONLY, default: []
|
53
|
+
|
54
|
+
#
|
55
|
+
# A collection containing the models of the currently selected items.
|
56
|
+
#
|
57
|
+
# TODO Rename to selectedModels
|
58
|
+
# TODO Add selectedItems
|
59
|
+
@property "selected", access: READONLY
|
60
|
+
|
61
|
+
#
|
62
|
+
# A collection containing all of the list view items managed by the list
|
63
|
+
# view.
|
64
|
+
#
|
65
|
+
@property "draggedItems"
|
66
|
+
|
67
|
+
# --------------------------------------------------------------------------
|
68
|
+
|
69
|
+
events:
|
70
|
+
"dblclick ul.list-view-items-container > li": "open"
|
71
|
+
"click ul.list-view-items-container > li": "onFocus"
|
72
|
+
"keydown ul.list-view-items-container": "onKeyDown"
|
73
|
+
"mousedown ul.list-view-items-container": "onMouseDown"
|
74
|
+
"dragover ul.list-view-items-container": "onDragOver"
|
75
|
+
"drop ul.list-view-items-container": "onDrop"
|
76
|
+
|
77
|
+
initialize: (options = {}) ->
|
78
|
+
# Initialize the View
|
79
|
+
super(options)
|
80
|
+
|
81
|
+
# Initialize List View Items Container
|
82
|
+
($ @el).append @_listViewItemsContainer = @make "ul",
|
83
|
+
class: "list-view-items-container"
|
84
|
+
tabindex: 0
|
85
|
+
|
86
|
+
# Keep the selected collection in the same order as the view collection
|
87
|
+
@_selected = new Backbone.Collection(
|
88
|
+
null,
|
89
|
+
comparator: (item) =>
|
90
|
+
@collection.indexOf(item)
|
91
|
+
)
|
92
|
+
|
93
|
+
# _.clone of properties is shallow only so we won't get a clean copy if we use the
|
94
|
+
# default method in property - instead, we'll maintain explicit creation here.
|
95
|
+
@_draggedItems = new Backbone.Collection
|
96
|
+
|
97
|
+
@observe @collection, "add", @addModel
|
98
|
+
@observe @collection, "reset", @update # TODO Don't re-render the entire view for removals
|
99
|
+
@observe @collection, "remove", @prepare # TODO Don't re-render the entire view for removals
|
100
|
+
|
101
|
+
prepare: ->
|
102
|
+
|
103
|
+
# Prepare the View
|
104
|
+
super()
|
105
|
+
|
106
|
+
# Update the View
|
107
|
+
@update()
|
108
|
+
|
109
|
+
update: ->
|
110
|
+
# Clean up all existing item views and their container elements.
|
111
|
+
(@$ "ul.list-view-items-container > li.list-view-item").each (idx, element) =>
|
112
|
+
@removeItem(element)
|
113
|
+
|
114
|
+
_.each @renderModels(), (view) => @addItem(view)
|
115
|
+
@restoreSelection() if @_selected.models.length > 0
|
116
|
+
|
117
|
+
renderModels: =>
|
118
|
+
items = _.map @collection.models, (model) =>
|
119
|
+
@renderModel(model)
|
120
|
+
|
121
|
+
renderModel: (model) =>
|
122
|
+
if @itemView? then new @itemView { model: model, listView: @ } else console.warn("Could not find corresponding itemView for #{@constructor.name}")
|
123
|
+
|
124
|
+
addModel: (model) =>
|
125
|
+
@addItem @renderModel(model)
|
126
|
+
|
127
|
+
#
|
128
|
+
# Adds an already instantiated list view item (must derive from
|
129
|
+
# Cartilage.Views.ListViewItem) to the list view. The list view item's
|
130
|
+
# model will automatically be added to the list view's collection, but
|
131
|
+
# will not trigger any notifications that it was added.
|
132
|
+
#
|
133
|
+
addItem: (itemView, container = @_listViewItemsContainer) =>
|
134
|
+
|
135
|
+
# Store the view in order in the _listItemView array so we can
|
136
|
+
# insert views in the correct order.
|
137
|
+
index = @collection.indexOf(itemView.model)
|
138
|
+
if @_listItemViews[index]?
|
139
|
+
@_listItemViews.splice(@collection.indexOf(itemView.model), 0, itemView)
|
140
|
+
else
|
141
|
+
@_listItemViews[index] = itemView
|
142
|
+
|
143
|
+
# TODO Ensure that the item derives from ListViewItem
|
144
|
+
@addSubview itemView, container
|
145
|
+
|
146
|
+
#
|
147
|
+
# Removes a passed element from the list view's superview as well
|
148
|
+
# as from the @_listViewItems array.
|
149
|
+
#
|
150
|
+
removeItem: (element) =>
|
151
|
+
if view = ($ element).data("view")
|
152
|
+
view.removeFromSuperview()
|
153
|
+
_.remove(@_listViewItems, view) if @_listViewItems?
|
154
|
+
else
|
155
|
+
($ element).remove()
|
156
|
+
|
157
|
+
|
158
|
+
#
|
159
|
+
# Override View's insertSubviewItem method so a list view item
|
160
|
+
# can be added at the correct position in the list
|
161
|
+
#
|
162
|
+
insertSubviewElement: (view, container) ->
|
163
|
+
if @collection.length == 1
|
164
|
+
($ container ).append(view.el)
|
165
|
+
else
|
166
|
+
flattenedListItemViews = _.flatten(@_listItemViews)
|
167
|
+
index = _.indexOf(flattenedListItemViews, view)
|
168
|
+
if index == -1
|
169
|
+
($ container ).append(view.el)
|
170
|
+
else if index == 0
|
171
|
+
($ container ).prepend(view.el)
|
172
|
+
else
|
173
|
+
($ flattenedListItemViews[index - 1].el ).after(view.el)
|
174
|
+
|
175
|
+
#
|
176
|
+
# Selects the specified list item. Returns true if the item was selected or
|
177
|
+
# false if no action was performed.
|
178
|
+
#
|
179
|
+
select: (e) =>
|
180
|
+
return unless @allowsSelection
|
181
|
+
|
182
|
+
element = unless _.isUndefined(e.target) then e.target else e
|
183
|
+
model = ($ element).data("model")
|
184
|
+
|
185
|
+
# Do not attempt to select the element if it is already selected.
|
186
|
+
return if @_selected.contains(model)
|
187
|
+
|
188
|
+
# If an element is already selected, deselect it before continuing.
|
189
|
+
if @allowsMultipleSelection
|
190
|
+
# In multi-select, do a full reset so observing "reset" on @_selected works as expected
|
191
|
+
@clearSelection() if @_selected.length > 0
|
192
|
+
else
|
193
|
+
# In single select, call deselect on each selected element so observing "deselect" works as expected
|
194
|
+
_.each(($ @el).find("li.list-view-item.selected"), (element) => @deselect(element))
|
195
|
+
|
196
|
+
@_selected.add model
|
197
|
+
($ element).addClass "selected"
|
198
|
+
# ($ element).focus() unless ($ element).is(":focus")
|
199
|
+
@focusedElement = ($ element)
|
200
|
+
@trigger "select", @_selected
|
201
|
+
|
202
|
+
selectAnother: (e) =>
|
203
|
+
# If multiple selection is not allowed, simply select the requested item
|
204
|
+
# and return instead of adding it to the selection.
|
205
|
+
unless @allowsSelection and @allowsMultipleSelection
|
206
|
+
@select(e)
|
207
|
+
return
|
208
|
+
|
209
|
+
element = e.target || e
|
210
|
+
model = ($ element).data("model")
|
211
|
+
|
212
|
+
# Do not attempt to select the element if it is already selected.
|
213
|
+
return if @_selected.contains(model)
|
214
|
+
|
215
|
+
@_selected.add model
|
216
|
+
($ element).addClass "selected"
|
217
|
+
@trigger "select", @_selected
|
218
|
+
|
219
|
+
#
|
220
|
+
# Selects the first element in the list.
|
221
|
+
#
|
222
|
+
selectFirst: ->
|
223
|
+
element = ($ @el).find("ul.list-view-items-container > li.list-view-item").first()
|
224
|
+
@select element
|
225
|
+
|
226
|
+
#
|
227
|
+
# Selects the last element in the list.
|
228
|
+
#
|
229
|
+
selectLast: ->
|
230
|
+
element = ($ @el).find "ul.list-view-items-container > li.list-view-item:last-of-type"
|
231
|
+
@select element
|
232
|
+
|
233
|
+
#
|
234
|
+
# Selects all elements in the list.
|
235
|
+
#
|
236
|
+
selectAll: ->
|
237
|
+
return unless @allowsSelection and @allowsMultipleSelection
|
238
|
+
elements = ($ @el).find "ul.list-view-items-container > li.list-view-item:not(.selected)"
|
239
|
+
_.each elements, @selectAnother
|
240
|
+
|
241
|
+
#
|
242
|
+
# Restores the selection after a re-rendering.
|
243
|
+
#
|
244
|
+
restoreSelection: =>
|
245
|
+
elements = ($ @el).find "ul.list-view-items-container > li.list-view-item"
|
246
|
+
_.each elements, (element) =>
|
247
|
+
model = ($ element).data("model")
|
248
|
+
if @_selected.contains(model)
|
249
|
+
($ element).addClass("selected")
|
250
|
+
else
|
251
|
+
@_selected.remove model
|
252
|
+
|
253
|
+
#
|
254
|
+
# Deselects the specified list view item, if it is currently selected.
|
255
|
+
# Returns true if the item was deselected or false if no action was
|
256
|
+
# performed.
|
257
|
+
#
|
258
|
+
deselect: (element) ->
|
259
|
+
model = ($ element).data("model")
|
260
|
+
($ element).removeClass "selected"
|
261
|
+
@_selected.remove model
|
262
|
+
@trigger "deselect", element
|
263
|
+
|
264
|
+
#
|
265
|
+
# Deselects the specified list view item, if it is currently selected.
|
266
|
+
# Returns true if the item was deselected or false if no action was
|
267
|
+
# performed.
|
268
|
+
#
|
269
|
+
clearSelection: (options = {}) ->
|
270
|
+
($ @el).find("ul.list-view-items-container > li.list-view-item.selected").removeClass "selected"
|
271
|
+
@_selected.reset(null, { silent: options["silent"] })
|
272
|
+
@trigger("clear", @_selected) unless options["silent"]
|
273
|
+
|
274
|
+
#
|
275
|
+
# Opens the selected item(s).
|
276
|
+
#
|
277
|
+
open: (e) ->
|
278
|
+
@trigger "open", @_selected
|
279
|
+
|
280
|
+
#
|
281
|
+
# Removes the selected item(s).
|
282
|
+
#
|
283
|
+
remove: (e) =>
|
284
|
+
return unless @allowsRemove
|
285
|
+
@collection.remove @_selected.models
|
286
|
+
@_selected.reset()
|
287
|
+
@trigger "remove", @_selected
|
288
|
+
|
289
|
+
#
|
290
|
+
# Handles click events for the entire list elements, including the list
|
291
|
+
# container.
|
292
|
+
#
|
293
|
+
onMouseDown: (event) =>
|
294
|
+
return unless @allowsSelection
|
295
|
+
# Get the list item element
|
296
|
+
element = if ($ event.target).is("li.list-view-item") then event.target else ($ event.target).parents("li.list-view-item")
|
297
|
+
|
298
|
+
# Clear the selection if the user clicks in the list container.
|
299
|
+
@clearSelection() if @allowsDeselection and event.target.tagName == "UL"
|
300
|
+
|
301
|
+
if event.metaKey
|
302
|
+
model = ($ element).data("model")
|
303
|
+
if model?
|
304
|
+
if @_selected.contains(model)
|
305
|
+
@deselect element
|
306
|
+
else
|
307
|
+
@selectAnother element
|
308
|
+
event.preventDefault()
|
309
|
+
|
310
|
+
else if event.shiftKey
|
311
|
+
element = event.target
|
312
|
+
@expandSelectionToElement ($ element).parents("li.list-view-item") || element
|
313
|
+
event.preventDefault()
|
314
|
+
|
315
|
+
#
|
316
|
+
# Handles focus events.
|
317
|
+
#
|
318
|
+
onFocus: (event) =>
|
319
|
+
return unless @allowsSelection
|
320
|
+
@focusedElement = $(event.target).closest("li.list-view-item")
|
321
|
+
@select @focusedElement unless event.metaKey
|
322
|
+
event.preventDefault()
|
323
|
+
|
324
|
+
#
|
325
|
+
# Handles key down events while the view is focused.
|
326
|
+
#
|
327
|
+
onKeyDown: (event) =>
|
328
|
+
keyCode = event.keyCode
|
329
|
+
|
330
|
+
# Arrow Keys
|
331
|
+
if keyCode in [ 38, 40, 63232, 63233 ]
|
332
|
+
|
333
|
+
event.preventDefault()
|
334
|
+
|
335
|
+
# Select the first item if there is no selection when the first key press
|
336
|
+
# occurs.
|
337
|
+
return @selectFirst() unless @_selected.length > 0
|
338
|
+
|
339
|
+
# Route the key event to the proper handler based on the combination of
|
340
|
+
# keys that were pressed.
|
341
|
+
if event.shiftKey or event.metaKey
|
342
|
+
return @expandSelectionUp event if keyCode in [ 38, 63232 ]
|
343
|
+
return @expandSelectionDown event if keyCode in [ 40, 63233 ]
|
344
|
+
else
|
345
|
+
return @moveSelectionUp event if keyCode in [ 38, 63232 ]
|
346
|
+
return @moveSelectionDown event if keyCode in [ 40, 63233 ]
|
347
|
+
|
348
|
+
# Command-A
|
349
|
+
if keyCode == 65 and event.metaKey
|
350
|
+
event.preventDefault()
|
351
|
+
return @selectAll()
|
352
|
+
|
353
|
+
# Enter Key
|
354
|
+
if keyCode == 13
|
355
|
+
event.preventDefault()
|
356
|
+
return @open event.target
|
357
|
+
|
358
|
+
# Delete Key
|
359
|
+
if keyCode in [ 8, 46 ]
|
360
|
+
event.preventDefault()
|
361
|
+
return @remove event.target
|
362
|
+
|
363
|
+
# Space Key
|
364
|
+
return event.preventDefault() if keyCode == 32
|
365
|
+
|
366
|
+
onDragOver: (event) =>
|
367
|
+
return unless @allowsDragToReorder
|
368
|
+
|
369
|
+
allowed = true
|
370
|
+
|
371
|
+
# Ensure that the dragged item is a list view item...
|
372
|
+
unless "application/x-list-view-item-id" in event.originalEvent.dataTransfer.types
|
373
|
+
allowed = false
|
374
|
+
|
375
|
+
# Ensure that the dragged item belongs to us...
|
376
|
+
unless @collection.contains(@draggedItems.models[0])
|
377
|
+
allowed = false
|
378
|
+
|
379
|
+
event.preventDefault() if allowed
|
380
|
+
|
381
|
+
onDrop: (event) =>
|
382
|
+
return unless @allowsDragToReorder
|
383
|
+
|
384
|
+
draggedElementId = event.originalEvent.dataTransfer.getData("application/x-list-view-item-id")
|
385
|
+
draggedElement = ($ "##{draggedElementId}")
|
386
|
+
droppedElement = ($ event.target).parents("li.list-view-item")[0] || ($ event.target)[0]
|
387
|
+
|
388
|
+
# Ensure that the dragged element is not the same as the dropped element
|
389
|
+
unless $(draggedElement)[0] is $(droppedElement)[0]
|
390
|
+
if droppedElement and droppedElement.tagName is "LI"
|
391
|
+
if event.originalEvent.offsetY < (($ droppedElement).height() / 2)
|
392
|
+
($ draggedElement).detach().insertBefore(droppedElement)
|
393
|
+
else
|
394
|
+
($ draggedElement).detach().insertAfter(droppedElement)
|
395
|
+
|
396
|
+
($ droppedElement).removeClass("drop-before").removeClass("drop-after")
|
397
|
+
|
398
|
+
# Clear the global draggedItem reference
|
399
|
+
@trigger 'drop', @_draggedItems
|
400
|
+
@_draggedItems.reset()
|
401
|
+
|
402
|
+
#
|
403
|
+
# Moves the selection to the item visually above the selected item. If there
|
404
|
+
# is no selection the first item will be selected.
|
405
|
+
#
|
406
|
+
moveSelectionUp: (e) =>
|
407
|
+
element = ($ @focusedElement).prev "li.list-view-item"
|
408
|
+
@select element if element.length > 0
|
409
|
+
|
410
|
+
#
|
411
|
+
# Moves the selection to the item visually below the selected item. If there
|
412
|
+
# is no selection the first item will be selected.
|
413
|
+
#
|
414
|
+
moveSelectionDown: (e) =>
|
415
|
+
element = ($ @focusedElement).next "li.list-view-item"
|
416
|
+
@select element if element.length > 0
|
417
|
+
|
418
|
+
#
|
419
|
+
# Expands the selection downward from the currently selected element.
|
420
|
+
#
|
421
|
+
expandSelectionDown: (e) ->
|
422
|
+
element = ($ @focusedElement).next "li.list-view-item"
|
423
|
+
if element.length > 0
|
424
|
+
@selectAnother element
|
425
|
+
@focusedElement = ($ element).focus()
|
426
|
+
|
427
|
+
#
|
428
|
+
# Expands the selection upward from the currently selected element.
|
429
|
+
#
|
430
|
+
expandSelectionUp: (e) ->
|
431
|
+
element = ($ @focusedElement).prev "li.list-view-item"
|
432
|
+
if element.length > 0
|
433
|
+
@selectAnother element
|
434
|
+
@focusedElement = ($ element).focus()
|
435
|
+
|
436
|
+
#
|
437
|
+
# Expands the selection from the currently selected element to the passed
|
438
|
+
# element.
|
439
|
+
#
|
440
|
+
expandSelectionToElement: (element) ->
|
441
|
+
indexes = [
|
442
|
+
($ @focusedElement).index(),
|
443
|
+
($ element).index()
|
444
|
+
].sort (a, b) -> a - b
|
445
|
+
|
446
|
+
# Increment the end range since slice does not include it...
|
447
|
+
indexes[1] += 1
|
448
|
+
|
449
|
+
# Select the elements in the requested indexes.
|
450
|
+
elements = ($ @el).find("li.list-view-item").slice indexes...
|
451
|
+
_.each elements, @selectAnother
|
452
|
+
|
453
|
+
#
|
454
|
+
# Scrolls the list view to fully expose the selected item if it is not
|
455
|
+
# fully visible within the list view. Returns true or false depending on
|
456
|
+
# whether the scroll was performed.
|
457
|
+
#
|
458
|
+
# scrollToSelectedItem: =>
|
459
|
+
# console.log "scrollToSelectedItem"
|
460
|
+
#
|
461
|
+
# scrollElement = ($ @el).parent()[0]
|
462
|
+
# console.log scrollElement
|
463
|
+
#
|
464
|
+
# if ($ scrollElement).scrollHeight < ($ scrollElement).innerHeight() and ($ scrollElement).scrollWidth < ($ scrollElement).innerWidth()
|
465
|
+
# console.log "returning"
|
466
|
+
# return
|
467
|
+
#
|
468
|
+
# selectedItemTop = @_selectedElement.offset().top - ($ scrollElement).offset().top
|
469
|
+
# selectedItemBottom = selectedItemTop + @_selectedElement.innerHeight()
|
470
|
+
# selectedItemLeft = @_selectedElement.offset().left - ($ scrollElement).offset().left
|
471
|
+
# selectedItemRight = selectedItemLeft + @_selectedElement.innerWidth()
|
472
|
+
# currentScrollTop = ($ scrollElement).scrollTop()
|
473
|
+
# currentScrollBottom = ($ scrollElement).scrollTop() + ($ scrollElement).innerHeight()
|
474
|
+
# currentScrollLeft = ($ scrollElement).scrollLeft()
|
475
|
+
# currentScrollRight = ($ scrollElement).scrollLeft() + ($ scrollElement).innerWidth()
|
476
|
+
# itemTopMargin = parseInt(@_selectedElement.css("margin-top"), 10)
|
477
|
+
# itemBottomMargin = parseInt(@_selectedElement.css("margin-bottom"), 10)
|
478
|
+
# itemLeftMargin = parseInt(@_selectedElement.css("margin-left"), 10)
|
479
|
+
# itemRightMargin = parseInt(@_selectedElement.css("margin-right"), 10)
|
480
|
+
# scrollTopValue = selectedItemTop - itemTopMargin
|
481
|
+
# scrollLeftValue = selectedItemLeft - itemLeftMargin
|
482
|
+
# shouldScrollTop = false
|
483
|
+
# shouldScrollLeft = false
|
484
|
+
#
|
485
|
+
# # selectedItem is above current scroll position
|
486
|
+
# if selectedItemTop < currentScrollTop
|
487
|
+
# shouldScrollTop = true
|
488
|
+
#
|
489
|
+
# # selectedItem is below current, viewable scroll position
|
490
|
+
# if selectedItemTop >= currentScrollBottom
|
491
|
+
# shouldScrollTop = true
|
492
|
+
#
|
493
|
+
# # selectedItem is partially visible below the current, viewable scroll position
|
494
|
+
# else if selectedItemBottom > currentScrollBottom
|
495
|
+
# shouldScrollTop = true
|
496
|
+
# scrollTopValue = currentScrollTop + (selectedItemBottom - currentScrollBottom) + itemBottomMargin
|
497
|
+
#
|
498
|
+
# # selectedItem is left of current scroll position
|
499
|
+
# if selectedItemLeft < currentScrollLeft
|
500
|
+
# shouldScrollLeft = true
|
501
|
+
#
|
502
|
+
# # selectedItem is right of current, viewable scroll position
|
503
|
+
# if selectedItemLeft >= currentScrollRight
|
504
|
+
# shouldScrollLeft = true
|
505
|
+
#
|
506
|
+
# # selectedItem is partially visible below the current, viewable scroll position
|
507
|
+
# else if selectedItemRight > currentScrollRight
|
508
|
+
# shouldScrollLeft = true
|
509
|
+
# scrollLeftValue = currentScrollLeft + (selectedItemRight - currentScrollRight) + itemRightMargin
|
510
|
+
#
|
511
|
+
# if shouldScrollTop or shouldScrollLeft
|
512
|
+
# ($ @el).scrollTop(scrollTopValue) if shouldScrollTop
|
513
|
+
# ($ @el).scrollLeft(scrollLeftValue) if shouldScrollLeft
|
514
|
+
#
|
515
|
+
# (shouldScrollTop or shouldScrollLeft)
|
@@ -0,0 +1,85 @@
|
|
1
|
+
#
|
2
|
+
# List View Item
|
3
|
+
#
|
4
|
+
|
5
|
+
class window.Cartilage.Views.ListViewItem extends Cartilage.View
|
6
|
+
|
7
|
+
# View Configuration -------------------------------------------------------
|
8
|
+
|
9
|
+
id: -> # TODO Hacky
|
10
|
+
"list-view-item-#{Math.floor(Math.random() * 1000000000)}"
|
11
|
+
|
12
|
+
tagName: "li"
|
13
|
+
|
14
|
+
events:
|
15
|
+
"dragover": "onDragOver"
|
16
|
+
"dragleave": "onDragLeave"
|
17
|
+
"dragstart": "onDragStart"
|
18
|
+
|
19
|
+
# Properties ---------------------------------------------------------------
|
20
|
+
|
21
|
+
#
|
22
|
+
# The list view instance that the list view item currently belongs to.
|
23
|
+
#
|
24
|
+
@property "listView"
|
25
|
+
|
26
|
+
#
|
27
|
+
# Whether or not the list view item is currently selected.
|
28
|
+
#
|
29
|
+
@property "isSelected", access: READONLY, default: no
|
30
|
+
|
31
|
+
# Internal Properties ------------------------------------------------------
|
32
|
+
|
33
|
+
# --------------------------------------------------------------------------
|
34
|
+
|
35
|
+
initialize: (options = {}) ->
|
36
|
+
|
37
|
+
# Initialize the View
|
38
|
+
super(options)
|
39
|
+
|
40
|
+
# Give the Element a tab index so that it can be focused
|
41
|
+
($ @el).attr "tabindex", 0
|
42
|
+
|
43
|
+
# Assign the Model to the Element
|
44
|
+
($ @el).data "model", @model
|
45
|
+
|
46
|
+
# Assign the List View Item to the Element
|
47
|
+
($ @el).data "view", @
|
48
|
+
|
49
|
+
prepare: ->
|
50
|
+
|
51
|
+
# Prepare the View
|
52
|
+
super()
|
53
|
+
|
54
|
+
# Enable dragging, if configured
|
55
|
+
if @listView and @listView.allowsDragToReorder
|
56
|
+
($ @el).attr "draggable", true
|
57
|
+
|
58
|
+
($ @el).attr('data-model-id', @model.get('id')) if @model?
|
59
|
+
|
60
|
+
onDragLeave: (event) =>
|
61
|
+
return unless @listView.allowsDragToReorder
|
62
|
+
($ @el).removeClass("drop-before").removeClass("drop-after")
|
63
|
+
|
64
|
+
onDragStart: (event) =>
|
65
|
+
return unless @listView.allowsDragToReorder
|
66
|
+
event.originalEvent.dataTransfer.setData("application/x-list-view-item-id", ($ @el).attr("id"))
|
67
|
+
@listView.draggedItems.add(@model)
|
68
|
+
|
69
|
+
onDragOver: (event) =>
|
70
|
+
return unless @listView.allowsDragToReorder
|
71
|
+
allowed = true
|
72
|
+
|
73
|
+
# Ensure that the dragged item is a list view item...
|
74
|
+
unless "application/x-list-view-item-id" in event.originalEvent.dataTransfer.types
|
75
|
+
allowed = false
|
76
|
+
|
77
|
+
# Ensure that the dragged item belongs to us...
|
78
|
+
unless @listView.collection.contains(@listView.draggedItems.models[0])
|
79
|
+
allowed = false
|
80
|
+
|
81
|
+
if allowed
|
82
|
+
if event.originalEvent.offsetY < (($ @el).height() / 2)
|
83
|
+
($ @el).removeClass("drop-after").addClass("drop-before")
|
84
|
+
else
|
85
|
+
($ @el).removeClass("drop-before").addClass("drop-after")
|