From 3a2c57524fb1fd6c910cfb051273ac097dcf168f Mon Sep 17 00:00:00 2001 From: Don Mitchell Date: Tue, 15 Jan 2013 10:35:20 -0500 Subject: [PATCH 01/13] Basic dnd for units --- cms/static/js/base.js | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 4137690395..59b5a3812c 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -59,7 +59,9 @@ $(document).ready(function() { $('.sortable-unit-list').sortable({ axis: 'y', handle: '.drag-handle', - update: onUnitReordered + update: onUnitReordered, + connectWith: '.sortable-unit-list', + revert: true }); // expand/collapse methods for optional date setters @@ -242,7 +244,7 @@ function removePolicyMetadata(e) { // This method only changes the ordering of the child objects in a subsection -function onUnitReordered() { +function onUnitReordered(event, ui) { var subsection_id = $(this).data('subsection-id'); var _els = $(this).children('li:.leaf'); @@ -256,6 +258,23 @@ function onUnitReordered() { contentType: "application/json", data:JSON.stringify({ 'id' : subsection_id, 'children' : children}) }); + + // remove from old container + if (ui.sender && subsection_id !== ui.sender.data('subsection-id')) { + + var _els = ui.sender.children('li:.leaf'); + var children = _els.map(function(idx, el) { return $(el).data('id'); }).get(); + + // call into server to commit the new order + $.ajax({ + url: "/save_item", + type: "POST", + dataType: "json", + contentType: "application/json", + data:JSON.stringify({ 'id' : ui.sender.data('subsection-id'), 'children' : children}) + }); + + } } function onSubsectionReordered() { From 244584ce6c213f37381ec92512491310beb196d1 Mon Sep 17 00:00:00 2001 From: Don Mitchell Date: Tue, 15 Jan 2013 11:33:11 -0500 Subject: [PATCH 02/13] DnD for subsections and units (no auto open) --- cms/static/js/base.js | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 59b5a3812c..8ee3ce806c 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -93,14 +93,17 @@ $(document).ready(function() { $('.subsection-list > ol').sortable({ axis: 'y', handle: '.section-item .drag-handle', - update: onSubsectionReordered + update: onSubsectionReordered, + connectWith: '.subsection-list > ol', + revert: true }); // Section reordering $('.courseware-overview').sortable({ axis: 'y', handle: 'header .drag-handle', - update: onSectionReordered + update: onSectionReordered, + revert: true }); $('.new-course-button').bind('click', addNewCourse); @@ -277,7 +280,7 @@ function onUnitReordered(event, ui) { } } -function onSubsectionReordered() { +function onSubsectionReordered(event, ui) { var section_id = $(this).data('section-id'); var _els = $(this).children('li:.branch'); @@ -291,6 +294,22 @@ function onSubsectionReordered() { contentType: "application/json", data:JSON.stringify({ 'id' : section_id, 'children' : children}) }); + + // remove from old container + if (ui.sender && section_id !== ui.sender.data('section-id')) { + var _els = ui.sender.children('li:.branch'); + var children = _els.map(function(idx, el) { return $(el).data('id'); }).get(); + + // call into server to commit the new order + $.ajax({ + url: "/save_item", + type: "POST", + dataType: "json", + contentType: "application/json", + data:JSON.stringify({ 'id' : ui.sender.data('section-id'), 'children' : children}) + }); + + } } function onSectionReordered() { From 480aa80db356acb4ff01fe2126d43832e6509039 Mon Sep 17 00:00:00 2001 From: Don Mitchell Date: Tue, 15 Jan 2013 14:32:39 -0500 Subject: [PATCH 03/13] Cache --- cms/static/js/base.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 4137690395..29d8e69f62 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -55,6 +55,7 @@ $(document).ready(function() { $("#start_date, #start_time, #due_date, #due_time").bind('change', autosaveInput); $('.sync-date, .remove-date').bind('click', autosaveInput); + var cachedHesitation = new CMS.HesitateEvent( expandSection, $('.branch .collapsed'), 'mouseLeave'); // making the unit list sortable $('.sortable-unit-list').sortable({ axis: 'y', @@ -240,6 +241,12 @@ function removePolicyMetadata(e) { saveSubsection() } +function expandSection(event) { + $(event.currentTarget).removeClass('collapsed'); + // TODO remove the hesitate trigger on this ele + // $(event.currentTarget).off('mouseEnter', expandSection); + $(event.currentTarget).find('.expand-collapse-icon').removeClass('expand').addClass('collapse'); +} // This method only changes the ordering of the child objects in a subsection function onUnitReordered() { From db4455e6e1b1712d811f54d8069f7cff9089da61 Mon Sep 17 00:00:00 2001 From: Don Mitchell Date: Wed, 16 Jan 2013 09:59:51 -0500 Subject: [PATCH 04/13] Cache --- cms/envs/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/envs/common.py b/cms/envs/common.py index c047d689ce..08e38320ab 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -229,7 +229,7 @@ PIPELINE_JS = { 'source_filenames': sorted( rooted_glob(COMMON_ROOT / 'static/', 'coffee/src/**/*.coffee') + rooted_glob(PROJECT_ROOT / 'static/', 'coffee/src/**/*.coffee') - ) + ['js/base.js'], + ) + ['js/hesitate.js', 'js/base.js'], 'output_filename': 'js/cms-application.js', }, 'module-js': { From ac09e18b9e0d73eb5e10479c8ccee879597a3c26 Mon Sep 17 00:00:00 2001 From: Don Mitchell Date: Thu, 17 Jan 2013 15:09:23 -0500 Subject: [PATCH 05/13] Now traps out of bounds as implying no change. Seems very slow. --- cms/static/js/base.js | 61 +++++++++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 318bb61eb4..46de7f0f10 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -253,44 +253,53 @@ function expandSection(event) { $(event.currentTarget).find('.expand-collapse-icon').removeClass('expand').addClass('collapse'); } +function checkDropValidity(event, ui) { + var posInDestination = ui.position.top - $(event.target).position().top; + if (posInDestination <= -ui.item.height() || posInDestination >= $(event.target).height()) { + // May need to call on ui.sender + $(event.target).sortable("cancel"); + return false; + } + return true; +} + // This method only changes the ordering of the child objects in a subsection function onUnitReordered(event, ui) { - var subsection_id = $(this).data('subsection-id'); - - var _els = $(this).children('li:.leaf'); + // This is called 2x when moving from one subsection to another: once for the sender and once + // for the receiver. The sender's call has ui.sender == null + var subsection_id = $(event.target).data('subsection-id'); + var _els = $(event.target).children('li:.leaf'); var children = _els.map(function(idx, el) { return $(el).data('id'); }).get(); - - // call into server to commit the new order - $.ajax({ - url: "/save_item", - type: "POST", - dataType: "json", - contentType: "application/json", - data:JSON.stringify({ 'id' : subsection_id, 'children' : children}) - }); - - // remove from old container - if (ui.sender && subsection_id !== ui.sender.data('subsection-id')) { - - var _els = ui.sender.children('li:.leaf'); - var children = _els.map(function(idx, el) { return $(el).data('id'); }).get(); - - // call into server to commit the new order + // if it believes the element belongs in this section, check that it was dropped w/in the bounds + if (_.contains(children, ui.item.data('id'))) { + if (checkDropValidity(event, ui)) { + // call into server to commit the new order + $.ajax({ + url: "/save_item", + type: "POST", + dataType: "json", + contentType: "application/json", + data:JSON.stringify({ 'id' : subsection_id, 'children' : children}) + }); + } + } + else { + // recording the removal (as element is not in the collection) $.ajax({ url: "/save_item", type: "POST", dataType: "json", contentType: "application/json", - data:JSON.stringify({ 'id' : ui.sender.data('subsection-id'), 'children' : children}) + data:JSON.stringify({ 'id' : subsection_id, 'children' : children}) }); - } + } function onSubsectionReordered(event, ui) { - var section_id = $(this).data('section-id'); + var section_id = $(event.target).data('section-id'); - var _els = $(this).children('li:.branch'); + var _els = $(event.target).children('li:.branch'); var children = _els.map(function(idx, el) { return $(el).data('id'); }).get(); // call into server to commit the new order @@ -320,9 +329,9 @@ function onSubsectionReordered(event, ui) { } function onSectionReordered() { - var course_id = $(this).data('course-id'); + var course_id = $(event.target).data('course-id'); - var _els = $(this).children('section:.branch'); + var _els = $(event.target).children('section:.branch'); var children = _els.map(function(idx, el) { return $(el).data('id'); }).get(); // call into server to commit the new order From 30c87be20f0cf36f3003c3def4c8fb64697c8cc1 Mon Sep 17 00:00:00 2001 From: Don Mitchell Date: Thu, 17 Jan 2013 17:09:31 -0500 Subject: [PATCH 06/13] Subsection drag between sections should work now. --- cms/static/js/base.js | 57 ++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 46de7f0f10..8e4a4c4916 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -55,16 +55,6 @@ $(document).ready(function() { $("#start_date, #start_time, #due_date, #due_time").bind('change', autosaveInput); $('.sync-date, .remove-date').bind('click', autosaveInput); - var cachedHesitation = new CMS.HesitateEvent( expandSection, $('.branch .collapsed'), 'mouseLeave'); - // making the unit list sortable - $('.sortable-unit-list').sortable({ - axis: 'y', - handle: '.drag-handle', - update: onUnitReordered, - connectWith: '.sortable-unit-list', - revert: true - }); - // expand/collapse methods for optional date setters $('.set-date').bind('click', showDateSetter); $('.remove-date').bind('click', removeDateSetter); @@ -90,6 +80,16 @@ $(document).ready(function() { $('.import .file-input').click(); }); + var cachedHesitation = new CMS.HesitateEvent( expandSection, $('.branch .collapsed'), 'mouseLeave'); + // making the unit list sortable + $('.sortable-unit-list').sortable({ + axis: 'y', + handle: '.drag-handle', + update: onUnitReordered, + connectWith: '.sortable-unit-list', + revert: true + }); + // Subsection reordering $('.subsection-list > ol').sortable({ axis: 'y', @@ -254,9 +254,8 @@ function expandSection(event) { } function checkDropValidity(event, ui) { - var posInDestination = ui.position.top - $(event.target).position().top; + var posInDestination = ui.item.position().top - $(event.target).position().top; if (posInDestination <= -ui.item.height() || posInDestination >= $(event.target).height()) { - // May need to call on ui.sender $(event.target).sortable("cancel"); return false; } @@ -297,34 +296,32 @@ function onUnitReordered(event, ui) { } function onSubsectionReordered(event, ui) { + // see onUnitReordered for pattern and comments var section_id = $(event.target).data('section-id'); - var _els = $(event.target).children('li:.branch'); var children = _els.map(function(idx, el) { return $(el).data('id'); }).get(); - - // call into server to commit the new order - $.ajax({ - url: "/save_item", - type: "POST", - dataType: "json", - contentType: "application/json", - data:JSON.stringify({ 'id' : section_id, 'children' : children}) - }); - - // remove from old container - if (ui.sender && section_id !== ui.sender.data('section-id')) { - var _els = ui.sender.children('li:.branch'); - var children = _els.map(function(idx, el) { return $(el).data('id'); }).get(); - + // if it believes the element belongs in this section, check that it was dropped w/in the bounds + if (_.contains(children, ui.item.data('id'))) { + if (checkDropValidity(event, ui)) { + // call into server to commit the new order + $.ajax({ + url: "/save_item", + type: "POST", + dataType: "json", + contentType: "application/json", + data:JSON.stringify({ 'id' : section_id, 'children' : children}) + }); + } + } + else { // call into server to commit the new order $.ajax({ url: "/save_item", type: "POST", dataType: "json", contentType: "application/json", - data:JSON.stringify({ 'id' : ui.sender.data('section-id'), 'children' : children}) + data:JSON.stringify({ 'id' : section_id, 'children' : children}) }); - } } From f10a5c4f83431a273ce084d457b8526c60fdb418 Mon Sep 17 00:00:00 2001 From: Don Mitchell Date: Fri, 18 Jan 2013 15:21:21 -0500 Subject: [PATCH 07/13] Oops, forgot new class --- cms/static/js/hesitate.js | 48 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 cms/static/js/hesitate.js diff --git a/cms/static/js/hesitate.js b/cms/static/js/hesitate.js new file mode 100644 index 0000000000..b3f14bc26e --- /dev/null +++ b/cms/static/js/hesitate.js @@ -0,0 +1,48 @@ +/* + * Create a HesitateEvent and assign it as the event to execute: + * $(el).on('mouseEnter', CMS.HesitateEvent( expand, 'mouseLeave').trigger); + * It calls the executeOnTimeOut function with the event.currentTarget after the configurable timeout IFF the cancelSelector event + * did not occur on the event.currentTarget. + * + * More specifically, when trigger is called (triggered by the event you bound it to), it starts a timer + * which the cancelSelector event will cancel or if the timer finished, it executes the executeOnTimeOut function + * passing it the original event (whose currentTarget s/b the specific ele). It never accumulates events; however, it doesn't hurt for your + * code to minimize invocations of trigger by binding to mouseEnter v mouseOver and such. + * + * NOTE: if something outside of this wants to cancel the event, invoke cachedhesitation.untrigger(null | anything); + */ + +CMS.HesitateEvent = function(executeOnTimeOut, jqueryEl, cancelSelector) { + this.executeOnTimeOut = executeOnTimeOut; + this.cancelSelector = cancelSelector; + this.timeoutEventId = null; + this.originalEvent = null; +} + +CMS.HesitateEvent.DURATION = 400; + +CMS.HesitateEvent.prototype.trigger = function(event) { +console.log('trigger'); + if (this.timeoutEventId === null) { + this.timeoutEventId = window.setTimeout(this.fireEvent, CMS.HesitateEvent.DURATION); + this.originalEvent = event; + // is it wrong to bind to the below v $(event.currentTarget)? + $(this.originalEvent.currentTarget).on(this.cancelSelector, this.untrigger); + } +} + +CMS.HesitateEvent.prototype.fireEvent = function(event) { +console.log('fire'); + this.timeoutEventId = null; + $(this.originalEvent.currentTarget).off(this.cancelSelector, this.untrigger); + this.executeOnTimeOut(this.originalEvent); +} + +CMS.HesitateEvent.prototype.untrigger = function(event) { +console.log('untrigger'); + if (this.timeoutEventId) { + window.clearTimeout(this.timeoutEventId); + $(this.originalEvent.currentTarget).off(this.cancelSelector, this.untrigger); + } + this.timeoutEventId = null; +} \ No newline at end of file From 48fa4b4a5e89a901b234959e3daa986294a54c05 Mon Sep 17 00:00:00 2001 From: Don Mitchell Date: Tue, 22 Jan 2013 09:49:12 -0500 Subject: [PATCH 08/13] Cache in order to try updated sortable lib --- cms/static/js/hesitate.js | 50 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 cms/static/js/hesitate.js diff --git a/cms/static/js/hesitate.js b/cms/static/js/hesitate.js new file mode 100644 index 0000000000..06107f3f4e --- /dev/null +++ b/cms/static/js/hesitate.js @@ -0,0 +1,50 @@ +/* + * Create a HesitateEvent and assign it as the event to execute: + * $(el).on('mouseEnter', CMS.HesitateEvent( expand, 'mouseLeave').trigger); + * It calls the executeOnTimeOut function with the event.currentTarget after the configurable timeout IFF the cancelSelector event + * did not occur on the event.currentTarget. + * + * More specifically, when trigger is called (triggered by the event you bound it to), it starts a timer + * which the cancelSelector event will cancel or if the timer finished, it executes the executeOnTimeOut function + * passing it the original event (whose currentTarget s/b the specific ele). It never accumulates events; however, it doesn't hurt for your + * code to minimize invocations of trigger by binding to mouseEnter v mouseOver and such. + * + * NOTE: if something outside of this wants to cancel the event, invoke cachedhesitation.untrigger(null | anything); + */ + +CMS.HesitateEvent = function(executeOnTimeOut, cancelSelector, onlyOnce = false) { + this.executeOnTimeOut = executeOnTimeOut; + this.cancelSelector = cancelSelector; + this.timeoutEventId = null; + this.originalEvent = null; + this.onlyOnce = onlyOnce; +} + +CMS.HesitateEvent.DURATION = 400; + +CMS.HesitateEvent.prototype.trigger = function(event) { +console.log('trigger'); + if (this.timeoutEventId === null) { + this.timeoutEventId = window.setTimeout(this.fireEvent, CMS.HesitateEvent.DURATION); + this.originalEvent = event; + // is it wrong to bind to the below v $(event.currentTarget)? + $(this.originalEvent.delegateTarget).on(this.cancelSelector, this.untrigger); + } +} + +CMS.HesitateEvent.prototype.fireEvent = function(event) { +console.log('fire'); + this.timeoutEventId = null; + $(this.originalEvent.delegateTarget).off(this.cancelSelector, this.untrigger); + if (this.onlyOnce) $(this.originalEvent.delegateTarget).off(this.originalEvent.type, this.trigger); + this.executeOnTimeOut(this.originalEvent); +} + +CMS.HesitateEvent.prototype.untrigger = function(event) { +console.log('untrigger'); + if (this.timeoutEventId) { + window.clearTimeout(this.timeoutEventId); + $(this.originalEvent.delegateTarget).off(this.cancelSelector, this.untrigger); + } + this.timeoutEventId = null; +} \ No newline at end of file From 1b64faf610911590c26be5e9da454b3a5e53745d Mon Sep 17 00:00:00 2001 From: Don Mitchell Date: Tue, 22 Jan 2013 10:37:56 -0500 Subject: [PATCH 09/13] Caching to change branches for other work --- cms/static/js/base.js | 147 +++++++++++++++++++++++++++----------- cms/static/js/hesitate.js | 4 +- 2 files changed, 109 insertions(+), 42 deletions(-) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index e99fc9a4da..34c08ef11a 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -55,13 +55,6 @@ $(document).ready(function() { $("#start_date, #start_time, #due_date, #due_time").bind('change', autosaveInput); $('.sync-date, .remove-date').bind('click', autosaveInput); - // making the unit list sortable - $('.sortable-unit-list').sortable({ - axis: 'y', - handle: '.drag-handle', - update: onUnitReordered - }); - // expand/collapse methods for optional date setters $('.set-date').bind('click', showDateSetter); $('.remove-date').bind('click', removeDateSetter); @@ -87,18 +80,44 @@ $(document).ready(function() { $('.import .file-input').click(); }); + var cachedHesitation = new CMS.HesitateEvent( expandSection, 'dropout', true); + // making the unit list draggable. Note: sortable didn't work b/c it considered + // drop points which the user hovered over as destinations if the user subsequently + // dropped at an illegal spot. + + $('.sortable-unit-list').droppable({ + accept : '.unit', + greedy: true, + drop: onUnitReordered + }); + $('.subsection-list > ol').droppable({ + // why don't we have a more useful class for subsections than id-holder? + accept : '.unit .id-holder', + drop: onSubsectionReordered, + greedy: true + }); + // Are there any other collapsed than possible droppables? + $('.collapsed').droppable({ + over : cachedHesitation.trigger + }); + + $('.unit').draggable({ + axis: 'y', + handle: '.drag-handle' + }); + // Subsection reordering - $('.subsection-list > ol').sortable({ + $('.id-holder').draggable({ axis: 'y', - handle: '.section-item .drag-handle', - update: onSubsectionReordered + handle: '.section-item .drag-handle' }); // Section reordering $('.courseware-overview').sortable({ axis: 'y', handle: 'header .drag-handle', - update: onSectionReordered + update: onSectionReordered, + revert: true }); $('.new-course-button').bind('click', addNewCourse); @@ -240,44 +259,92 @@ function removePolicyMetadata(e) { saveSubsection() } - -// This method only changes the ordering of the child objects in a subsection -function onUnitReordered() { - var subsection_id = $(this).data('subsection-id'); - - var _els = $(this).children('li:.leaf'); - var children = _els.map(function(idx, el) { return $(el).data('id'); }).get(); - - // call into server to commit the new order - $.ajax({ - url: "/save_item", - type: "POST", - dataType: "json", - contentType: "application/json", - data:JSON.stringify({ 'id' : subsection_id, 'children' : children}) - }); +function expandSection(event) { + $(event.delegateTarget).removeClass('collapsed'); + $(event.delegateTarget).find('.expand-collapse-icon').removeClass('expand').addClass('collapse'); } -function onSubsectionReordered() { - var section_id = $(this).data('section-id'); +function checkDropValidity(event, ui) { + var posInDestination = ui.item.position().top - $(event.target).position().top; + if (posInDestination <= -ui.item.height() || posInDestination >= $(event.target).height()) { + $(event.target).sortable("cancel"); + return false; + } + return true; +} - var _els = $(this).children('li:.branch'); +// This method only changes the ordering of the child objects in a subsection +function onUnitReordered(event, ui) { + // a unit's been dropped on this subsection, + // figure out where + + // This is called 2x when moving from one subsection to another: once for the sender and once + // for the receiver. The sender's call has ui.sender == null + var subsection_id = $(event.target).data('subsection-id'); + var _els = $(event.target).children('li:.leaf'); var children = _els.map(function(idx, el) { return $(el).data('id'); }).get(); + // if it believes the element belongs in this section, check that it was dropped w/in the bounds + if (_.contains(children, ui.item.data('id'))) { + if (checkDropValidity(event, ui)) { + // call into server to commit the new order + $.ajax({ + url: "/save_item", + type: "POST", + dataType: "json", + contentType: "application/json", + data:JSON.stringify({ 'id' : subsection_id, 'children' : children}) + }); + } + } + else { + // recording the removal (as element is not in the collection) + $.ajax({ + url: "/save_item", + type: "POST", + dataType: "json", + contentType: "application/json", + data:JSON.stringify({ 'id' : subsection_id, 'children' : children}) + }); + } + +} - // call into server to commit the new order - $.ajax({ - url: "/save_item", - type: "POST", - dataType: "json", - contentType: "application/json", - data:JSON.stringify({ 'id' : section_id, 'children' : children}) - }); +function onSubsectionReordered(event, ui) { + // dropped object may be either unit or subsection! + + // see onUnitReordered for pattern and comments + var section_id = $(event.target).data('section-id'); + var _els = $(event.target).children('li:.branch'); + var children = _els.map(function(idx, el) { return $(el).data('id'); }).get(); + // if it believes the element belongs in this section, check that it was dropped w/in the bounds + if (_.contains(children, ui.item.data('id'))) { + if (checkDropValidity(event, ui)) { + // call into server to commit the new order + $.ajax({ + url: "/save_item", + type: "POST", + dataType: "json", + contentType: "application/json", + data:JSON.stringify({ 'id' : section_id, 'children' : children}) + }); + } + } + else { + // call into server to commit the new order + $.ajax({ + url: "/save_item", + type: "POST", + dataType: "json", + contentType: "application/json", + data:JSON.stringify({ 'id' : section_id, 'children' : children}) + }); + } } function onSectionReordered() { - var course_id = $(this).data('course-id'); + var course_id = $(event.target).data('course-id'); - var _els = $(this).children('section:.branch'); + var _els = $(event.target).children('section:.branch'); var children = _els.map(function(idx, el) { return $(el).data('id'); }).get(); // call into server to commit the new order diff --git a/cms/static/js/hesitate.js b/cms/static/js/hesitate.js index 06107f3f4e..63806ba0ec 100644 --- a/cms/static/js/hesitate.js +++ b/cms/static/js/hesitate.js @@ -12,12 +12,12 @@ * NOTE: if something outside of this wants to cancel the event, invoke cachedhesitation.untrigger(null | anything); */ -CMS.HesitateEvent = function(executeOnTimeOut, cancelSelector, onlyOnce = false) { +CMS.HesitateEvent = function(executeOnTimeOut, cancelSelector, onlyOnce) { this.executeOnTimeOut = executeOnTimeOut; this.cancelSelector = cancelSelector; this.timeoutEventId = null; this.originalEvent = null; - this.onlyOnce = onlyOnce; + this.onlyOnce = (onlyOnce === true); } CMS.HesitateEvent.DURATION = 400; From 2ae92cbd8854dca6f2c14308c5a37d6e352a6efc Mon Sep 17 00:00:00 2001 From: Don Mitchell Date: Tue, 22 Jan 2013 17:25:28 -0500 Subject: [PATCH 10/13] Added RequireJS to fix expectation that it's included. Unit drag and drop works correctly now. --- cms/envs/common.py | 2 +- cms/static/js/base.js | 103 ++++++++++++++++++++++++------------------ 2 files changed, 60 insertions(+), 45 deletions(-) diff --git a/cms/envs/common.py b/cms/envs/common.py index 08e38320ab..93901f2c9c 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -229,7 +229,7 @@ PIPELINE_JS = { 'source_filenames': sorted( rooted_glob(COMMON_ROOT / 'static/', 'coffee/src/**/*.coffee') + rooted_glob(PROJECT_ROOT / 'static/', 'coffee/src/**/*.coffee') - ) + ['js/hesitate.js', 'js/base.js'], + ) + [ 'js/vendor/RequireJS.js', 'js/hesitate.js', 'js/base.js'], 'output_filename': 'js/cms-application.js', }, 'module-js': { diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 34c08ef11a..378ec80ccf 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -80,10 +80,23 @@ $(document).ready(function() { $('.import .file-input').click(); }); - var cachedHesitation = new CMS.HesitateEvent( expandSection, 'dropout', true); // making the unit list draggable. Note: sortable didn't work b/c it considered - // drop points which the user hovered over as destinations if the user subsequently - // dropped at an illegal spot. + // drop points which the user hovered over as destinations and proactively changed + // the dom; so, if the user subsequently dropped at an illegal spot, the reversion + // point was the last dom change. + $('.unit').draggable({ + axis: 'y', + handle: '.drag-handle', + stack: '.unit', + revert: "invalid" + }); + + // Subsection reordering + $('.id-holder').draggable({ + axis: 'y', + handle: '.section-item .drag-handle', + revert: "invalid" + }); $('.sortable-unit-list').droppable({ accept : '.unit', @@ -92,26 +105,11 @@ $(document).ready(function() { }); $('.subsection-list > ol').droppable({ // why don't we have a more useful class for subsections than id-holder? - accept : '.unit .id-holder', + accept : '.id-holder', // '.unit, .id-holder', drop: onSubsectionReordered, greedy: true }); - // Are there any other collapsed than possible droppables? - $('.collapsed').droppable({ - over : cachedHesitation.trigger - }); - $('.unit').draggable({ - axis: 'y', - handle: '.drag-handle' - }); - - // Subsection reordering - $('.id-holder').draggable({ - axis: 'y', - handle: '.section-item .drag-handle' - }); - // Section reordering $('.courseware-overview').sortable({ axis: 'y', @@ -273,39 +271,56 @@ function checkDropValidity(event, ui) { return true; } -// This method only changes the ordering of the child objects in a subsection function onUnitReordered(event, ui) { // a unit's been dropped on this subsection, - // figure out where - - // This is called 2x when moving from one subsection to another: once for the sender and once - // for the receiver. The sender's call has ui.sender == null + // figure out where it came from and where it slots in. var subsection_id = $(event.target).data('subsection-id'); var _els = $(event.target).children('li:.leaf'); var children = _els.map(function(idx, el) { return $(el).data('id'); }).get(); - // if it believes the element belongs in this section, check that it was dropped w/in the bounds - if (_.contains(children, ui.item.data('id'))) { - if (checkDropValidity(event, ui)) { - // call into server to commit the new order - $.ajax({ - url: "/save_item", - type: "POST", - dataType: "json", - contentType: "application/json", - data:JSON.stringify({ 'id' : subsection_id, 'children' : children}) - }); - } + // if new to this parent, figure out which parent to remove it from and do so + if (!_.contains(children, ui.draggable.data('id'))) { + var old_parent = ui.draggable.parent(); + var old_children = old_parent.children('li:.leaf').map(function(idx, el) { return $(el).data('id'); }).get(); + old_children = _.without(old_children, ui.draggable.data('id')); + // call into server to commit the new order + $.ajax({ + url: "/save_item", + type: "POST", + dataType: "json", + contentType: "application/json", + data:JSON.stringify({ 'id' : old_parent.data('subsection-id'), 'children' : old_children}) + }); + // } else { - // recording the removal (as element is not in the collection) - $.ajax({ - url: "/save_item", - type: "POST", - dataType: "json", - contentType: "application/json", - data:JSON.stringify({ 'id' : subsection_id, 'children' : children}) - }); + // staying in same parent + // remove so that the replacement in the right place doesn't double it + children = _.without(children, ui.draggable.data('id')); } + // add to this parent (figure out where) + for (var i = 0; i < _els.length; i++) { + if (ui.offset.top < $(_els[i]).offset().top) { + // insert at i in children and _els + ui.draggable.insertBefore($(_els[i])); + // TODO figure out correct way to have it format (and again below) + ui.draggable.attr("style", "position:relative;"); + children.splice(i, 0, ui.draggable.data('id')); + break; + } + } + // see if it goes at end (the above loop didn't insert it) + if (!_.contains(children, ui.draggable.data('id'))) { + $(event.target).append(ui.draggable); + ui.draggable.attr("style", "position:relative;"); // STYLE hack too + children.push(ui.draggable.data('id')); + } + $.ajax({ + url: "/save_item", + type: "POST", + dataType: "json", + contentType: "application/json", + data:JSON.stringify({ 'id' : subsection_id, 'children' : children}) + }); } From c2fcdfde3b42c805eeb6c7794e0ea44c681f0b33 Mon Sep 17 00:00:00 2001 From: Don Mitchell Date: Tue, 22 Jan 2013 17:26:12 -0500 Subject: [PATCH 11/13] RequireJS included 2x in main_vendor_js list --- lms/envs/common.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lms/envs/common.py b/lms/envs/common.py index 64cfc276a4..6bf76d1f5e 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -423,7 +423,6 @@ courseware_js = ( ) main_vendor_js = [ - 'js/vendor/RequireJS.js', 'js/vendor/json2.js', 'js/vendor/RequireJS.js', 'js/vendor/jquery.min.js', From ad5f00f2700540b6bc07d7c1c962188a29b39463 Mon Sep 17 00:00:00 2001 From: Don Mitchell Date: Wed, 23 Jan 2013 11:16:04 -0500 Subject: [PATCH 12/13] Drag and drop for sections, subsections, and units works with following caveats: - it drops wherever the y of the cursor is no matter the x for the cursor. I've constrained the draggables to never change x; so, the drop doesn't care where the x of the cursor is only the y. - if you force auto scroll, the visual thing you'll dragging gets out of sync w/ the mouse cursor; however, the mouse cursor is the "true" location; so, the drop's posit is the mouse cursor y not the visual clone's y. --- cms/static/js/base.js | 95 ++++++++++++++++--------------------------- 1 file changed, 36 insertions(+), 59 deletions(-) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 378ec80ccf..553e79a8af 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -95,9 +95,19 @@ $(document).ready(function() { $('.id-holder').draggable({ axis: 'y', handle: '.section-item .drag-handle', + stack: '.id-holder', revert: "invalid" }); + // Section reordering + $('.courseware-section').draggable({ + axis: 'y', + handle: 'header .drag-handle', + stack: '.courseware-section', + revert: "invalid" + }); + + $('.sortable-unit-list').droppable({ accept : '.unit', greedy: true, @@ -111,11 +121,10 @@ $(document).ready(function() { }); // Section reordering - $('.courseware-overview').sortable({ - axis: 'y', - handle: 'header .drag-handle', - update: onSectionReordered, - revert: true + $('.courseware-overview').droppable({ + accept : '.courseware-section', + drop: onSectionReordered, + greedy: true }); $('.new-course-button').bind('click', addNewCourse); @@ -274,13 +283,29 @@ function checkDropValidity(event, ui) { function onUnitReordered(event, ui) { // a unit's been dropped on this subsection, // figure out where it came from and where it slots in. - var subsection_id = $(event.target).data('subsection-id'); - var _els = $(event.target).children('li:.leaf'); + _handleReorder(event, ui, 'subsection-id', 'li:.leaf'); +} + +function onSubsectionReordered(event, ui) { + // a subsection has been dropped on this section, + // figure out where it came from and where it slots in. + _handleReorder(event, ui, 'section-id', 'li:.branch'); +} + +function onSectionReordered(event, ui) { + // a section moved w/in the overall (cannot change course via this, so no parentage change possible, just order) + _handleReorder(event, ui, 'course-id', '.courseware-section'); +} + +function _handleReorder(event, ui, parentIdField, childrenSelector) { + // figure out where it came from and where it slots in. + var subsection_id = $(event.target).data(parentIdField); + var _els = $(event.target).children(childrenSelector); var children = _els.map(function(idx, el) { return $(el).data('id'); }).get(); // if new to this parent, figure out which parent to remove it from and do so if (!_.contains(children, ui.draggable.data('id'))) { var old_parent = ui.draggable.parent(); - var old_children = old_parent.children('li:.leaf').map(function(idx, el) { return $(el).data('id'); }).get(); + var old_children = old_parent.children(childrenSelector).map(function(idx, el) { return $(el).data('id'); }).get(); old_children = _.without(old_children, ui.draggable.data('id')); // call into server to commit the new order $.ajax({ @@ -288,7 +313,7 @@ function onUnitReordered(event, ui) { type: "POST", dataType: "json", contentType: "application/json", - data:JSON.stringify({ 'id' : old_parent.data('subsection-id'), 'children' : old_children}) + data:JSON.stringify({ 'id' : old_parent.data(parentIdField), 'children' : old_children}) }); // } @@ -299,10 +324,10 @@ function onUnitReordered(event, ui) { } // add to this parent (figure out where) for (var i = 0; i < _els.length; i++) { - if (ui.offset.top < $(_els[i]).offset().top) { + if (!ui.draggable.is(_els[i]) && ui.offset.top < $(_els[i]).offset().top) { // insert at i in children and _els ui.draggable.insertBefore($(_els[i])); - // TODO figure out correct way to have it format (and again below) + // TODO figure out correct way to have it format (and similar line below) ui.draggable.attr("style", "position:relative;"); children.splice(i, 0, ui.draggable.data('id')); break; @@ -324,54 +349,6 @@ function onUnitReordered(event, ui) { } -function onSubsectionReordered(event, ui) { - // dropped object may be either unit or subsection! - - // see onUnitReordered for pattern and comments - var section_id = $(event.target).data('section-id'); - var _els = $(event.target).children('li:.branch'); - var children = _els.map(function(idx, el) { return $(el).data('id'); }).get(); - // if it believes the element belongs in this section, check that it was dropped w/in the bounds - if (_.contains(children, ui.item.data('id'))) { - if (checkDropValidity(event, ui)) { - // call into server to commit the new order - $.ajax({ - url: "/save_item", - type: "POST", - dataType: "json", - contentType: "application/json", - data:JSON.stringify({ 'id' : section_id, 'children' : children}) - }); - } - } - else { - // call into server to commit the new order - $.ajax({ - url: "/save_item", - type: "POST", - dataType: "json", - contentType: "application/json", - data:JSON.stringify({ 'id' : section_id, 'children' : children}) - }); - } -} - -function onSectionReordered() { - var course_id = $(event.target).data('course-id'); - - var _els = $(event.target).children('section:.branch'); - var children = _els.map(function(idx, el) { return $(el).data('id'); }).get(); - - // call into server to commit the new order - $.ajax({ - url: "/save_item", - type: "POST", - dataType: "json", - contentType: "application/json", - data:JSON.stringify({ 'id' : course_id, 'children' : children}) - }); -} - function getEdxTimeFromDateTimeVals(date_val, time_val, format) { var edxTimeStr = null; From 4d0d46462cb122cc553137a3db495f07452bc280 Mon Sep 17 00:00:00 2001 From: Don Mitchell Date: Wed, 23 Jan 2013 12:12:40 -0500 Subject: [PATCH 13/13] Fixed the y axis drop problem by requiring it to be within the x bounds. --- cms/static/js/base.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 553e79a8af..741b017210 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -111,11 +111,13 @@ $(document).ready(function() { $('.sortable-unit-list').droppable({ accept : '.unit', greedy: true, + tolerance: "pointer", drop: onUnitReordered }); $('.subsection-list > ol').droppable({ // why don't we have a more useful class for subsections than id-holder? accept : '.id-holder', // '.unit, .id-holder', + tolerance: "pointer", drop: onSubsectionReordered, greedy: true }); @@ -123,6 +125,7 @@ $(document).ready(function() { // Section reordering $('.courseware-overview').droppable({ accept : '.courseware-section', + tolerance: "pointer", drop: onSectionReordered, greedy: true });