Move overview drag and drop code to a utility class.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
313
cms/static/js/spec/utils/drag_and_drop_spec.js
Normal file
313
cms/static/js/spec/utils/drag_and_drop_spec.js
Normal file
@@ -0,0 +1,313 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
346
cms/static/js/utils/drag_and_drop.js
Normal file
346
cms/static/js/utils/drag_and_drop.js
Normal file
@@ -0,0 +1,346 @@
|
||||
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;
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
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
|
||||
overviewDragger.makeDraggable(
|
||||
ContentDragger.makeDraggable(
|
||||
'.courseware-section',
|
||||
'.section-drag-handle',
|
||||
'.courseware-overview',
|
||||
'article.courseware-overview'
|
||||
);
|
||||
// Subsection
|
||||
overviewDragger.makeDraggable(
|
||||
ContentDragger.makeDraggable(
|
||||
'.id-holder',
|
||||
'.subsection-drag-handle',
|
||||
'.subsection-list > ol',
|
||||
'.courseware-section'
|
||||
);
|
||||
// Unit
|
||||
overviewDragger.makeDraggable(
|
||||
ContentDragger.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
|
||||
};
|
||||
});
|
||||
|
||||
29
cms/templates/js/mock/mock-outline.underscore
Normal file
29
cms/templates/js/mock/mock-outline.underscore
Normal file
@@ -0,0 +1,29 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user