Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions cms/djangoapps/contentstore/features/course-outline.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,14 +131,3 @@ def all_sections_are_collapsed_or_expanded(step, text):
def change_grading_status(step):
world.css_find('a.menu-toggle').click()
world.css_find('.menu li').first.click()


@step(u'I reorder subsections')
def reorder_subsections(_step):
draggable_css = '.subsection-drag-handle'
ele = world.css_find(draggable_css).first
ele.action_chains.drag_and_drop_by_offset(
ele._element,
0,
25
).perform()
48 changes: 37 additions & 11 deletions cms/static/js/spec/utils/drag_and_drop_spec.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,32 @@
define(["js/utils/drag_and_drop", "js/views/feedback_notification", "js/spec_helpers/create_sinon", "jquery"],
function (ContentDragger, Notification, create_sinon, $) {
define(["js/utils/drag_and_drop", "js/views/feedback_notification", "js/spec_helpers/create_sinon", "jquery", "underscore"],
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');
_.each(
$('.unit'),
function (element) {
ContentDragger.makeDraggable(element, {
type: '.unit',
handleClass: '.unit-drag-handle',
droppableClass: 'ol.sortable-unit-list',
parentLocationSelector: 'li.courseware-subsection',
refresh: jasmine.createSpy('Spy on Unit')
});
}
);
_.each(
$('.courseware-subsection'),
function (element) {
ContentDragger.makeDraggable(element, {
type: '.courseware-subsection',
handleClass: '.subsection-drag-handle',
droppableClass: '.sortable-subsection-list',
parentLocationSelector: 'section',
refresh: jasmine.createSpy('Spy on Subsection')
});
}
);
});

describe("findDestination", function () {
Expand Down Expand Up @@ -115,7 +137,7 @@ define(["js/utils/drag_and_drop", "js/views/feedback_notification", "js/spec_hel
});
it("can drag into a collapsed list", function () {
var $ele, destination;
$('#subsection-2').addClass('collapsed');
$('#subsection-2').addClass('is-collapsed');
$ele = $('#unit-2');
$ele.offset({
top: $('#subsection-2').offset().top + 3,
Expand All @@ -142,11 +164,11 @@ define(["js/utils/drag_and_drop", "js/views/feedback_notification", "js/spec_hel
});
});
it("collapses expanded elements", function () {
expect($('#subsection-1')).not.toHaveClass('collapsed');
expect($('#subsection-1')).not.toHaveClass('is-collapsed');
ContentDragger.onDragStart({
element: $('#subsection-1')
}, null, null);
expect($('#subsection-1')).toHaveClass('collapsed');
expect($('#subsection-1')).toHaveClass('is-collapsed');
expect($('#subsection-1')).toHaveClass('expand-on-drop');
});
});
Expand Down Expand Up @@ -246,16 +268,16 @@ define(["js/utils/drag_and_drop", "js/views/feedback_notification", "js/spec_hel
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('is-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('is-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');
$('#subsection-2').addClass('is-collapsed');
ContentDragger.dragState.dropDestination = $('#list-2');
ContentDragger.dragState.attachMethod = "prepend";
ContentDragger.dragState.parentList = $('#subsection-2');
Expand All @@ -264,7 +286,7 @@ define(["js/utils/drag_and_drop", "js/views/feedback_notification", "js/spec_hel
}, null, {
clientX: $('#unit-1').offset().left
});
expect($('#subsection-2')).not.toHaveClass('collapsed');
expect($('#subsection-2')).not.toHaveClass('is-collapsed');
});
});
describe("AJAX", function () {
Expand Down Expand Up @@ -306,6 +328,10 @@ define(["js/utils/drag_and_drop", "js/views/feedback_notification", "js/spec_hel
expect(this.savingSpies.hide).toHaveBeenCalled();
this.clock.tick(1001);
expect($('#unit-1')).not.toHaveClass('was-dropped');
// source
expect($('#subsection-1').data('refresh')).toHaveBeenCalled();
// target
expect($('#subsection-2').data('refresh')).toHaveBeenCalled();
});
});
});
Expand Down
6 changes: 3 additions & 3 deletions cms/static/js/spec/views/pages/course_outline_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
create_sinon.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-course');
create_sinon.respondWithJson(requests, mockSingleSectionCourseJSON);
expect(outlinePage.$('.no-content')).not.toExist();
expect(outlinePage.$('.list-sections li').data('locator')).toEqual('mock-section');
expect(outlinePage.$('.list-sections li.outline-section').data('locator')).toEqual('mock-section');
});

it('can add a second section', function() {
Expand Down Expand Up @@ -237,7 +237,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
create_sinon.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-course');
create_sinon.respondWithJson(requests, mockSingleSectionCourseJSON);
expect(outlinePage.$('.no-content')).not.toExist();
expect(outlinePage.$('.list-sections li').data('locator')).toEqual('mock-section');
expect(outlinePage.$('.list-sections li.outline-section').data('locator')).toEqual('mock-section');
});

it('remains empty if an add fails', function() {
Expand Down Expand Up @@ -303,7 +303,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
requestCount = requests.length;
create_sinon.respondWithError(requests);
expect(requests.length).toBe(requestCount); // No additional requests should be made
expect(outlinePage.$('.list-sections li').data('locator')).toEqual('mock-section');
expect(outlinePage.$('.list-sections li.outline-section').data('locator')).toEqual('mock-section');
});

it('can add a subsection', function() {
Expand Down
106 changes: 66 additions & 40 deletions cms/static/js/utils/drag_and_drop.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ define(["jquery", "jquery.ui", "underscore", "gettext", "js/views/feedback_notif
droppableClasses: 'drop-target drop-target-prepend drop-target-before drop-target-after',
validDropClass: "valid-drop",
expandOnDropClass: "expand-on-drop",
collapsedClass: "is-collapsed",

/*
* Determine information about where to drop the currently dragged
Expand All @@ -14,7 +15,7 @@ define(["jquery", "jquery.ui", "underscore", "gettext", "js/views/feedback_notif
*/
findDestination: function (ele, yChange) {
var eleY = ele.offset().top;
var eleYEnd = eleY + ele.height();
var eleYEnd = eleY + ele.outerHeight();
var containers = $(ele.data('droppable-class'));

for (var i = 0; i < containers.length; i++) {
Expand All @@ -28,15 +29,15 @@ define(["jquery", "jquery.ui", "underscore", "gettext", "js/views/feedback_notif
// 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')) {
if (parentList.hasClass(this.collapsedClass)) {
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())
eleYEnd - collapseFudge <= parentListTop + parentList.outerHeight())
) {
return {
ele: container,
Expand Down Expand Up @@ -65,12 +66,12 @@ define(["jquery", "jquery.ui", "underscore", "gettext", "js/views/feedback_notif
for (var j = 0; j < siblings.length; j++) {
var $sibling = $(siblings[j]);
var siblingY = $sibling.offset().top;
var siblingHeight = $sibling.height();
var siblingHeight = $sibling.outerHeight();
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);
var fudge = Math.min(Math.ceil(siblingHeight / 2), 35);

// 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.
Expand Down Expand Up @@ -158,12 +159,16 @@ define(["jquery", "jquery.ui", "underscore", "gettext", "js/views/feedback_notif
// The direction the drag is moving in (negative means up, positive down).
dragDirection: 0
};
if (!ele.hasClass('collapsed')) {
ele.addClass('collapsed');
if (!ele.hasClass(this.collapsedClass)) {
ele.addClass(this.collapsedClass);
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);
}

// We should remove this class name before start dragging to
// avoid performance issues.
ele.removeClass('was-dragging');
},

onDragMove: function (draggie, event, pointer) {
Expand Down Expand Up @@ -251,41 +256,46 @@ define(["jquery", "jquery.ui", "underscore", "gettext", "js/views/feedback_notif
},

pointerInBounds: function (pointer, ele) {
return pointer.clientX >= ele.offset().left && pointer.clientX < ele.offset().left + ele.width();
return pointer.clientX >= ele.offset().left && pointer.clientX < ele.offset().left + ele.outerWidth();
},

expandElement: function (ele) {
ele.removeClass('collapsed');
ele.removeClass(this.collapsedClass);
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');
handleReorder: function (element) {
var parentSelector = element.data('parent-location-selector'),
childrenSelector = element.data('child-selector'),
newParentEle = element.parents(parentSelector).first(),
newParentLocator = newParentEle.data('locator'),
oldParentLocator = element.data('parent'),
oldParentEle, saving;
// 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 () {
oldParentEle = $(parentSelector).filter(function () {
return $(this).data('locator') === oldParentLocator;
});
this.saveItem(oldParentEle, childrenSelector, function () {
ele.data('parent', newParentLocator);
element.data('parent', newParentLocator);
_.each([oldParentEle, newParentEle], function (element) {
var refresh = element.data('refresh');
if (_.isFunction(refresh)) { refresh(); }
});
});
}
var saving = new NotificationView.Mini({
saving = new NotificationView.Mini({
title: gettext('Saving&hellip;')
});
saving.show();
ele.addClass('was-dropped');
element.addClass('was-dropped');
// Timeout interval has to match what is in the CSS.
setTimeout(function () {
ele.removeClass('was-dropped');
element.removeClass('was-dropped');
}, 1000);
this.saveItem(newParentEle, childrenSelector, function () {
saving.hide();
Expand Down Expand Up @@ -318,27 +328,43 @@ define(["jquery", "jquery.ui", "underscore", "gettext", "js/views/feedback_notif
},

/*
* Make `type` draggable using `handleClass`, able to be dropped
* into `droppableClass`, and with parent type
* `parentLocationSelector`.
* Make DOM element with class `type` draggable using `handleClass`, able to be dropped
* into `droppableClass`, and with parent type `parentLocationSelector`.
* @param {DOM element, jQuery element} element
* @param {Object} options The list of options. Possible options:
* `type` - class name of the element.
* `handleClass` - specifies on what element the drag interaction starts.
* `droppableClass` - specifies on what elements draggable element can be dropped.
* `parentLocationSelector` - class name of a parent element with data-locator.
* `refresh` - method that will be called after dragging to refresh
* views of the target and source xblocks.
*/
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));
}
);
makeDraggable: function (element, options) {
var draggable;
options = _.defaults({
type: null,
handleClass: null,
droppableClass: null,
parentLocationSelector: null,
refresh: null
}, options);

if ($(element).data('droppable-class') !== options.droppableClass) {
$(element).data({
'droppable-class': options.droppableClass,
'parent-location-selector': options.parentLocationSelector,
'child-selector': options.type,
'refresh': options.refresh
});

draggable = new Draggabilly(element, {
handle: options.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));
}
}
};

Expand Down
Loading