diff --git a/cms/static/coffee/spec/main.coffee b/cms/static/coffee/spec/main.coffee index 4980e86c65..11f937a2b4 100644 --- a/cms/static/coffee/spec/main.coffee +++ b/cms/static/coffee/spec/main.coffee @@ -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 diff --git a/cms/static/coffee/spec/views/overview_spec.coffee b/cms/static/coffee/spec/views/overview_spec.coffee index c29d31f414..d89c64c145 100644 --- a/cms/static/coffee/spec/views/overview_spec.coffee +++ b/cms/static/coffee/spec/views/overview_spec.coffee @@ -54,38 +54,6 @@ define ["js/views/overview", "js/views/feedback_notification", "js/spec/create_s """ - appendSetFixtures """ -
-
    -
  1. -
      -
    1. -
    -
  2. -
  3. -
      -
    1. -
    2. -
    3. -
    -
  4. -
  5. -
      -
    1. -
    -
  6. -
  7. -
      -
    1. -
    2. -
        -
      1. -
      -
    3. -
    -
    - """ - 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') diff --git a/cms/static/js/spec/utils/drag_and_drop_spec.js b/cms/static/js/spec/utils/drag_and_drop_spec.js new file mode 100644 index 0000000000..74558ca22f --- /dev/null +++ b/cms/static/js/spec/utils/drag_and_drop_spec.js @@ -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'); + }); + }); + }); + }); + diff --git a/cms/static/js/utils/drag_and_drop.js b/cms/static/js/utils/drag_and_drop.js new file mode 100644 index 0000000000..2479ec47ba --- /dev/null +++ b/cms/static/js/utils/drag_and_drop.js @@ -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; + }); diff --git a/cms/static/js/views/overview.js b/cms/static/js/views/overview.js index 1a4daa826d..6fb18a39f3 100644 --- a/cms/static/js/views/overview.js +++ b/cms/static/js/views/overview.js @@ -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 }; }); diff --git a/cms/templates/js/mock/mock-outline.underscore b/cms/templates/js/mock/mock-outline.underscore new file mode 100644 index 0000000000..2991935827 --- /dev/null +++ b/cms/templates/js/mock/mock-outline.underscore @@ -0,0 +1,29 @@ +
    +
      +
    1. +
        +
      1. +
      +
    2. +
    3. +
        +
      1. +
      2. +
      3. +
      +
    4. +
    5. +
        +
      1. +
      +
    6. +
    7. +
        +
      1. +
      2. +
          +
        1. +
        +
      3. +
      +
      \ No newline at end of file