diff --git a/cms/static/coffee/spec/views/overview_spec.coffee b/cms/static/coffee/spec/views/overview_spec.coffee index d0e6bb77d3..ee7a41c709 100644 --- a/cms/static/coffee/spec/views/overview_spec.coffee +++ b/cms/static/coffee/spec/views/overview_spec.coffee @@ -1,7 +1,7 @@ describe "Course Overview", -> beforeEach -> - _.each ["/static/js/vendor/date.js", "/static/js/vendor/timepicker/jquery.timepicker.js", "/jsi18n/"], (path) -> + _.each ["/static/js/vendor/date.js", "/static/js/vendor/timepicker/jquery.timepicker.js", "/jsi18n/", "/static/js/vendor/draggabilly.pkgd.js"], (path) -> appendSetFixtures """ """ @@ -45,15 +45,23 @@ describe "Course Overview", -> """ - # appendSetFixtures """ - #
- #
    - #
  1. - #
  2. - #
  3. - #
- #
- # """ + appendSetFixtures """ +
+
    +
  1. +
  2. +
  3. +
+
+ +
    +
  1. +
+ +
+
    +
    + """#" spyOn(window, 'saveSetSectionScheduleDate').andCallThrough() # Have to do this here, as it normally gets bound in document.ready() @@ -68,6 +76,13 @@ describe "Course Overview", -> requests = @requests = [] @xhr.onCreate = (req) -> requests.push(req) + CMS.Views.Draggabilly.makeDraggable( + '.unit', + '.unit-drag-handle', + 'ol.sortable-unit-list', + 'li.branch, article.subsection-body' + ) + afterEach -> delete window.analytics delete window.course_location_analytics @@ -100,3 +115,125 @@ describe "Course Overview", -> $('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 = CMS.Views.Draggabilly.findDestination($ele) + expect(destination.ele).toBe($('#unit-2')) + expect(destination.attachMethod).toBe('before') + + it "can drag and drop across section boundaries", -> + $ele = $('#unit-1') + $ele.offset( + top: $('#unit-4').offset().top + 10 + left: $ele.offset().left + ) + destination = CMS.Views.Draggabilly.findDestination($ele) + expect(destination.ele).toBe($('#unit-4')) + expect(destination.attachMethod).toBe('after') + + it "can drag into an empty list", -> + $ele = $('#unit-1') + $ele.offset( + top: $('#list-3').offset().top + 10 + left: $ele.offset().left + ) + destination = CMS.Views.Draggabilly.findDestination($ele) + expect(destination.ele).toBe($('#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 = CMS.Views.Draggabilly.findDestination($ele) + expect(destination).toEqual( + ele: null + attachMethod: "" + ) + + describe "onDragStart", -> + it "sets the dragState to its default values", -> + expect(CMS.Views.Draggabilly.dragState).toEqual({}) + # Call with some dummy data + CMS.Views.Draggabilly.onDragStart( + {element: $('#unit-1')}, + null, + null + ) + expect(CMS.Views.Draggabilly.dragState).toEqual( + offset: $('#unit-1').offset() + dropDestination: null, + expandTimer: null, + toExpand: null + ) + + describe "onDragMove", -> + it "clears the expand timer state", -> + timerSpy = spyOn(window, 'clearTimeout').andCallThrough() + $ele = $('#unit-1') + $ele.offset( + top: $ele.offset().top + 10 + left: $ele.offset().left + ) + CMS.Views.Draggabilly.onDragMove( + {element: $ele}, + null, + null + ) + expect(timerSpy).toHaveBeenCalled() + timerSpy.reset() + + it "adds the correct CSS class to the drop destination", -> + $ele = $('#unit-1') + $ele.offset( + top: $ele.offset().top + 10, left: $ele.offset().left + ) + CMS.Views.Draggabilly.onDragMove( + {element: $ele}, + '', + '' + ) + expect($('#unit-2')).toHaveClass('drop-target drop-target-before') + + describe "onDragEnd", -> + beforeEach -> + @reorderSpy = spyOn(CMS.Views.Draggabilly, 'handleReorder') + + afterEach -> + @reorderSpy.reset() + + it "calls handleReorder on a successful drag", -> + $('#unit-1').offset( + top: $('#unit-1').offset().top + 10 + left: $('#unit-1').offset().left + ) + CMS.Views.Draggabilly.onDragEnd( + {element: $('#unit-1')}, + null, + {x: $('#unit-1').offset().left} + ) + expect(@reorderSpy).toHaveBeenCalled() + + it "clears out the drag state", -> + CMS.Views.Draggabilly.onDragEnd( + {element: $('#unit-1')}, + null, + null + ) + expect(CMS.Views.Draggabilly.dragState).toEqual({}) + + it "sets the element to the correct position", -> + CMS.Views.Draggabilly.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')) diff --git a/cms/static/js/views/overview.js b/cms/static/js/views/overview.js index 7bfaa8dbea..3c54da9ea3 100644 --- a/cms/static/js/views/overview.js +++ b/cms/static/js/views/overview.js @@ -1,13 +1,12 @@ -$(document).ready(function() { - - var droppableClasses = 'drop-target drop-target-prepend drop-target-before drop-target-after'; +CMS.Views.Draggabilly = { + droppableClasses: 'drop-target drop-target-prepend drop-target-before drop-target-after', /* * 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'). */ - var findDestination = function(ele) { + findDestination: function(ele) { var eleY = ele.offset().top; var containers = $(ele.data('droppable-class')); @@ -55,7 +54,7 @@ $(document).ready(function() { // element is actually on top of the sibling, // rather than next to it. This prevents // saving when expanding/collapsing a list. - if(Math.abs(eleY - siblingY) < $sibling.height() - 1) { + if(Math.abs(eleY - siblingY) < ele.height() - 1) { return { ele: $sibling, attachMethod: siblingY > eleY ? 'before' : 'after' @@ -69,15 +68,15 @@ $(document).ready(function() { return { ele: null, attachMethod: '' - }; - }; + } + }, // Information about the current drag. - var dragState = {}; + dragState: {}, - var onDragStart = function(draggie, event, pointer) { + onDragStart: function(draggie, event, pointer) { var ele = $(draggie.element); - dragState = { + this.dragState = { // Where we started, in case of a failed drag offset: ele.offset(), // Which element will be dropped into/onto on success @@ -87,42 +86,42 @@ $(document).ready(function() { // The list which will be expanded on hover toExpand: null }; - }; + }, - var onDragMove = function(draggie, event, pointer) { + onDragMove: function(draggie, event, pointer) { var ele = $(draggie.element); - var destinationInfo = findDestination(ele); + var destinationInfo = this.findDestination(ele); var destinationEle = destinationInfo.ele; var parentList = destinationInfo.parentList; // Clear the timer if we're not hovering over any element if(!parentList) { - clearTimeout(dragState.expandTimer); + clearTimeout(this.dragState.expandTimer); } // If we're hovering over a new element, clear the timer and // set a new one - else if(!dragState.toExpand || parentList[0] !== dragState.toExpand[0]) { - clearTimeout(dragState.expandTimer); - dragState.expandTimer = setTimeout(function() { + else if(!this.dragState.toExpand || parentList[0] !== this.dragState.toExpand[0]) { + clearTimeout(this.dragState.expandTimer); + this.dragState.expandTimer = setTimeout(function() { parentList.removeClass('collapsed'); parentList.find('.expand-collapse-icon').removeClass('expand').addClass('collapse'); }, 400); - dragState.toExpand = parentList; + this.dragState.toExpand = parentList; } // Clear out the old destination - if(dragState.dropDestination) { - dragState.dropDestination.removeClass(droppableClasses); + if(this.dragState.dropDestination) { + this.dragState.dropDestination.removeClass(this.droppableClasses); } // Mark the new destination if(destinationEle) { destinationEle.addClass('drop-target drop-target-' + destinationInfo.attachMethod); - dragState.dropDestination = destinationEle; + this.dragState.dropDestination = destinationEle; } - }; + }, - var onDragEnd = function(draggie, event, pointer) { + onDragEnd: function(draggie, event, pointer) { var ele = $(draggie.element); - var destinationInfo = findDestination(ele); + var destinationInfo = this.findDestination(ele); var destination = destinationInfo.ele; // If the drag succeeded, rearrange the DOM and send the result. @@ -134,7 +133,7 @@ $(document).ready(function() { } var method = destinationInfo.attachMethod; destination[method](ele); - handleReorder(ele); + this.handleReorder(ele); } // Everything in its right place @@ -144,17 +143,17 @@ $(document).ready(function() { }); // Clear dragging state in preparation for the next event. - if(dragState.dropDestination) { - dragState.dropDestination.removeClass(droppableClasses); + if(this.dragState.dropDestination) { + this.dragState.dropDestination.removeClass(this.droppableClasses); } - clearTimeout(dragState.expandTimer); - dragState = {}; - }; + clearTimeout(this.dragState.expandTimer); + this.dragState = {}; + }, /* * Find all parent-child changes and save them. */ - var handleReorder = function(ele) { + handleReorder: function(ele) { var parentSelector = ele.data('parent-location-selector'); var childrenSelector = ele.data('child-selector'); var newParentEle = ele.parents(parentSelector).first(); @@ -166,7 +165,7 @@ $(document).ready(function() { var oldParentEle = $(parentSelector).filter(function() { return $(this).data('id') === oldParentID; }); - saveItem(oldParentEle, childrenSelector, function() { + this.saveItem(oldParentEle, childrenSelector, function() { ele.data('parent-id', newParentID); }); } @@ -174,17 +173,17 @@ $(document).ready(function() { title: gettext('Saving…') }); saving.show(); - saveItem(newParentEle, childrenSelector, function() { + 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. */ - var saveItem = function(ele, childrenSelector, success) { + saveItem: function(ele, childrenSelector, success) { // Find all current child IDs. var children = _.map( ele.find(childrenSelector), @@ -203,14 +202,14 @@ $(document).ready(function() { }), success: success }); - }; + }, /* * Make `type` draggable using `handleClass`, able to be dropped * into `droppableClass`, and with parent type * `parentLocationSelector`. */ - var makeDraggable = function(type, handleClass, droppableClass, parentLocationSelector) { + makeDraggable: function(type, handleClass, droppableClass, parentLocationSelector) { _.each( $(type), function(ele) { @@ -222,29 +221,31 @@ $(document).ready(function() { handle: handleClass, axis: 'y' }); - draggable.on('dragStart', onDragStart); - draggable.on('dragMove', onDragMove); - draggable.on('dragEnd', onDragEnd); + draggable.on('dragStart', _.bind(CMS.Views.Draggabilly.onDragStart, CMS.Views.Draggabilly)); + draggable.on('dragMove', _.bind(CMS.Views.Draggabilly.onDragMove, CMS.Views.Draggabilly)); + draggable.on('dragEnd', _.bind(CMS.Views.Draggabilly.onDragEnd, CMS.Views.Draggabilly)); } ); - }; + } +}; +$(document).ready(function() { // Section - makeDraggable( + CMS.Views.Draggabilly.makeDraggable( '.courseware-section', '.section-drag-handle', '.courseware-overview', 'article.courseware-overview' ); // Subsection - makeDraggable( + CMS.Views.Draggabilly.makeDraggable( '.id-holder', '.subsection-drag-handle', '.subsection-list > ol', '.courseware-section' ); // Unit - makeDraggable( + CMS.Views.Draggabilly.makeDraggable( '.unit', '.unit-drag-handle', 'ol.sortable-unit-list',