diff --git a/cms/envs/common.py b/cms/envs/common.py
index c047d689ce..98a5fbf26d 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': {
diff --git a/cms/static/js/base.js b/cms/static/js/base.js
index e99fc9a4da..01a8d6450e 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,54 @@ $(document).ready(function() {
$('.import .file-input').click();
});
- // Subsection reordering
- $('.subsection-list > ol').sortable({
- axis: 'y',
- handle: '.section-item .drag-handle',
- update: onSubsectionReordered
+ // making the unit list draggable. Note: sortable didn't work b/c it considered
+ // 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',
+ stack: '.id-holder',
+ revert: "invalid"
+ });
+
// Section reordering
- $('.courseware-overview').sortable({
- axis: 'y',
- handle: 'header .drag-handle',
- update: onSectionReordered
+ $('.courseware-section').draggable({
+ axis: 'y',
+ handle: 'header .drag-handle',
+ stack: '.courseware-section',
+ revert: "invalid"
+ });
+
+
+ $('.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
+ });
+
+ // Section reordering
+ $('.courseware-overview').droppable({
+ accept : '.courseware-section',
+ tolerance: "pointer",
+ drop: onSectionReordered,
+ greedy: true
});
$('.new-course-button').bind('click', addNewCourse);
@@ -240,54 +269,76 @@ function removePolicyMetadata(e) {
saveSubsection()
}
+function expandSection(event) {
+ $(event.delegateTarget).removeClass('collapsed');
+ $(event.delegateTarget).find('.expand-collapse-icon').removeClass('expand').addClass('collapse');
+}
-// This method only changes the ordering of the child objects in a subsection
-function onUnitReordered() {
- var subsection_id = $(this).data('subsection-id');
+function onUnitReordered(event, ui) {
+ // a unit's been dropped on this subsection,
+ // figure out where it came from and where it slots in.
+ _handleReorder(event, ui, 'subsection-id', 'li:.leaf');
+}
- var _els = $(this).children('li:.leaf');
- var children = _els.map(function(idx, el) { return $(el).data('id'); }).get();
+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');
+}
- // call into server to commit the new order
- $.ajax({
- url: "/save_item",
+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(childrenSelector).map(function(idx, el) { return $(el).data('id'); }).get();
+ old_children = _.without(old_children, ui.draggable.data('id'));
+ $.ajax({
+ url: "/save_item",
+ type: "POST",
+ dataType: "json",
+ contentType: "application/json",
+ data:JSON.stringify({ 'id' : old_parent.data(parentIdField), 'children' : old_children})
+ });
+ }
+ else {
+ // 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.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 remove the style: top:n; setting (and similar line 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})
});
-}
-function onSubsectionReordered() {
- var section_id = $(this).data('section-id');
-
- var _els = $(this).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})
- });
-}
-
-function onSectionReordered() {
- var course_id = $(this).data('course-id');
-
- var _els = $(this).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) {
diff --git a/cms/static/js/hesitate.js b/cms/static/js/hesitate.js
new file mode 100644
index 0000000000..63806ba0ec
--- /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) {
+ this.executeOnTimeOut = executeOnTimeOut;
+ this.cancelSelector = cancelSelector;
+ this.timeoutEventId = null;
+ this.originalEvent = null;
+ this.onlyOnce = (onlyOnce === true);
+}
+
+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
diff --git a/common/static/js/vendor/jquery-ui.min.js b/common/static/js/vendor/jquery-ui.min.js
index e36a7f0f6c..9c44a8b01b 100755
--- a/common/static/js/vendor/jquery-ui.min.js
+++ b/common/static/js/vendor/jquery-ui.min.js
@@ -1,125 +1,6 @@
-/*! jQuery UI - v1.8.22 - 2012-07-24
-* https://github.com/jquery/jquery-ui
-* Includes: jquery.ui.core.js
-* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */
-(function(a,b){function c(b,c){var e=b.nodeName.toLowerCase();if("area"===e){var f=b.parentNode,g=f.name,h;return!b.href||!g||f.nodeName.toLowerCase()!=="map"?!1:(h=a("img[usemap=#"+g+"]")[0],!!h&&d(h))}return(/input|select|textarea|button|object/.test(e)?!b.disabled:"a"==e?b.href||c:c)&&d(b)}function d(b){return!a(b).parents().andSelf().filter(function(){return a.curCSS(this,"visibility")==="hidden"||a.expr.filters.hidden(this)}).length}a.ui=a.ui||{};if(a.ui.version)return;a.extend(a.ui,{version:"1.8.22",keyCode:{ALT:18,BACKSPACE:8,CAPS_LOCK:20,COMMA:188,COMMAND:91,COMMAND_LEFT:91,COMMAND_RIGHT:93,CONTROL:17,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,INSERT:45,LEFT:37,MENU:93,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SHIFT:16,SPACE:32,TAB:9,UP:38,WINDOWS:91}}),a.fn.extend({propAttr:a.fn.prop||a.fn.attr,_focus:a.fn.focus,focus:function(b,c){return typeof b=="number"?this.each(function(){var d=this;setTimeout(function(){a(d).focus(),c&&c.call(d)},b)}):this._focus.apply(this,arguments)},scrollParent:function(){var b;return a.browser.msie&&/(static|relative)/.test(this.css("position"))||/absolute/.test(this.css("position"))?b=this.parents().filter(function(){return/(relative|absolute|fixed)/.test(a.curCSS(this,"position",1))&&/(auto|scroll)/.test(a.curCSS(this,"overflow",1)+a.curCSS(this,"overflow-y",1)+a.curCSS(this,"overflow-x",1))}).eq(0):b=this.parents().filter(function(){return/(auto|scroll)/.test(a.curCSS(this,"overflow",1)+a.curCSS(this,"overflow-y",1)+a.curCSS(this,"overflow-x",1))}).eq(0),/fixed/.test(this.css("position"))||!b.length?a(document):b},zIndex:function(c){if(c!==b)return this.css("zIndex",c);if(this.length){var d=a(this[0]),e,f;while(d.length&&d[0]!==document){e=d.css("position");if(e==="absolute"||e==="relative"||e==="fixed"){f=parseInt(d.css("zIndex"),10);if(!isNaN(f)&&f!==0)return f}d=d.parent()}}return 0},disableSelection:function(){return this.bind((a.support.selectstart?"selectstart":"mousedown")+".ui-disableSelection",function(a){a.preventDefault()})},enableSelection:function(){return this.unbind(".ui-disableSelection")}}),a("").outerWidth(1).jquery||a.each(["Width","Height"],function(c,d){function h(b,c,d,f){return a.each(e,function(){c-=parseFloat(a.curCSS(b,"padding"+this,!0))||0,d&&(c-=parseFloat(a.curCSS(b,"border"+this+"Width",!0))||0),f&&(c-=parseFloat(a.curCSS(b,"margin"+this,!0))||0)}),c}var e=d==="Width"?["Left","Right"]:["Top","Bottom"],f=d.toLowerCase(),g={innerWidth:a.fn.innerWidth,innerHeight:a.fn.innerHeight,outerWidth:a.fn.outerWidth,outerHeight:a.fn.outerHeight};a.fn["inner"+d]=function(c){return c===b?g["inner"+d].call(this):this.each(function(){a(this).css(f,h(this,c)+"px")})},a.fn["outer"+d]=function(b,c){return typeof b!="number"?g["outer"+d].call(this,b):this.each(function(){a(this).css(f,h(this,b,!0,c)+"px")})}}),a.extend(a.expr[":"],{data:a.expr.createPseudo?a.expr.createPseudo(function(b){return function(c){return!!a.data(c,b)}}):function(b,c,d){return!!a.data(b,d[3])},focusable:function(b){return c(b,!isNaN(a.attr(b,"tabindex")))},tabbable:function(b){var d=a.attr(b,"tabindex"),e=isNaN(d);return(e||d>=0)&&c(b,!e)}}),a(function(){var b=document.body,c=b.appendChild(c=document.createElement("div"));c.offsetHeight,a.extend(c.style,{minHeight:"100px",height:"auto",padding:0,borderWidth:0}),a.support.minHeight=c.offsetHeight===100,a.support.selectstart="onselectstart"in c,b.removeChild(c).style.display="none"}),a.curCSS||(a.curCSS=a.css),a.extend(a.ui,{plugin:{add:function(b,c,d){var e=a.ui[b].prototype;for(var f in d)e.plugins[f]=e.plugins[f]||[],e.plugins[f].push([c,d[f]])},call:function(a,b,c){var d=a.plugins[b];if(!d||!a.element[0].parentNode)return;for(var e=0;e
| '+this._get(a,"weekHeader")+" | ":"";for(var S=0;S<7;S++){var T=(S+y)%7;R+="=5?' class="ui-datepicker-week-end"':"")+">"+''+C[T]+" | "}Q+=R+"'+this._get(a,"calculateWeek")(Y)+" | ":"";for(var S=0;S<7;S++){var ba=F?F.apply(a.input?a.input[0]:null,[Y]):[!0,""],bb=Y.getMonth()!=n,bc=bb&&!H||!ba[0]||l&&Y"+(bb&&!G?" ":bc?''+Y.getDate()+"":''+Y.getDate()+"")+" | ",Y.setDate(Y.getDate()+1),Y=this._daylightSavingAdjust(Y)}Q+=_+""}n++,n>11&&(n=0,o++),Q+="
|---|