Skip to content

Commit dfd7b89

Browse files
author
Matthew Irish
committed
accessible drag and drop
fixes CNVS-7043 Added a `MoveToDialogView` that presents the user with one or two (if there are nested collections) <select>s. Each select (and corresponding label) is its own view called `MoveDialogSelect`. Using this new view, Assignments and Assignment Groups are now re-orderable from these dialogs (as well as drag and drop). Updated SortableCollectionView and DraggableCollectionView to sort the collection after updating positions and to also enforce uniqueness of the "position" attribute on DraggableCollectionView. Changed the `reorder_assignments` method on the AssignmentGroupsController to only return ids of active assignments. Test plan: - turn on draft state - with only one Assignment Group, there should be no "Move To ..." item in the cog menu - with more than one Assignment Group, there should be a "Move To ..." item - when clicked the "Move To" should open a dialog asking you where you want to move the Assignment group verify that: - the title of the Assignment Group is in dialog "Where would you like to move {AG Name here}?" - There is 1 select that does not include the Assignment Group as a value - the label for the select reads "Place Before:" - the select has an option to move to the bottom of the list - with multiple assignments, it is similar to the above with the exceptions: - the "Move To" menu item should disappear when there's only one assignment and one assignment group - there should be two selects in the dialog. One with the label "Place Before:", and one listing the assignment groups with the label "Assignment Group:" - the select for "Assignment Group" should have the current Assignment Group selected when the dialog is opened - when the Assignment Group select is changed, it should update the value of the "Place Before:" select with the names of the assignments in that Assignment Group - after saving, the items should update their order on the page - after moving an assignment once, bring up the move dialog again and verify that the Assignment Group select has the updated assignment group as the selected value Change-Id: Ifb6c1df155d3011949e25afeac111c230f5ff56a Reviewed-on: https://gerrit.instructure.com/24362 Tested-by: Jenkins <jenkins@instructure.com> QA-Review: Amber Taniuchi <amber@instructure.com> Reviewed-by: Simon Williams <simon@instructure.com> Product-Review: Simon Williams <simon@instructure.com>
1 parent c71bbb1 commit dfd7b89

23 files changed

Lines changed: 699 additions & 49 deletions

app/coffeescripts/collections/AssignmentCollection.coffee

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ define [
66
class AssignmentCollection extends Backbone.Collection
77

88
model: Assignment
9+
10+
comparator: 'position'

app/coffeescripts/collections/AssignmentGroupCollection.coffee

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@ define [
1414
defaults:
1515
params:
1616
include: ["assignments"]
17+
18+
comparator: 'position'

app/coffeescripts/views/DraggableCollectionView.coffee

Lines changed: 58 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ define [
4949
# add the view that is used when there are no items in a group
5050
_noItemsViewIfEmpty: =>
5151
items = @$list.children()
52+
5253
if items.length == 0
5354
@noItems = new Backbone.View
5455
template: @noItemTemplate
@@ -113,40 +114,66 @@ define [
113114
# Returns nothing.
114115
_updateSort: (e, ui) =>
115116
e.stopImmediatePropagation(); #parent sortables won't fire
117+
118+
# if the ui.item is not still inside the view, we only want to
119+
# resort, not save
120+
shouldSave = @$(ui.item).length
116121
#only save the sorting if this is the group that the item is in (moving to)
117122
id = @_getItemId(ui.item)
118-
sibling = @$list.children().find("[data-item-id=" + id + "]")
119-
if sibling.length > 0
120-
positions = {}
121-
positions[id] = ui.item.index() + 1
122-
for s in ui.item.siblings()
123-
$s = $(s)
124-
if $s.hasClass("no-items")
125-
$s.remove()
126-
else
127-
model_id = @_getItemId($s)
128-
129-
index = $s.prevAll().length
130-
new_position = index + 1
131-
positions[model_id] = new_position
132-
model = @searchItem(model_id)
133-
model.set('position', new_position)
134-
135-
@_sendPositions(@_orderPositions(positions))
136-
137-
# Internal: takes an object of {model_id:position} and returns an array
138-
# of model_ids in the correct order
139-
_orderPositions: (positions) ->
140-
sortable = []
141-
for id,order of positions
142-
sortable.push [id,order]
143-
sortable.sort (a,b) -> a[1] - b[1]
144-
output = []
145-
for model in sortable
146-
output.push model[0]
147-
output
123+
model = @collection.get(id)
124+
new_index = ui.item.index()
125+
126+
models = @updateModels(model, new_index, shouldSave)
127+
if shouldSave
128+
model.set 'position', new_index + 1
129+
@collection.sort()
130+
@_sendPositions(@collection.pluck('id'))
131+
else
132+
# will still have the moved model in the collection for now
133+
@collection.sort()
134+
135+
updateModels: (model, new_index, inView) =>
136+
# start at the model's current position because we don't want to include the model in the slice,
137+
# we'll update it separately
138+
old_pos = model.get('position')
139+
if old_pos
140+
old_index = old_pos - 1
141+
142+
movedDown = (old_index < new_index)
143+
#figure out how to slice the models
144+
slice_args =
145+
if !inView
146+
#model is being removed so we need to update everything
147+
#after it
148+
model.unset('position')
149+
[old_index]
150+
else if not old_pos
151+
#model is new so we need to update everything after it
152+
[new_index]
153+
else if movedDown
154+
# moved down so slice from old to new
155+
# we want to include the one at new index
156+
# so we add 1
157+
[old_index, new_index + 1]
158+
else
159+
# moved up so slice from new to old
160+
[new_index, old_index + 1]
161+
162+
#carve out just the models that need updating
163+
models_to_update = @collection.slice.apply @collection, slice_args
164+
#update the position on just these models
165+
_.each models_to_update, (m) ->
166+
#if the model gets sliced in here, don't update its
167+
#position as we'll update it later
168+
if m.id != model.id
169+
old = m.get('position')
170+
#if we moved an item down we want to move
171+
#the shifted items up (so we subtract 1)
172+
neue = if !inView or movedDown then old - 1 else old + 1
173+
m.set 'position', neue
174+
148175

149176
# Internal: sends an array of model_ids as a comma delimited string
150177
# to the sortURL
151178
_sendPositions: (ids) ->
152-
$.post @reorderURL, order: ids.join(",")
179+
$.post @reorderURL, order: ids.join(",")
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
define [
2+
'i18n!assignments'
3+
'Backbone'
4+
'jst/MoveDialogSelect'
5+
], (I18n, Backbone, template) ->
6+
7+
class MoveDialogSelect extends Backbone.View
8+
setViewProperties: false
9+
@optionProperty 'lastList'
10+
@optionProperty 'excludeModel'
11+
@optionProperty 'labelText'
12+
13+
className: 'move_select'
14+
template: template
15+
16+
getLabelText: ->
17+
@labelText or
18+
I18n.beforeLabel 'label_place_before', "Place before"
19+
20+
initialize: (options) ->
21+
super
22+
if @model and not @collection
23+
@collection = @model.collection if @model.collection
24+
25+
setCollection: (coll) ->
26+
return unless coll
27+
@collection = coll
28+
@renderOptions()
29+
30+
renderOptions: ->
31+
# I'm sorry, Voiceover + jQueryUI made me do it
32+
# VO won't acknowledge the existance of the re-rendered view
33+
# but if we render just the options, it's OK
34+
fragment = $(@template @toJSON())
35+
opts = fragment.filter('select').find('option')
36+
@$('select').empty().append(opts)
37+
38+
value: ->
39+
@$('select').val()
40+
41+
toJSON: ->
42+
data = @model.toView?() or @model.toJSON()
43+
44+
data.lastList = @lastList
45+
data.labelText = @getLabelText()
46+
data.items =
47+
if @excludeModel
48+
@collection.reject((m) =>
49+
@model.id == m.id
50+
).map (m) -> m.toView?() or m.toJSON()
51+
else
52+
@collection.toJSON()
53+
data
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
define [
2+
'underscore'
3+
'compiled/views/DialogFormView'
4+
'compiled/views/MoveDialogSelect'
5+
'jst/MoveDialog'
6+
'jst/EmptyDialogFormWrapper'
7+
], (_, DialogFormView, MoveDialogSelect, template, wrapper) ->
8+
9+
class MoveDialogView extends DialogFormView
10+
setViewProperties: false
11+
12+
defaults:
13+
width: 450
14+
height: 340
15+
16+
# {bool}
17+
@optionProperty 'nested'
18+
19+
# {Backbone.Collection}
20+
# Backbone Collection whose models contain a reference
21+
# to another Backbone Collection - referenced by @childKey
22+
# required if @nested
23+
@optionProperty 'parentCollection'
24+
25+
# {string}
26+
# text used to describe the select
27+
# MUST BE I18N STRING
28+
# required if @nested
29+
@optionProperty 'parentLabelText'
30+
31+
# {string}
32+
# The name of the attribute on @model
33+
# that stores the relation to @parentCollection
34+
@optionProperty 'parentKey'
35+
36+
# {string}
37+
# key to use to get the child collection from a model
38+
# in the parent collection
39+
# so if assignments are stored on a model and you
40+
# access the collection like so:
41+
# `model.get('assignments')`
42+
# then the childKey would be 'assignments'
43+
# required if @nested
44+
@optionProperty 'childKey'
45+
46+
# {string or function}
47+
# url to post to when saving the form
48+
#
49+
# if a function, will be called with this
50+
# view as context
51+
@optionProperty 'saveURL'
52+
53+
events: _.extend({}, @::events,
54+
'click .dialog_closer': 'close'
55+
'change .move_select_parent_collection': 'updateListView'
56+
)
57+
58+
els:
59+
'.child_container': '$childContainer'
60+
'.form-dialog-content': '$content'
61+
62+
template: template
63+
wrapperTemplate: wrapper
64+
65+
openAgain: ->
66+
super
67+
@initChildViews()
68+
@dialog.option "close", @cleanup
69+
70+
# creates @listView and @parentListView (if @nested)
71+
initChildViews: ->
72+
@listView = @parentListView = null
73+
if @nested and @parentCollection
74+
@listView = new MoveDialogSelect
75+
model: @model
76+
excludeModel: true
77+
lastList: true
78+
@parentListView = new MoveDialogSelect
79+
collection: @parentCollection
80+
model: @model
81+
labelText: @parentLabelText
82+
else
83+
@listView = new MoveDialogSelect
84+
model: @model
85+
excludeModel: true
86+
lastList: true
87+
88+
@attachChildViews()
89+
90+
91+
# attaches child views to @$childContainer
92+
attachChildViews: ->
93+
container = @$childContainer.detach()
94+
if @parentListView
95+
container.append(@parentListView.render().el)
96+
container.append(@listView.render().el)
97+
@$content.append(container)
98+
99+
cleanup: =>
100+
@parentListView?.remove()
101+
@listView?.remove()
102+
@parentListView = @listView = null
103+
@dialog.option "close", null
104+
105+
#lookup new collection, and set it on
106+
#the nested view
107+
updateListView: (e)->
108+
return unless @nested
109+
groupId = $(e.currentTarget).val()
110+
group = @parentCollection.get(groupId)
111+
children = group.get(@childKey)
112+
113+
@listView.setCollection(children)
114+
115+
toJSON: ->
116+
data = @model.toView?() or super
117+
118+
# should return an array of ids
119+
# in the order they should save
120+
getFormData: ->
121+
$select = @listView.$('select')
122+
selected = $select.val()
123+
vals = []
124+
_.each $select.find('option'), (ele, i) ->
125+
{value} = ele
126+
vals.push value unless value == 'last'
127+
128+
if selected == 'last'
129+
# just push model onto the end
130+
vals.push @model.id
131+
else
132+
# or find the index to insert it at and splice it in
133+
vals.splice _.indexOf(vals, selected), 0, @model.id
134+
vals
135+
136+
137+
# will always have data as we're
138+
# overriding getFormData
139+
#
140+
# getFromData should return a list
141+
# of ids
142+
saveFormData: (data) ->
143+
url = if typeof @saveURL is 'function'
144+
@saveURL.call @
145+
else
146+
@saveURL
147+
$.post url, order: data.join ','
148+
149+
150+
onSaveSuccess: (data) =>
151+
# assume collID is an int
152+
collID = parseInt @parentListView?.value(), 10
153+
newCollection = @parentCollection?.get(collID).get(@childKey)
154+
155+
# there is a currentCollection, but it doesn't match the model's collection
156+
if newCollection and newCollection != @model.collection
157+
#we need to remove the model from the previous collection
158+
#and add it to to the new one
159+
@model.collection.remove @model
160+
newCollection.add @model
161+
# also update the relationship to the collection
162+
# if we know how
163+
if @parentKey
164+
@model.set @parentKey, collID
165+
166+
positions = [1..newCollection.length]
167+
else
168+
newCollection = @model.collection
169+
#unfortunate thing we have to do for AssignmentGroups,
170+
#not sure about others...
171+
positions = newCollection.pluck 'position'
172+
173+
#update all of the position attributes
174+
_.each data.order, (id, index) ->
175+
newCollection.get(id)?.set 'position', positions[index]
176+
177+
newCollection.sort()
178+
# finally, call reset to trigger a re-render
179+
newCollection.reset newCollection.models
180+
181+
# close the dialog
182+
super

app/coffeescripts/views/SortableCollectionView.coffee

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ define [
6666
new_position = positions[model.id]
6767
model.set('position', new_position)
6868

69+
# make sure the collection stays in order
70+
@collection.sort()
6971
@_sendPositions(@_orderPositions(positions))
7072

7173
# Internal: takes an object of {model_id:position} and returns and array

0 commit comments

Comments
 (0)