Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
aade444f
Commit
aade444f
authored
Apr 03, 2014
by
cahrens
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Move overview drag and drop code to a utility class.
parent
2f16bbe1
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
698 additions
and
695 deletions
+698
-695
cms/static/coffee/spec/main.coffee
+4
-3
cms/static/coffee/spec/views/overview_spec.coffee
+0
-348
cms/static/js/spec/utils/drag_and_drop_spec.js
+313
-0
cms/static/js/utils/drag_and_drop.js
+346
-0
cms/static/js/views/overview.js
+5
-344
cms/templates/js/mock/mock-outline.underscore
+30
-0
No files found.
cms/static/coffee/spec/main.coffee
View file @
aade444f
...
...
@@ -207,16 +207,17 @@ define([
"js/spec/video/transcripts/videolist_spec"
,
"js/spec/video/transcripts/message_manager_spec"
,
"js/spec/video/transcripts/file_uploader_spec"
,
"js/spec/models/explicit_url_spec"
"js/spec/models/explicit_url_spec"
,
"js/spec/utils/drag_and_drop_spec"
,
"js/spec/utils/handle_iframe_binding_spec"
,
"js/spec/utils/module_spec"
,
"js/spec/views/baseview_spec"
,
"js/spec/views/paging_spec"
,
"js/spec/views/unit_spec"
"js/spec/views/xblock_spec"
"js/spec/views/unit_spec"
,
"js/spec/views/xblock_spec"
,
# these tests are run separate in the cms-squire suite, due to process
# isolation issues with Squire.js
...
...
cms/static/coffee/spec/views/overview_spec.coffee
View file @
aade444f
...
...
@@ -54,38 +54,6 @@ define ["js/views/overview", "js/views/feedback_notification", "js/spec/create_s
</section>
"""
appendSetFixtures
"""
<section>
<ol class="sortable-subsection-list">
<li class="courseware-subsection is-collapsible id-holder is-draggable" id="subsection-0" data-locator="subsection-0-id" style="margin:5px">
<ol class="sortable-unit-list" id="subsection-list-0">
<li class="courseware-unit unit is-draggable" id="unit-0" data-parent="subsection-0-id" data-locator="zero-unit-id"></li>
</ol>
</li>
<li class="courseware-subsection is-collapsible id-holder is-draggable" id="subsection-1" data-locator="subsection-1-id" style="margin:5px">
<ol class="sortable-unit-list" id="subsection-list-1">
<li class="courseware-unit unit is-draggable" id="unit-1" data-parent="subsection-1-id" data-locator="first-unit-id"></li>
<li class="courseware-unit unit is-draggable" id="unit-2" data-parent="subsection-1-id" data-locator="second-unit-id"></li>
<li class="courseware-unit unit is-draggable" id="unit-3" data-parent="subsection-1-id" data-locator="third-unit-id"></li>
</ol>
</li>
<li class="courseware-subsection is-collapsible id-holder is-draggable" id="subsection-2" data-locator="subsection-2-id" style="margin:5px">
<ol class="sortable-unit-list" id="subsection-list-2">
<li class="courseware-unit unit is-draggable" id="unit-4" data-parent="subsection-2" data-locator="fourth-unit-id"></li>
</ol>
</li>
<li class="courseware-subsection is-collapsible id-holder is-draggable" id="subsection-3" data-locator="subsection-3-id" style="margin:5px">
<ol class="sortable-unit-list" id="subsection-list-3"></ol>
</li>
<li class="courseware-subsection is-collapsible id-holder is-draggable" id="subsection-4" data-locator="subsection-4-id" style="margin:5px">
<ol class="sortable-unit-list" id="subsection-list-4">
<li class="courseware-unit unit is-draggable" id="unit-5" data-parent="subsection-4-id" data-locator="fifth-unit-id"></li>
</ol>
</li>
</ol>
</section>
"""
spyOn
(
Overview
,
'saveSetSectionScheduleDate'
).
andCallThrough
()
# Have to do this here, as it normally gets bound in document.ready()
$
(
'a.action-save'
).
click
(
Overview
.
saveSetSectionScheduleDate
)
...
...
@@ -96,20 +64,6 @@ define ["js/views/overview", "js/views/feedback_notification", "js/spec/create_s
window
.
analytics
=
jasmine
.
createSpyObj
(
'analytics'
,
[
'track'
])
window
.
course_location_analytics
=
jasmine
.
createSpy
()
Overview
.
overviewDragger
.
makeDraggable
(
'.unit'
,
'.unit-drag-handle'
,
'ol.sortable-unit-list'
,
'li.courseware-subsection, article.subsection-body'
)
Overview
.
overviewDragger
.
makeDraggable
(
'.courseware-subsection'
,
'.subsection-drag-handle'
,
'.sortable-subsection-list'
,
'section'
)
afterEach
->
delete
window
.
analytics
delete
window
.
course_location_analytics
...
...
@@ -143,305 +97,3 @@ define ["js/views/overview", "js/views/feedback_notification", "js/spec/create_s
# $('a.delete-section-button').click()
# $('a.action-primary').click()
# expect(@notificationSpy).toHaveBeenCalled()
describe
"findDestination"
,
->
it
"correctly finds the drop target of a drag"
,
->
$ele
=
$
(
'#unit-1'
)
$ele
.
offset
(
top
:
$ele
.
offset
().
top
+
10
,
left
:
$ele
.
offset
().
left
)
destination
=
Overview
.
overviewDragger
.
findDestination
(
$ele
,
1
)
expect
(
destination
.
ele
).
toBe
(
$
(
'#unit-2'
))
expect
(
destination
.
attachMethod
).
toBe
(
'before'
)
it
"can drag and drop across section boundaries, with special handling for single sibling"
,
->
$ele
=
$
(
'#unit-1'
)
$unit4
=
$
(
'#unit-4'
)
$ele
.
offset
(
top
:
$unit4
.
offset
().
top
+
8
left
:
$ele
.
offset
().
left
)
# Dragging down, we will insert after.
destination
=
Overview
.
overviewDragger
.
findDestination
(
$ele
,
1
)
expect
(
destination
.
ele
).
toBe
(
$unit4
)
expect
(
destination
.
attachMethod
).
toBe
(
'after'
)
# Dragging up, we will insert before.
destination
=
Overview
.
overviewDragger
.
findDestination
(
$ele
,
-
1
)
expect
(
destination
.
ele
).
toBe
(
$unit4
)
expect
(
destination
.
attachMethod
).
toBe
(
'before'
)
# If past the end the drop target, will attach after.
$ele
.
offset
(
top
:
$unit4
.
offset
().
top
+
$unit4
.
height
()
+
1
left
:
$ele
.
offset
().
left
)
destination
=
Overview
.
overviewDragger
.
findDestination
(
$ele
,
0
)
expect
(
destination
.
ele
).
toBe
(
$unit4
)
expect
(
destination
.
attachMethod
).
toBe
(
'after'
)
$unit0
=
$
(
'#unit-0'
)
# If before the start the drop target, will attach before.
$ele
.
offset
(
top
:
$unit0
.
offset
().
top
-
16
left
:
$ele
.
offset
().
left
)
destination
=
Overview
.
overviewDragger
.
findDestination
(
$ele
,
0
)
expect
(
destination
.
ele
).
toBe
(
$unit0
)
expect
(
destination
.
attachMethod
).
toBe
(
'before'
)
it
"""can drop before the first element, even if element being dragged is
slightly before the first element"""
,
->
$ele
=
$
(
'#subsection-2'
)
$ele
.
offset
(
top
:
$
(
'#subsection-0'
).
offset
().
top
-
5
left
:
$ele
.
offset
().
left
)
destination
=
Overview
.
overviewDragger
.
findDestination
(
$ele
,
-
1
)
expect
(
destination
.
ele
).
toBe
(
$
(
'#subsection-0'
))
expect
(
destination
.
attachMethod
).
toBe
(
'before'
)
it
"can drag and drop across section boundaries, with special handling for last element"
,
->
$ele
=
$
(
'#unit-4'
)
$ele
.
offset
(
top
:
$
(
'#unit-3'
).
offset
().
bottom
+
4
left
:
$ele
.
offset
().
left
)
destination
=
Overview
.
overviewDragger
.
findDestination
(
$ele
,
-
1
)
expect
(
destination
.
ele
).
toBe
(
$
(
'#unit-3'
))
# Dragging down up into last element, we have a fudge factor makes it easier to drag at beginning.
expect
(
destination
.
attachMethod
).
toBe
(
'after'
)
# Now past the "fudge factor".
$ele
.
offset
(
top
:
$
(
'#unit-3'
).
offset
().
top
+
4
left
:
$ele
.
offset
().
left
)
destination
=
Overview
.
overviewDragger
.
findDestination
(
$ele
,
-
1
)
expect
(
destination
.
ele
).
toBe
(
$
(
'#unit-3'
))
expect
(
destination
.
attachMethod
).
toBe
(
'before'
)
it
"""can drop past the last element, even if element being dragged is
slightly before/taller then the last element"""
,
->
$ele
=
$
(
'#subsection-2'
)
$ele
.
offset
(
# Make the top 1 before the top of the last element in the list.
# This mimics the problem when the element being dropped is taller then then
# the last element in the list.
top
:
$
(
'#subsection-4'
).
offset
().
top
-
1
left
:
$ele
.
offset
().
left
)
destination
=
Overview
.
overviewDragger
.
findDestination
(
$ele
,
1
)
expect
(
destination
.
ele
).
toBe
(
$
(
'#subsection-4'
))
expect
(
destination
.
attachMethod
).
toBe
(
'after'
)
it
"can drag into an empty list"
,
->
$ele
=
$
(
'#unit-1'
)
$ele
.
offset
(
top
:
$
(
'#subsection-3'
).
offset
().
top
+
10
left
:
$ele
.
offset
().
left
)
destination
=
Overview
.
overviewDragger
.
findDestination
(
$ele
,
1
)
expect
(
destination
.
ele
).
toBe
(
$
(
'#subsection-list-3'
))
expect
(
destination
.
attachMethod
).
toBe
(
'prepend'
)
it
"reports a null destination on a failed drag"
,
->
$ele
=
$
(
'#unit-1'
)
$ele
.
offset
(
top
:
$ele
.
offset
().
top
+
200
,
left
:
$ele
.
offset
().
left
)
destination
=
Overview
.
overviewDragger
.
findDestination
(
$ele
,
1
)
expect
(
destination
).
toEqual
(
ele
:
null
attachMethod
:
""
)
it
"can drag into a collapsed list"
,
->
$
(
'#subsection-2'
).
addClass
(
'collapsed'
)
$ele
=
$
(
'#unit-2'
)
$ele
.
offset
(
top
:
$
(
'#subsection-2'
).
offset
().
top
+
3
left
:
$ele
.
offset
().
left
)
destination
=
Overview
.
overviewDragger
.
findDestination
(
$ele
,
1
)
expect
(
destination
.
ele
).
toBe
(
$
(
'#subsection-list-2'
))
expect
(
destination
.
parentList
).
toBe
(
$
(
'#subsection-2'
))
expect
(
destination
.
attachMethod
).
toBe
(
'prepend'
)
describe
"onDragStart"
,
->
it
"sets the dragState to its default values"
,
->
expect
(
Overview
.
overviewDragger
.
dragState
).
toEqual
({})
# Call with some dummy data
Overview
.
overviewDragger
.
onDragStart
(
{
element
:
$
(
'#unit-1'
)},
null
,
null
)
expect
(
Overview
.
overviewDragger
.
dragState
).
toEqual
(
dropDestination
:
null
,
attachMethod
:
''
,
parentList
:
null
,
lastY
:
0
,
dragDirection
:
0
)
it
"collapses expanded elements"
,
->
expect
(
$
(
'#subsection-1'
)).
not
.
toHaveClass
(
'collapsed'
)
Overview
.
overviewDragger
.
onDragStart
(
{
element
:
$
(
'#subsection-1'
)},
null
,
null
)
expect
(
$
(
'#subsection-1'
)).
toHaveClass
(
'collapsed'
)
expect
(
$
(
'#subsection-1'
)).
toHaveClass
(
'expand-on-drop'
)
describe
"onDragMove"
,
->
beforeEach
->
@
scrollSpy
=
spyOn
(
window
,
'scrollBy'
).
andCallThrough
()
it
"adds the correct CSS class to the drop destination"
,
->
$ele
=
$
(
'#unit-1'
)
dragY
=
$ele
.
offset
().
top
+
10
dragX
=
$ele
.
offset
().
left
$ele
.
offset
(
top
:
dragY
,
left
:
dragX
)
Overview
.
overviewDragger
.
onDragMove
(
{
element
:
$ele
,
dragPoint
:
{
y
:
dragY
}},
''
,
{
clientX
:
dragX
}
)
expect
(
$
(
'#unit-2'
)).
toHaveClass
(
'drop-target drop-target-before'
)
expect
(
$ele
).
toHaveClass
(
'valid-drop'
)
it
"does not add CSS class to the drop destination if out of bounds"
,
->
$ele
=
$
(
'#unit-1'
)
dragY
=
$ele
.
offset
().
top
+
10
$ele
.
offset
(
top
:
dragY
,
left
:
$ele
.
offset
().
left
)
Overview
.
overviewDragger
.
onDragMove
(
{
element
:
$ele
,
dragPoint
:
{
y
:
dragY
}},
''
,
{
clientX
:
$ele
.
offset
().
left
-
3
}
)
expect
(
$
(
'#unit-2'
)).
not
.
toHaveClass
(
'drop-target drop-target-before'
)
expect
(
$ele
).
not
.
toHaveClass
(
'valid-drop'
)
it
"scrolls up if necessary"
,
->
Overview
.
overviewDragger
.
onDragMove
(
{
element
:
$
(
'#unit-1'
)},
''
,
{
clientY
:
2
}
)
expect
(
@
scrollSpy
).
toHaveBeenCalledWith
(
0
,
-
10
)
it
"scrolls down if necessary"
,
->
Overview
.
overviewDragger
.
onDragMove
(
{
element
:
$
(
'#unit-1'
)},
''
,
{
clientY
:
(
window
.
innerHeight
-
5
)}
)
expect
(
@
scrollSpy
).
toHaveBeenCalledWith
(
0
,
10
)
describe
"onDragEnd"
,
->
beforeEach
->
@
reorderSpy
=
spyOn
(
Overview
.
overviewDragger
,
'handleReorder'
)
afterEach
->
@
reorderSpy
.
reset
()
it
"calls handleReorder on a successful drag"
,
->
Overview
.
overviewDragger
.
dragState
.
dropDestination
=
$
(
'#unit-2'
)
Overview
.
overviewDragger
.
dragState
.
attachMethod
=
"before"
Overview
.
overviewDragger
.
dragState
.
parentList
=
$
(
'#subsection-1'
)
$
(
'#unit-1'
).
offset
(
top
:
$
(
'#unit-1'
).
offset
().
top
+
10
left
:
$
(
'#unit-1'
).
offset
().
left
)
Overview
.
overviewDragger
.
onDragEnd
(
{
element
:
$
(
'#unit-1'
)},
null
,
{
clientX
:
$
(
'#unit-1'
).
offset
().
left
}
)
expect
(
@
reorderSpy
).
toHaveBeenCalled
()
it
"clears out the drag state"
,
->
Overview
.
overviewDragger
.
onDragEnd
(
{
element
:
$
(
'#unit-1'
)},
null
,
null
)
expect
(
Overview
.
overviewDragger
.
dragState
).
toEqual
({})
it
"sets the element to the correct position"
,
->
Overview
.
overviewDragger
.
onDragEnd
(
{
element
:
$
(
'#unit-1'
)},
null
,
null
)
# Chrome sets the CSS to 'auto', but Firefox uses '0px'.
expect
([
'0px'
,
'auto'
]).
toContain
(
$
(
'#unit-1'
).
css
(
'top'
))
expect
([
'0px'
,
'auto'
]).
toContain
(
$
(
'#unit-1'
).
css
(
'left'
))
it
"expands an element if it was collapsed on drag start"
,
->
$
(
'#subsection-1'
).
addClass
(
'collapsed'
)
$
(
'#subsection-1'
).
addClass
(
'expand-on-drop'
)
Overview
.
overviewDragger
.
onDragEnd
(
{
element
:
$
(
'#subsection-1'
)},
null
,
null
)
expect
(
$
(
'#subsection-1'
)).
not
.
toHaveClass
(
'collapsed'
)
expect
(
$
(
'#subsection-1'
)).
not
.
toHaveClass
(
'expand-on-drop'
)
it
"expands a collapsed element when something is dropped in it"
,
->
$
(
'#subsection-2'
).
addClass
(
'collapsed'
)
Overview
.
overviewDragger
.
dragState
.
dropDestination
=
$
(
'#list-2'
)
Overview
.
overviewDragger
.
dragState
.
attachMethod
=
"prepend"
Overview
.
overviewDragger
.
dragState
.
parentList
=
$
(
'#subsection-2'
)
Overview
.
overviewDragger
.
onDragEnd
(
{
element
:
$
(
'#unit-1'
)},
null
,
{
clientX
:
$
(
'#unit-1'
).
offset
().
left
}
)
expect
(
$
(
'#subsection-2'
)).
not
.
toHaveClass
(
'collapsed'
)
describe
"AJAX"
,
->
beforeEach
->
@
savingSpies
=
spyOnConstructor
(
Notification
,
"Mini"
,
[
"show"
,
"hide"
])
@
savingSpies
.
show
.
andReturn
(
@
savingSpies
)
@
clock
=
sinon
.
useFakeTimers
()
afterEach
->
@
clock
.
restore
()
it
"should send an update on reorder"
,
->
requests
=
create_sinon
[
"requests"
](
this
)
Overview
.
overviewDragger
.
dragState
.
dropDestination
=
$
(
'#unit-4'
)
Overview
.
overviewDragger
.
dragState
.
attachMethod
=
"after"
Overview
.
overviewDragger
.
dragState
.
parentList
=
$
(
'#subsection-2'
)
# Drag Unit 1 from Subsection 1 to the end of Subsection 2.
$
(
'#unit-1'
).
offset
(
top
:
$
(
'#unit-4'
).
offset
().
top
+
10
left
:
$
(
'#unit-4'
).
offset
().
left
)
Overview
.
overviewDragger
.
onDragEnd
(
{
element
:
$
(
'#unit-1'
)},
null
,
{
clientX
:
$
(
'#unit-1'
).
offset
().
left
}
)
expect
(
requests
.
length
).
toEqual
(
2
)
expect
(
@
savingSpies
.
constructor
).
toHaveBeenCalled
()
expect
(
@
savingSpies
.
show
).
toHaveBeenCalled
()
expect
(
@
savingSpies
.
hide
).
not
.
toHaveBeenCalled
()
savingOptions
=
@
savingSpies
.
constructor
.
mostRecentCall
.
args
[
0
]
expect
(
savingOptions
.
title
).
toMatch
(
/Saving/
)
expect
(
$
(
'#unit-1'
)).
toHaveClass
(
'was-dropped'
)
# We expect 2 requests to be sent-- the first for removing Unit 1 from Subsection 1,
# and the second for adding Unit 1 to the end of Subsection 2.
expect
(
requests
[
0
].
requestBody
).
toEqual
(
'{"children":["second-unit-id","third-unit-id"]}'
)
requests
[
0
].
respond
(
200
)
expect
(
@
savingSpies
.
hide
).
not
.
toHaveBeenCalled
()
expect
(
requests
[
1
].
requestBody
).
toEqual
(
'{"children":["fourth-unit-id","first-unit-id"]}'
)
requests
[
1
].
respond
(
200
)
expect
(
@
savingSpies
.
hide
).
toHaveBeenCalled
()
# Class is removed in a timeout.
@
clock
.
tick
(
1001
)
expect
(
$
(
'#unit-1'
)).
not
.
toHaveClass
(
'was-dropped'
)
cms/static/js/spec/utils/drag_and_drop_spec.js
0 → 100644
View file @
aade444f
define
([
"js/utils/drag_and_drop"
,
"js/views/feedback_notification"
,
"js/spec/create_sinon"
,
"jquery"
],
function
(
ContentDragger
,
Notification
,
create_sinon
,
$
)
{
describe
(
"Overview drag and drop functionality"
,
function
()
{
beforeEach
(
function
()
{
setFixtures
(
readFixtures
(
'mock/mock-outline.underscore'
));
ContentDragger
.
makeDraggable
(
'.unit'
,
'.unit-drag-handle'
,
'ol.sortable-unit-list'
,
'li.courseware-subsection, article.subsection-body'
);
ContentDragger
.
makeDraggable
(
'.courseware-subsection'
,
'.subsection-drag-handle'
,
'.sortable-subsection-list'
,
'section'
);
});
describe
(
"findDestination"
,
function
()
{
it
(
"correctly finds the drop target of a drag"
,
function
()
{
var
$ele
,
destination
;
$ele
=
$
(
'#unit-1'
);
$ele
.
offset
({
top
:
$ele
.
offset
().
top
+
10
,
left
:
$ele
.
offset
().
left
});
destination
=
ContentDragger
.
findDestination
(
$ele
,
1
);
expect
(
destination
.
ele
).
toBe
(
$
(
'#unit-2'
));
expect
(
destination
.
attachMethod
).
toBe
(
'before'
);
});
it
(
"can drag and drop across section boundaries, with special handling for single sibling"
,
function
()
{
var
$ele
,
$unit0
,
$unit4
,
destination
;
$ele
=
$
(
'#unit-1'
);
$unit4
=
$
(
'#unit-4'
);
$ele
.
offset
({
top
:
$unit4
.
offset
().
top
+
8
,
left
:
$ele
.
offset
().
left
});
destination
=
ContentDragger
.
findDestination
(
$ele
,
1
);
expect
(
destination
.
ele
).
toBe
(
$unit4
);
expect
(
destination
.
attachMethod
).
toBe
(
'after'
);
destination
=
ContentDragger
.
findDestination
(
$ele
,
-
1
);
expect
(
destination
.
ele
).
toBe
(
$unit4
);
expect
(
destination
.
attachMethod
).
toBe
(
'before'
);
$ele
.
offset
({
top
:
$unit4
.
offset
().
top
+
$unit4
.
height
()
+
1
,
left
:
$ele
.
offset
().
left
});
destination
=
ContentDragger
.
findDestination
(
$ele
,
0
);
expect
(
destination
.
ele
).
toBe
(
$unit4
);
expect
(
destination
.
attachMethod
).
toBe
(
'after'
);
$unit0
=
$
(
'#unit-0'
);
$ele
.
offset
({
top
:
$unit0
.
offset
().
top
-
16
,
left
:
$ele
.
offset
().
left
});
destination
=
ContentDragger
.
findDestination
(
$ele
,
0
);
expect
(
destination
.
ele
).
toBe
(
$unit0
);
expect
(
destination
.
attachMethod
).
toBe
(
'before'
);
});
it
(
"can drop before the first element, even if element being dragged is
\
nslightly before the first element"
,
function
()
{
var
$ele
,
destination
;
$ele
=
$
(
'#subsection-2'
);
$ele
.
offset
({
top
:
$
(
'#subsection-0'
).
offset
().
top
-
5
,
left
:
$ele
.
offset
().
left
});
destination
=
ContentDragger
.
findDestination
(
$ele
,
-
1
);
expect
(
destination
.
ele
).
toBe
(
$
(
'#subsection-0'
));
expect
(
destination
.
attachMethod
).
toBe
(
'before'
);
});
it
(
"can drag and drop across section boundaries, with special handling for last element"
,
function
()
{
var
$ele
,
destination
;
$ele
=
$
(
'#unit-4'
);
$ele
.
offset
({
top
:
$
(
'#unit-3'
).
offset
().
bottom
+
4
,
left
:
$ele
.
offset
().
left
});
destination
=
ContentDragger
.
findDestination
(
$ele
,
-
1
);
expect
(
destination
.
ele
).
toBe
(
$
(
'#unit-3'
));
expect
(
destination
.
attachMethod
).
toBe
(
'after'
);
$ele
.
offset
({
top
:
$
(
'#unit-3'
).
offset
().
top
+
4
,
left
:
$ele
.
offset
().
left
});
destination
=
ContentDragger
.
findDestination
(
$ele
,
-
1
);
expect
(
destination
.
ele
).
toBe
(
$
(
'#unit-3'
));
expect
(
destination
.
attachMethod
).
toBe
(
'before'
);
});
it
(
"can drop past the last element, even if element being dragged is
\
nslightly before/taller then the last element"
,
function
()
{
var
$ele
,
destination
;
$ele
=
$
(
'#subsection-2'
);
$ele
.
offset
({
top
:
$
(
'#subsection-4'
).
offset
().
top
-
1
,
left
:
$ele
.
offset
().
left
});
destination
=
ContentDragger
.
findDestination
(
$ele
,
1
);
expect
(
destination
.
ele
).
toBe
(
$
(
'#subsection-4'
));
expect
(
destination
.
attachMethod
).
toBe
(
'after'
);
});
it
(
"can drag into an empty list"
,
function
()
{
var
$ele
,
destination
;
$ele
=
$
(
'#unit-1'
);
$ele
.
offset
({
top
:
$
(
'#subsection-3'
).
offset
().
top
+
10
,
left
:
$ele
.
offset
().
left
});
destination
=
ContentDragger
.
findDestination
(
$ele
,
1
);
expect
(
destination
.
ele
).
toBe
(
$
(
'#subsection-list-3'
));
expect
(
destination
.
attachMethod
).
toBe
(
'prepend'
);
});
it
(
"reports a null destination on a failed drag"
,
function
()
{
var
$ele
,
destination
;
$ele
=
$
(
'#unit-1'
);
$ele
.
offset
({
top
:
$ele
.
offset
().
top
+
200
,
left
:
$ele
.
offset
().
left
});
destination
=
ContentDragger
.
findDestination
(
$ele
,
1
);
expect
(
destination
).
toEqual
({
ele
:
null
,
attachMethod
:
""
});
});
it
(
"can drag into a collapsed list"
,
function
()
{
var
$ele
,
destination
;
$
(
'#subsection-2'
).
addClass
(
'collapsed'
);
$ele
=
$
(
'#unit-2'
);
$ele
.
offset
({
top
:
$
(
'#subsection-2'
).
offset
().
top
+
3
,
left
:
$ele
.
offset
().
left
});
destination
=
ContentDragger
.
findDestination
(
$ele
,
1
);
expect
(
destination
.
ele
).
toBe
(
$
(
'#subsection-list-2'
));
expect
(
destination
.
parentList
).
toBe
(
$
(
'#subsection-2'
));
expect
(
destination
.
attachMethod
).
toBe
(
'prepend'
);
});
});
describe
(
"onDragStart"
,
function
()
{
it
(
"sets the dragState to its default values"
,
function
()
{
expect
(
ContentDragger
.
dragState
).
toEqual
({});
ContentDragger
.
onDragStart
({
element
:
$
(
'#unit-1'
)
},
null
,
null
);
expect
(
ContentDragger
.
dragState
).
toEqual
({
dropDestination
:
null
,
attachMethod
:
''
,
parentList
:
null
,
lastY
:
0
,
dragDirection
:
0
});
});
it
(
"collapses expanded elements"
,
function
()
{
expect
(
$
(
'#subsection-1'
)).
not
.
toHaveClass
(
'collapsed'
);
ContentDragger
.
onDragStart
({
element
:
$
(
'#subsection-1'
)
},
null
,
null
);
expect
(
$
(
'#subsection-1'
)).
toHaveClass
(
'collapsed'
);
expect
(
$
(
'#subsection-1'
)).
toHaveClass
(
'expand-on-drop'
);
});
});
describe
(
"onDragMove"
,
function
()
{
beforeEach
(
function
()
{
this
.
scrollSpy
=
spyOn
(
window
,
'scrollBy'
).
andCallThrough
();
});
it
(
"adds the correct CSS class to the drop destination"
,
function
()
{
var
$ele
,
dragX
,
dragY
;
$ele
=
$
(
'#unit-1'
);
dragY
=
$ele
.
offset
().
top
+
10
;
dragX
=
$ele
.
offset
().
left
;
$ele
.
offset
({
top
:
dragY
,
left
:
dragX
});
ContentDragger
.
onDragMove
({
element
:
$ele
,
dragPoint
:
{
y
:
dragY
}
},
''
,
{
clientX
:
dragX
});
expect
(
$
(
'#unit-2'
)).
toHaveClass
(
'drop-target drop-target-before'
);
expect
(
$ele
).
toHaveClass
(
'valid-drop'
);
});
it
(
"does not add CSS class to the drop destination if out of bounds"
,
function
()
{
var
$ele
,
dragY
;
$ele
=
$
(
'#unit-1'
);
dragY
=
$ele
.
offset
().
top
+
10
;
$ele
.
offset
({
top
:
dragY
,
left
:
$ele
.
offset
().
left
});
ContentDragger
.
onDragMove
({
element
:
$ele
,
dragPoint
:
{
y
:
dragY
}
},
''
,
{
clientX
:
$ele
.
offset
().
left
-
3
});
expect
(
$
(
'#unit-2'
)).
not
.
toHaveClass
(
'drop-target drop-target-before'
);
expect
(
$ele
).
not
.
toHaveClass
(
'valid-drop'
);
});
it
(
"scrolls up if necessary"
,
function
()
{
ContentDragger
.
onDragMove
({
element
:
$
(
'#unit-1'
)
},
''
,
{
clientY
:
2
});
expect
(
this
.
scrollSpy
).
toHaveBeenCalledWith
(
0
,
-
10
);
});
it
(
"scrolls down if necessary"
,
function
()
{
ContentDragger
.
onDragMove
({
element
:
$
(
'#unit-1'
)
},
''
,
{
clientY
:
window
.
innerHeight
-
5
});
expect
(
this
.
scrollSpy
).
toHaveBeenCalledWith
(
0
,
10
);
});
});
describe
(
"onDragEnd"
,
function
()
{
beforeEach
(
function
()
{
this
.
reorderSpy
=
spyOn
(
ContentDragger
,
'handleReorder'
);
});
afterEach
(
function
()
{
this
.
reorderSpy
.
reset
();
});
it
(
"calls handleReorder on a successful drag"
,
function
()
{
ContentDragger
.
dragState
.
dropDestination
=
$
(
'#unit-2'
);
ContentDragger
.
dragState
.
attachMethod
=
"before"
;
ContentDragger
.
dragState
.
parentList
=
$
(
'#subsection-1'
);
$
(
'#unit-1'
).
offset
({
top
:
$
(
'#unit-1'
).
offset
().
top
+
10
,
left
:
$
(
'#unit-1'
).
offset
().
left
});
ContentDragger
.
onDragEnd
({
element
:
$
(
'#unit-1'
)
},
null
,
{
clientX
:
$
(
'#unit-1'
).
offset
().
left
});
expect
(
this
.
reorderSpy
).
toHaveBeenCalled
();
});
it
(
"clears out the drag state"
,
function
()
{
ContentDragger
.
onDragEnd
({
element
:
$
(
'#unit-1'
)
},
null
,
null
);
expect
(
ContentDragger
.
dragState
).
toEqual
({});
});
it
(
"sets the element to the correct position"
,
function
()
{
ContentDragger
.
onDragEnd
({
element
:
$
(
'#unit-1'
)
},
null
,
null
);
expect
([
'0px'
,
'auto'
]).
toContain
(
$
(
'#unit-1'
).
css
(
'top'
));
expect
([
'0px'
,
'auto'
]).
toContain
(
$
(
'#unit-1'
).
css
(
'left'
));
});
it
(
"expands an element if it was collapsed on drag start"
,
function
()
{
$
(
'#subsection-1'
).
addClass
(
'collapsed'
);
$
(
'#subsection-1'
).
addClass
(
'expand-on-drop'
);
ContentDragger
.
onDragEnd
({
element
:
$
(
'#subsection-1'
)
},
null
,
null
);
expect
(
$
(
'#subsection-1'
)).
not
.
toHaveClass
(
'collapsed'
);
expect
(
$
(
'#subsection-1'
)).
not
.
toHaveClass
(
'expand-on-drop'
);
});
it
(
"expands a collapsed element when something is dropped in it"
,
function
()
{
$
(
'#subsection-2'
).
addClass
(
'collapsed'
);
ContentDragger
.
dragState
.
dropDestination
=
$
(
'#list-2'
);
ContentDragger
.
dragState
.
attachMethod
=
"prepend"
;
ContentDragger
.
dragState
.
parentList
=
$
(
'#subsection-2'
);
ContentDragger
.
onDragEnd
({
element
:
$
(
'#unit-1'
)
},
null
,
{
clientX
:
$
(
'#unit-1'
).
offset
().
left
});
expect
(
$
(
'#subsection-2'
)).
not
.
toHaveClass
(
'collapsed'
);
});
});
describe
(
"AJAX"
,
function
()
{
beforeEach
(
function
()
{
this
.
savingSpies
=
spyOnConstructor
(
Notification
,
"Mini"
,
[
"show"
,
"hide"
]);
this
.
savingSpies
.
show
.
andReturn
(
this
.
savingSpies
);
this
.
clock
=
sinon
.
useFakeTimers
();
});
afterEach
(
function
()
{
this
.
clock
.
restore
();
});
it
(
"should send an update on reorder"
,
function
()
{
var
requests
,
savingOptions
;
requests
=
create_sinon
[
"requests"
](
this
);
ContentDragger
.
dragState
.
dropDestination
=
$
(
'#unit-4'
);
ContentDragger
.
dragState
.
attachMethod
=
"after"
;
ContentDragger
.
dragState
.
parentList
=
$
(
'#subsection-2'
);
$
(
'#unit-1'
).
offset
({
top
:
$
(
'#unit-4'
).
offset
().
top
+
10
,
left
:
$
(
'#unit-4'
).
offset
().
left
});
ContentDragger
.
onDragEnd
({
element
:
$
(
'#unit-1'
)
},
null
,
{
clientX
:
$
(
'#unit-1'
).
offset
().
left
});
expect
(
requests
.
length
).
toEqual
(
2
);
expect
(
this
.
savingSpies
.
constructor
).
toHaveBeenCalled
();
expect
(
this
.
savingSpies
.
show
).
toHaveBeenCalled
();
expect
(
this
.
savingSpies
.
hide
).
not
.
toHaveBeenCalled
();
savingOptions
=
this
.
savingSpies
.
constructor
.
mostRecentCall
.
args
[
0
];
expect
(
savingOptions
.
title
).
toMatch
(
/Saving/
);
expect
(
$
(
'#unit-1'
)).
toHaveClass
(
'was-dropped'
);
expect
(
requests
[
0
].
requestBody
).
toEqual
(
'{"children":["second-unit-id","third-unit-id"]}'
);
requests
[
0
].
respond
(
200
);
expect
(
this
.
savingSpies
.
hide
).
not
.
toHaveBeenCalled
();
expect
(
requests
[
1
].
requestBody
).
toEqual
(
'{"children":["fourth-unit-id","first-unit-id"]}'
);
requests
[
1
].
respond
(
200
);
expect
(
this
.
savingSpies
.
hide
).
toHaveBeenCalled
();
this
.
clock
.
tick
(
1001
);
expect
(
$
(
'#unit-1'
)).
not
.
toHaveClass
(
'was-dropped'
);
});
});
});
});
cms/static/js/utils/drag_and_drop.js
0 → 100644
View file @
aade444f
define
([
"jquery"
,
"jquery.ui"
,
"underscore"
,
"gettext"
,
"js/views/feedback_notification"
,
"draggabilly"
,
"js/utils/module"
],
function
(
$
,
ui
,
_
,
gettext
,
NotificationView
,
Draggabilly
,
ModuleUtils
)
{
var
contentDragger
=
{
droppableClasses
:
'drop-target drop-target-prepend drop-target-before drop-target-after'
,
validDropClass
:
"valid-drop"
,
expandOnDropClass
:
"expand-on-drop"
,
/*
* Determine information about where to drop the currently dragged
* element. Returns the element to attach to and the method of
* attachment ('before', 'after', or 'prepend').
*/
findDestination
:
function
(
ele
,
yChange
)
{
var
eleY
=
ele
.
offset
().
top
;
var
eleYEnd
=
eleY
+
ele
.
height
();
var
containers
=
$
(
ele
.
data
(
'droppable-class'
));
for
(
var
i
=
0
;
i
<
containers
.
length
;
i
++
)
{
var
container
=
$
(
containers
[
i
]);
// Exclude the 'new unit' buttons, and make sure we don't
// prepend an element to itself
var
siblings
=
container
.
children
().
filter
(
function
()
{
return
$
(
this
).
data
(
'locator'
)
!==
undefined
&&
!
$
(
this
).
is
(
ele
);
});
// If the container is collapsed, check to see if the
// element is on top of its parent list -- don't check the
// position of the container
var
parentList
=
container
.
parents
(
ele
.
data
(
'parent-location-selector'
)).
first
();
if
(
parentList
.
hasClass
(
'collapsed'
))
{
var
parentListTop
=
parentList
.
offset
().
top
;
// To make it easier to drop subsections into collapsed sections (which have
// a lot of visual padding around them), allow a fudge factor around the
// parent element.
var
collapseFudge
=
10
;
if
(
Math
.
abs
(
eleY
-
parentListTop
)
<
collapseFudge
||
(
eleY
>
parentListTop
&&
eleYEnd
-
collapseFudge
<=
parentListTop
+
parentList
.
height
())
)
{
return
{
ele
:
container
,
attachMethod
:
'prepend'
,
parentList
:
parentList
};
}
}
// Otherwise, do check the container
else
{
// If the list is empty, we should prepend to it,
// unless both elements are at the same location --
// this prevents the user from being unable to expand
// a section
var
containerY
=
container
.
offset
().
top
;
if
(
siblings
.
length
===
0
&&
containerY
!==
eleY
&&
Math
.
abs
(
eleY
-
containerY
)
<
50
)
{
return
{
ele
:
container
,
attachMethod
:
'prepend'
};
}
// Otherwise the list is populated, and we should attach before/after a sibling
else
{
for
(
var
j
=
0
;
j
<
siblings
.
length
;
j
++
)
{
var
$sibling
=
$
(
siblings
[
j
]);
var
siblingY
=
$sibling
.
offset
().
top
;
var
siblingHeight
=
$sibling
.
height
();
var
siblingYEnd
=
siblingY
+
siblingHeight
;
// Facilitate dropping into the beginning or end of a list
// (coming from opposite direction) via a "fudge factor". Math.min is for Jasmine test.
var
fudge
=
Math
.
min
(
Math
.
ceil
(
siblingHeight
/
2
),
20
);
// Dragging to top or bottom of a list with only one element is tricky
// because the element being dragged may be the same size as the sibling.
if
(
siblings
.
length
===
1
)
{
// Element being dragged is within the drop target. Use the direction
// of the drag (yChange) to determine before or after.
if
(
eleY
+
fudge
>=
siblingY
&&
eleYEnd
-
fudge
<=
siblingYEnd
)
{
return
{
ele
:
$sibling
,
attachMethod
:
yChange
>
0
?
'after'
:
'before'
};
}
// Element being dragged is before the drop target.
else
if
(
Math
.
abs
(
eleYEnd
-
siblingY
)
<=
fudge
)
{
return
{
ele
:
$sibling
,
attachMethod
:
'before'
};
}
// Element being dragged is after the drop target.
else
if
(
Math
.
abs
(
eleY
-
siblingYEnd
)
<=
fudge
)
{
return
{
ele
:
$sibling
,
attachMethod
:
'after'
};
}
}
else
{
// Dragging up into end of list.
if
(
j
===
siblings
.
length
-
1
&&
yChange
<
0
&&
Math
.
abs
(
eleY
-
siblingYEnd
)
<=
fudge
)
{
return
{
ele
:
$sibling
,
attachMethod
:
'after'
};
}
// Dragging up or down into beginning of list.
else
if
(
j
===
0
&&
Math
.
abs
(
eleY
-
siblingY
)
<=
fudge
)
{
return
{
ele
:
$sibling
,
attachMethod
:
'before'
};
}
// Dragging down into end of list. Special handling required because
// the element being dragged may be taller then the element being dragged over
// (if eleY can never be >= siblingY, general case at the end does not work).
else
if
(
j
===
siblings
.
length
-
1
&&
yChange
>
0
&&
Math
.
abs
(
eleYEnd
-
siblingYEnd
)
<=
fudge
)
{
return
{
ele
:
$sibling
,
attachMethod
:
'after'
};
}
else
if
(
eleY
>=
siblingY
&&
eleY
<=
siblingYEnd
)
{
return
{
ele
:
$sibling
,
attachMethod
:
eleY
-
siblingY
<=
siblingHeight
/
2
?
'before'
:
'after'
};
}
}
}
}
}
}
// Failed drag
return
{
ele
:
null
,
attachMethod
:
''
};
},
// Information about the current drag.
dragState
:
{},
onDragStart
:
function
(
draggie
,
event
,
pointer
)
{
var
ele
=
$
(
draggie
.
element
);
this
.
dragState
=
{
// Which element will be dropped into/onto on success
dropDestination
:
null
,
// How we attach to the destination: 'before', 'after', 'prepend'
attachMethod
:
''
,
// If dragging to an empty section, the parent section
parentList
:
null
,
// The y location of the last dragMove event (to determine direction).
lastY
:
0
,
// The direction the drag is moving in (negative means up, positive down).
dragDirection
:
0
};
if
(
!
ele
.
hasClass
(
'collapsed'
))
{
ele
.
addClass
(
'collapsed'
);
ele
.
find
(
'.expand-collapse'
).
first
().
addClass
(
'expand'
).
removeClass
(
'collapse'
);
// onDragStart gets called again after the collapse, so we can't just store a variable in the dragState.
ele
.
addClass
(
this
.
expandOnDropClass
);
}
},
onDragMove
:
function
(
draggie
,
event
,
pointer
)
{
// Handle scrolling of the browser.
var
scrollAmount
=
0
;
var
dragBuffer
=
10
;
if
(
window
.
innerHeight
-
dragBuffer
<
pointer
.
clientY
)
{
scrollAmount
=
dragBuffer
;
}
else
if
(
dragBuffer
>
pointer
.
clientY
)
{
scrollAmount
=
-
(
dragBuffer
);
}
if
(
scrollAmount
!==
0
)
{
window
.
scrollBy
(
0
,
scrollAmount
);
return
;
}
var
yChange
=
draggie
.
dragPoint
.
y
-
this
.
dragState
.
lastY
;
if
(
yChange
!==
0
)
{
this
.
dragState
.
direction
=
yChange
;
}
this
.
dragState
.
lastY
=
draggie
.
dragPoint
.
y
;
var
ele
=
$
(
draggie
.
element
);
var
destinationInfo
=
this
.
findDestination
(
ele
,
this
.
dragState
.
direction
);
var
destinationEle
=
destinationInfo
.
ele
;
this
.
dragState
.
parentList
=
destinationInfo
.
parentList
;
// Clear out the old destination
if
(
this
.
dragState
.
dropDestination
)
{
this
.
dragState
.
dropDestination
.
removeClass
(
this
.
droppableClasses
);
}
// Mark the new destination
if
(
destinationEle
&&
this
.
pointerInBounds
(
pointer
,
ele
))
{
ele
.
addClass
(
this
.
validDropClass
);
destinationEle
.
addClass
(
'drop-target drop-target-'
+
destinationInfo
.
attachMethod
);
this
.
dragState
.
attachMethod
=
destinationInfo
.
attachMethod
;
this
.
dragState
.
dropDestination
=
destinationEle
;
}
else
{
ele
.
removeClass
(
this
.
validDropClass
);
this
.
dragState
.
attachMethod
=
''
;
this
.
dragState
.
dropDestination
=
null
;
}
},
onDragEnd
:
function
(
draggie
,
event
,
pointer
)
{
var
ele
=
$
(
draggie
.
element
);
var
destination
=
this
.
dragState
.
dropDestination
;
// Clear dragging state in preparation for the next event.
if
(
destination
)
{
destination
.
removeClass
(
this
.
droppableClasses
);
}
ele
.
removeClass
(
this
.
validDropClass
);
// If the drag succeeded, rearrange the DOM and send the result.
if
(
destination
&&
this
.
pointerInBounds
(
pointer
,
ele
))
{
// Make sure we don't drop into a collapsed element
if
(
this
.
dragState
.
parentList
)
{
this
.
expandElement
(
this
.
dragState
.
parentList
);
}
var
method
=
this
.
dragState
.
attachMethod
;
destination
[
method
](
ele
);
this
.
handleReorder
(
ele
);
}
// If the drag failed, send it back
else
{
$
(
'.was-dragging'
).
removeClass
(
'was-dragging'
);
ele
.
addClass
(
'was-dragging'
);
}
if
(
ele
.
hasClass
(
this
.
expandOnDropClass
))
{
this
.
expandElement
(
ele
);
ele
.
removeClass
(
this
.
expandOnDropClass
);
}
// Everything in its right place
ele
.
css
({
top
:
'auto'
,
left
:
'auto'
});
this
.
dragState
=
{};
},
pointerInBounds
:
function
(
pointer
,
ele
)
{
return
pointer
.
clientX
>=
ele
.
offset
().
left
&&
pointer
.
clientX
<
ele
.
offset
().
left
+
ele
.
width
();
},
expandElement
:
function
(
ele
)
{
ele
.
removeClass
(
'collapsed'
);
ele
.
find
(
'.expand-collapse'
).
first
().
removeClass
(
'expand'
).
addClass
(
'collapse'
);
},
/*
* Find all parent-child changes and save them.
*/
handleReorder
:
function
(
ele
)
{
var
parentSelector
=
ele
.
data
(
'parent-location-selector'
);
var
childrenSelector
=
ele
.
data
(
'child-selector'
);
var
newParentEle
=
ele
.
parents
(
parentSelector
).
first
();
var
newParentLocator
=
newParentEle
.
data
(
'locator'
);
var
oldParentLocator
=
ele
.
data
(
'parent'
);
// If the parent has changed, update the children of the old parent.
if
(
newParentLocator
!==
oldParentLocator
)
{
// Find the old parent element.
var
oldParentEle
=
$
(
parentSelector
).
filter
(
function
()
{
return
$
(
this
).
data
(
'locator'
)
===
oldParentLocator
;
});
this
.
saveItem
(
oldParentEle
,
childrenSelector
,
function
()
{
ele
.
data
(
'parent'
,
newParentLocator
);
});
}
var
saving
=
new
NotificationView
.
Mini
({
title
:
gettext
(
'Saving…'
)
});
saving
.
show
();
ele
.
addClass
(
'was-dropped'
);
// Timeout interval has to match what is in the CSS.
setTimeout
(
function
()
{
ele
.
removeClass
(
'was-dropped'
);
},
1000
);
this
.
saveItem
(
newParentEle
,
childrenSelector
,
function
()
{
saving
.
hide
();
});
},
/*
* Actually save the update to the server. Takes the element
* representing the parent item to save, a CSS selector to find
* its children, and a success callback.
*/
saveItem
:
function
(
ele
,
childrenSelector
,
success
)
{
// Find all current child IDs.
var
children
=
_
.
map
(
ele
.
find
(
childrenSelector
),
function
(
child
)
{
return
$
(
child
).
data
(
'locator'
);
}
);
$
.
ajax
({
url
:
ModuleUtils
.
getUpdateUrl
(
ele
.
data
(
'locator'
)),
type
:
'PUT'
,
dataType
:
'json'
,
contentType
:
'application/json'
,
data
:
JSON
.
stringify
({
children
:
children
}),
success
:
success
});
},
/*
* Make `type` draggable using `handleClass`, able to be dropped
* into `droppableClass`, and with parent type
* `parentLocationSelector`.
*/
makeDraggable
:
function
(
type
,
handleClass
,
droppableClass
,
parentLocationSelector
)
{
_
.
each
(
$
(
type
),
function
(
ele
)
{
// Remember data necessary to reconstruct the parent-child relationships
$
(
ele
).
data
(
'droppable-class'
,
droppableClass
);
$
(
ele
).
data
(
'parent-location-selector'
,
parentLocationSelector
);
$
(
ele
).
data
(
'child-selector'
,
type
);
var
draggable
=
new
Draggabilly
(
ele
,
{
handle
:
handleClass
,
containment
:
'.wrapper-dnd'
});
draggable
.
on
(
'dragStart'
,
_
.
bind
(
contentDragger
.
onDragStart
,
contentDragger
));
draggable
.
on
(
'dragMove'
,
_
.
bind
(
contentDragger
.
onDragMove
,
contentDragger
));
draggable
.
on
(
'dragEnd'
,
_
.
bind
(
contentDragger
.
onDragEnd
,
contentDragger
));
}
);
}
};
return
contentDragger
;
});
cms/static/js/views/overview.js
View file @
aade444f
define
([
"domReady"
,
"jquery"
,
"jquery.ui"
,
"underscore"
,
"gettext"
,
"js/views/feedback_notification"
,
"
draggabilly
"
,
define
([
"domReady"
,
"jquery"
,
"jquery.ui"
,
"underscore"
,
"gettext"
,
"js/views/feedback_notification"
,
"
js/utils/drag_and_drop
"
,
"js/utils/cancel_on_escape"
,
"js/utils/get_date"
,
"js/utils/module"
],
function
(
domReady
,
$
,
ui
,
_
,
gettext
,
NotificationView
,
Draggabilly
,
CancelOnEscape
,
function
(
domReady
,
$
,
ui
,
_
,
gettext
,
NotificationView
,
ContentDragger
,
CancelOnEscape
,
DateUtils
,
ModuleUtils
)
{
var
modalSelector
=
'.edit-section-publish-settings'
;
...
...
@@ -207,345 +207,7 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
$
(
this
).
parents
(
'li.courseware-subsection'
).
remove
();
};
var
overviewDragger
=
{
droppableClasses
:
'drop-target drop-target-prepend drop-target-before drop-target-after'
,
validDropClass
:
"valid-drop"
,
expandOnDropClass
:
"expand-on-drop"
,
/*
* Determine information about where to drop the currently dragged
* element. Returns the element to attach to and the method of
* attachment ('before', 'after', or 'prepend').
*/
findDestination
:
function
(
ele
,
yChange
)
{
var
eleY
=
ele
.
offset
().
top
;
var
eleYEnd
=
eleY
+
ele
.
height
();
var
containers
=
$
(
ele
.
data
(
'droppable-class'
));
for
(
var
i
=
0
;
i
<
containers
.
length
;
i
++
)
{
var
container
=
$
(
containers
[
i
]);
// Exclude the 'new unit' buttons, and make sure we don't
// prepend an element to itself
var
siblings
=
container
.
children
().
filter
(
function
()
{
return
$
(
this
).
data
(
'locator'
)
!==
undefined
&&
!
$
(
this
).
is
(
ele
);
});
// If the container is collapsed, check to see if the
// element is on top of its parent list -- don't check the
// position of the container
var
parentList
=
container
.
parents
(
ele
.
data
(
'parent-location-selector'
)).
first
();
if
(
parentList
.
hasClass
(
'collapsed'
))
{
var
parentListTop
=
parentList
.
offset
().
top
;
// To make it easier to drop subsections into collapsed sections (which have
// a lot of visual padding around them), allow a fudge factor around the
// parent element.
var
collapseFudge
=
10
;
if
(
Math
.
abs
(
eleY
-
parentListTop
)
<
collapseFudge
||
(
eleY
>
parentListTop
&&
eleYEnd
-
collapseFudge
<=
parentListTop
+
parentList
.
height
())
)
{
return
{
ele
:
container
,
attachMethod
:
'prepend'
,
parentList
:
parentList
};
}
}
// Otherwise, do check the container
else
{
// If the list is empty, we should prepend to it,
// unless both elements are at the same location --
// this prevents the user from being unable to expand
// a section
var
containerY
=
container
.
offset
().
top
;
if
(
siblings
.
length
==
0
&&
containerY
!=
eleY
&&
Math
.
abs
(
eleY
-
containerY
)
<
50
)
{
return
{
ele
:
container
,
attachMethod
:
'prepend'
};
}
// Otherwise the list is populated, and we should attach before/after a sibling
else
{
for
(
var
j
=
0
;
j
<
siblings
.
length
;
j
++
)
{
var
$sibling
=
$
(
siblings
[
j
]);
var
siblingY
=
$sibling
.
offset
().
top
;
var
siblingHeight
=
$sibling
.
height
();
var
siblingYEnd
=
siblingY
+
siblingHeight
;
// Facilitate dropping into the beginning or end of a list
// (coming from opposite direction) via a "fudge factor". Math.min is for Jasmine test.
var
fudge
=
Math
.
min
(
Math
.
ceil
(
siblingHeight
/
2
),
20
);
// Dragging to top or bottom of a list with only one element is tricky
// because the element being dragged may be the same size as the sibling.
if
(
siblings
.
length
==
1
)
{
// Element being dragged is within the drop target. Use the direction
// of the drag (yChange) to determine before or after.
if
(
eleY
+
fudge
>=
siblingY
&&
eleYEnd
-
fudge
<=
siblingYEnd
)
{
return
{
ele
:
$sibling
,
attachMethod
:
yChange
>
0
?
'after'
:
'before'
};
}
// Element being dragged is before the drop target.
else
if
(
Math
.
abs
(
eleYEnd
-
siblingY
)
<=
fudge
)
{
return
{
ele
:
$sibling
,
attachMethod
:
'before'
};
}
// Element being dragged is after the drop target.
else
if
(
Math
.
abs
(
eleY
-
siblingYEnd
)
<=
fudge
)
{
return
{
ele
:
$sibling
,
attachMethod
:
'after'
};
}
}
else
{
// Dragging up into end of list.
if
(
j
==
siblings
.
length
-
1
&&
yChange
<
0
&&
Math
.
abs
(
eleY
-
siblingYEnd
)
<=
fudge
)
{
return
{
ele
:
$sibling
,
attachMethod
:
'after'
};
}
// Dragging up or down into beginning of list.
else
if
(
j
==
0
&&
Math
.
abs
(
eleY
-
siblingY
)
<=
fudge
)
{
return
{
ele
:
$sibling
,
attachMethod
:
'before'
};
}
// Dragging down into end of list. Special handling required because
// the element being dragged may be taller then the element being dragged over
// (if eleY can never be >= siblingY, general case at the end does not work).
else
if
(
j
==
siblings
.
length
-
1
&&
yChange
>
0
&&
Math
.
abs
(
eleYEnd
-
siblingYEnd
)
<=
fudge
)
{
return
{
ele
:
$sibling
,
attachMethod
:
'after'
};
}
else
if
(
eleY
>=
siblingY
&&
eleY
<=
siblingYEnd
)
{
return
{
ele
:
$sibling
,
attachMethod
:
eleY
-
siblingY
<=
siblingHeight
/
2
?
'before'
:
'after'
};
}
}
}
}
}
}
// Failed drag
return
{
ele
:
null
,
attachMethod
:
''
}
},
// Information about the current drag.
dragState
:
{},
onDragStart
:
function
(
draggie
,
event
,
pointer
)
{
var
ele
=
$
(
draggie
.
element
);
this
.
dragState
=
{
// Which element will be dropped into/onto on success
dropDestination
:
null
,
// How we attach to the destination: 'before', 'after', 'prepend'
attachMethod
:
''
,
// If dragging to an empty section, the parent section
parentList
:
null
,
// The y location of the last dragMove event (to determine direction).
lastY
:
0
,
// The direction the drag is moving in (negative means up, positive down).
dragDirection
:
0
};
if
(
!
ele
.
hasClass
(
'collapsed'
))
{
ele
.
addClass
(
'collapsed'
);
ele
.
find
(
'.expand-collapse'
).
first
().
addClass
(
'expand'
).
removeClass
(
'collapse'
);
// onDragStart gets called again after the collapse, so we can't just store a variable in the dragState.
ele
.
addClass
(
this
.
expandOnDropClass
);
}
},
onDragMove
:
function
(
draggie
,
event
,
pointer
)
{
// Handle scrolling of the browser.
var
scrollAmount
=
0
;
var
dragBuffer
=
10
;
if
(
window
.
innerHeight
-
dragBuffer
<
pointer
.
clientY
)
{
scrollAmount
=
dragBuffer
;
}
else
if
(
dragBuffer
>
pointer
.
clientY
)
{
scrollAmount
=
-
(
dragBuffer
);
}
if
(
scrollAmount
!==
0
)
{
window
.
scrollBy
(
0
,
scrollAmount
);
return
;
}
var
yChange
=
draggie
.
dragPoint
.
y
-
this
.
dragState
.
lastY
;
if
(
yChange
!==
0
)
{
this
.
dragState
.
direction
=
yChange
;
}
this
.
dragState
.
lastY
=
draggie
.
dragPoint
.
y
;
var
ele
=
$
(
draggie
.
element
);
var
destinationInfo
=
this
.
findDestination
(
ele
,
this
.
dragState
.
direction
);
var
destinationEle
=
destinationInfo
.
ele
;
this
.
dragState
.
parentList
=
destinationInfo
.
parentList
;
// Clear out the old destination
if
(
this
.
dragState
.
dropDestination
)
{
this
.
dragState
.
dropDestination
.
removeClass
(
this
.
droppableClasses
);
}
// Mark the new destination
if
(
destinationEle
&&
this
.
pointerInBounds
(
pointer
,
ele
))
{
ele
.
addClass
(
this
.
validDropClass
);
destinationEle
.
addClass
(
'drop-target drop-target-'
+
destinationInfo
.
attachMethod
);
this
.
dragState
.
attachMethod
=
destinationInfo
.
attachMethod
;
this
.
dragState
.
dropDestination
=
destinationEle
;
}
else
{
ele
.
removeClass
(
this
.
validDropClass
);
this
.
dragState
.
attachMethod
=
''
;
this
.
dragState
.
dropDestination
=
null
;
}
},
onDragEnd
:
function
(
draggie
,
event
,
pointer
)
{
var
ele
=
$
(
draggie
.
element
);
var
destination
=
this
.
dragState
.
dropDestination
;
// Clear dragging state in preparation for the next event.
if
(
destination
)
{
destination
.
removeClass
(
this
.
droppableClasses
);
}
ele
.
removeClass
(
this
.
validDropClass
);
// If the drag succeeded, rearrange the DOM and send the result.
if
(
destination
&&
this
.
pointerInBounds
(
pointer
,
ele
))
{
// Make sure we don't drop into a collapsed element
if
(
this
.
dragState
.
parentList
)
{
this
.
expandElement
(
this
.
dragState
.
parentList
);
}
var
method
=
this
.
dragState
.
attachMethod
;
destination
[
method
](
ele
);
this
.
handleReorder
(
ele
);
}
// If the drag failed, send it back
else
{
$
(
'.was-dragging'
).
removeClass
(
'was-dragging'
);
ele
.
addClass
(
'was-dragging'
);
}
if
(
ele
.
hasClass
(
this
.
expandOnDropClass
))
{
this
.
expandElement
(
ele
);
ele
.
removeClass
(
this
.
expandOnDropClass
);
}
// Everything in its right place
ele
.
css
({
top
:
'auto'
,
left
:
'auto'
});
this
.
dragState
=
{};
},
pointerInBounds
:
function
(
pointer
,
ele
)
{
return
pointer
.
clientX
>=
ele
.
offset
().
left
&&
pointer
.
clientX
<
ele
.
offset
().
left
+
ele
.
width
();
},
expandElement
:
function
(
ele
)
{
ele
.
removeClass
(
'collapsed'
);
ele
.
find
(
'.expand-collapse'
).
first
().
removeClass
(
'expand'
).
addClass
(
'collapse'
);
},
/*
* Find all parent-child changes and save them.
*/
handleReorder
:
function
(
ele
)
{
var
parentSelector
=
ele
.
data
(
'parent-location-selector'
);
var
childrenSelector
=
ele
.
data
(
'child-selector'
);
var
newParentEle
=
ele
.
parents
(
parentSelector
).
first
();
var
newParentLocator
=
newParentEle
.
data
(
'locator'
);
var
oldParentLocator
=
ele
.
data
(
'parent'
);
// If the parent has changed, update the children of the old parent.
if
(
newParentLocator
!==
oldParentLocator
)
{
// Find the old parent element.
var
oldParentEle
=
$
(
parentSelector
).
filter
(
function
()
{
return
$
(
this
).
data
(
'locator'
)
===
oldParentLocator
;
});
this
.
saveItem
(
oldParentEle
,
childrenSelector
,
function
()
{
ele
.
data
(
'parent'
,
newParentLocator
);
});
}
var
saving
=
new
NotificationView
.
Mini
({
title
:
gettext
(
'Saving…'
)
});
saving
.
show
();
ele
.
addClass
(
'was-dropped'
);
// Timeout interval has to match what is in the CSS.
setTimeout
(
function
()
{
ele
.
removeClass
(
'was-dropped'
);
},
1000
);
this
.
saveItem
(
newParentEle
,
childrenSelector
,
function
()
{
saving
.
hide
();
});
},
/*
* Actually save the update to the server. Takes the element
* representing the parent item to save, a CSS selector to find
* its children, and a success callback.
*/
saveItem
:
function
(
ele
,
childrenSelector
,
success
)
{
// Find all current child IDs.
var
children
=
_
.
map
(
ele
.
find
(
childrenSelector
),
function
(
child
)
{
return
$
(
child
).
data
(
'locator'
);
}
);
$
.
ajax
({
url
:
ModuleUtils
.
getUpdateUrl
(
ele
.
data
(
'locator'
)),
type
:
'PUT'
,
dataType
:
'json'
,
contentType
:
'application/json'
,
data
:
JSON
.
stringify
({
children
:
children
}),
success
:
success
});
},
/*
* Make `type` draggable using `handleClass`, able to be dropped
* into `droppableClass`, and with parent type
* `parentLocationSelector`.
*/
makeDraggable
:
function
(
type
,
handleClass
,
droppableClass
,
parentLocationSelector
)
{
_
.
each
(
$
(
type
),
function
(
ele
)
{
// Remember data necessary to reconstruct the parent-child relationships
$
(
ele
).
data
(
'droppable-class'
,
droppableClass
);
$
(
ele
).
data
(
'parent-location-selector'
,
parentLocationSelector
);
$
(
ele
).
data
(
'child-selector'
,
type
);
var
draggable
=
new
Draggabilly
(
ele
,
{
handle
:
handleClass
,
containment
:
'.wrapper-dnd'
});
draggable
.
on
(
'dragStart'
,
_
.
bind
(
overviewDragger
.
onDragStart
,
overviewDragger
));
draggable
.
on
(
'dragMove'
,
_
.
bind
(
overviewDragger
.
onDragMove
,
overviewDragger
));
draggable
.
on
(
'dragEnd'
,
_
.
bind
(
overviewDragger
.
onDragEnd
,
overviewDragger
));
}
);
}
};
domReady
(
function
()
{
// toggling overview section details
...
...
@@ -566,21 +228,21 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
$
(
'.new-subsection-item'
).
bind
(
'click'
,
addNewSubsection
);
// Section
overview
Dragger
.
makeDraggable
(
Content
Dragger
.
makeDraggable
(
'.courseware-section'
,
'.section-drag-handle'
,
'.courseware-overview'
,
'article.courseware-overview'
);
// Subsection
overview
Dragger
.
makeDraggable
(
Content
Dragger
.
makeDraggable
(
'.id-holder'
,
'.subsection-drag-handle'
,
'.subsection-list > ol'
,
'.courseware-section'
);
// Unit
overview
Dragger
.
makeDraggable
(
Content
Dragger
.
makeDraggable
(
'.unit'
,
'.unit-drag-handle'
,
'ol.sortable-unit-list'
,
...
...
@@ -589,7 +251,6 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
});
return
{
overviewDragger
:
overviewDragger
,
saveSetSectionScheduleDate
:
saveSetSectionScheduleDate
};
});
cms/templates/js/mock/mock-outline.underscore
0 → 100644
View file @
aade444f
<section>
<ol class="sortable-subsection-list">
<li class="courseware-subsection is-collapsible id-holder is-draggable" id="subsection-0" data-locator="subsection-0-id" style="margin:5px">
<ol class="sortable-unit-list" id="subsection-list-0">
<li class="courseware-unit unit is-draggable" id="unit-0" data-parent="subsection-0-id" data-locator="zero-unit-id"></li>
</ol>
</li>
<li class="courseware-subsection is-collapsible id-holder is-draggable" id="subsection-1" data-locator="subsection-1-id" style="margin:5px">
<ol class="sortable-unit-list" id="subsection-list-1">
<li class="courseware-unit unit is-draggable" id="unit-1" data-parent="subsection-1-id" data-locator="first-unit-id"></li>
<li class="courseware-unit unit is-draggable" id="unit-2" data-parent="subsection-1-id" data-locator="second-unit-id"></li>
<li class="courseware-unit unit is-draggable" id="unit-3" data-parent="subsection-1-id" data-locator="third-unit-id"></li>
</ol>
</li>
<li class="courseware-subsection is-collapsible id-holder is-draggable" id="subsection-2" data-locator="subsection-2-id" style="margin:5px">
<ol class="sortable-unit-list" id="subsection-list-2">
<li class="courseware-unit unit is-draggable" id="unit-4" data-parent="subsection-2" data-locator="fourth-unit-id"></li>
</ol>
</li>
<li class="courseware-subsection is-collapsible id-holder is-draggable" id="subsection-3" data-locator="subsection-3-id" style="margin:5px">
<ol class="sortable-unit-list" id="subsection-list-3"></ol>
</li>
<li class="courseware-subsection is-collapsible id-holder is-draggable" id="subsection-4" data-locator="subsection-4-id" style="margin:5px">
<ol class="sortable-unit-list" id="subsection-list-4">
<li class="courseware-unit unit is-draggable" id="unit-5" data-parent="subsection-4-id" data-locator="fifth-unit-id"></li>
</ol>
</li>
</ol>
</section>
\ No newline at end of file
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment