diff --git a/cms/static/coffee/spec/views/course_info_spec.coffee b/cms/static/coffee/spec/views/course_info_spec.coffee index de33b850833b..49ad3b23e7a4 100644 --- a/cms/static/coffee/spec/views/course_info_spec.coffee +++ b/cms/static/coffee/spec/views/course_info_spec.coffee @@ -54,17 +54,14 @@ define ["js/views/course_info_handout", "js/views/course_info_update", "js/model @courseInfoEdit.$el.find('.save-button').click() @cancelNewCourseInfo = (useCancelButton) -> - spyOn(@courseInfoEdit.$modalCover, 'show').andCallThrough() - spyOn(@courseInfoEdit.$modalCover, 'hide').andCallThrough() - @courseInfoEdit.onNew(@event) - expect(@courseInfoEdit.$modalCover.show).toHaveBeenCalled() + spyOn(@courseInfoEdit.$modalCover, 'hide').andCallThrough() spyOn(@courseInfoEdit.$codeMirror, 'getValue').andReturn('unsaved changes') model = @collection.at(0) spyOn(model, "save").andCallThrough() - cancelEditingUpdate(useCancelButton) + cancelEditingUpdate(@courseInfoEdit, @courseInfoEdit.$modalCover, useCancelButton) expect(@courseInfoEdit.$modalCover.hide).toHaveBeenCalled() expect(model.save).not.toHaveBeenCalled() @@ -73,28 +70,25 @@ define ["js/views/course_info_handout", "js/views/course_info_update", "js/model @cancelExistingCourseInfo = (useCancelButton) -> @createNewUpdate('existing update') - - spyOn(@courseInfoEdit.$modalCover, 'show').andCallThrough() - spyOn(@courseInfoEdit.$modalCover, 'hide').andCallThrough() @courseInfoEdit.$el.find('.edit-button').click() - expect(@courseInfoEdit.$modalCover.show).toHaveBeenCalled() + spyOn(@courseInfoEdit.$modalCover, 'hide').andCallThrough() spyOn(@courseInfoEdit.$codeMirror, 'getValue').andReturn('modification') model = @collection.at(0) spyOn(model, "save").andCallThrough() - - cancelEditingUpdate(useCancelButton) + model.id = "saved_to_server" + cancelEditingUpdate(@courseInfoEdit, @courseInfoEdit.$modalCover, useCancelButton) expect(@courseInfoEdit.$modalCover.hide).toHaveBeenCalled() expect(model.save).not.toHaveBeenCalled() previewContents = @courseInfoEdit.$el.find('.update-contents').html() expect(previewContents).toEqual('existing update') - cancelEditingUpdate = (update, useCancelButton) -> + cancelEditingUpdate = (update, modalCover, useCancelButton) -> if useCancelButton update.$el.find('.cancel-button').click() else - $('.modal-cover').click() + modalCover.click() afterEach -> @xhrRestore() diff --git a/cms/static/coffee/src/views/module_edit.coffee b/cms/static/coffee/src/views/module_edit.coffee index 2faccdab2649..a9ffae7d137e 100644 --- a/cms/static/coffee/src/views/module_edit.coffee +++ b/cms/static/coffee/src/views/module_edit.coffee @@ -1,7 +1,7 @@ define ["backbone", "jquery", "underscore", "gettext", "xmodule", - "js/views/feedback_notification", "js/views/metadata", "js/collections/metadata" - "jquery.inputnumber"], -(Backbone, $, _, gettext, XModule, NotificationView, MetadataView, MetadataCollection) -> + "js/views/feedback_notification", "js/views/metadata", "js/collections/metadata", + "js/utils/modal", "jquery.inputnumber"], +(Backbone, $, _, gettext, XModule, NotificationView, MetadataView, MetadataCollection, ModalUtils) -> class ModuleEdit extends Backbone.View tagName: 'li' className: 'component' @@ -103,17 +103,12 @@ define ["backbone", "jquery", "underscore", "gettext", "xmodule", event.preventDefault() @$el.removeClass('editing') @$component_editor().slideUp(150) - @hideModal() - - hideModal: -> - $modalCover = $(".modal-cover") - $modalCover.hide() - $modalCover.removeClass('is-fixed') + ModalUtils.hideModalCover() clickEditButton: (event) -> event.preventDefault() @$el.addClass('editing') - $(".modal-cover").show().addClass('is-fixed') + ModalUtils.showModalCover(true) @$component_editor().slideDown(150) @loadEdit() diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 0ac99325998d..84121fccd7f2 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -1,10 +1,8 @@ require(["domReady", "jquery", "underscore", "gettext", "js/views/feedback_notification", "js/views/feedback_prompt", - "jquery.ui", "jquery.timepicker", "jquery.leanModal", "jquery.form", "jquery.smoothScroll"], - function(domReady, $, _, gettext, NotificationView, PromptView) { + "js/utils/get_date", "js/utils/check_for_cancel", "jquery.ui", "jquery.timepicker", "jquery.leanModal", "jquery.form", "jquery.smoothScroll"], + function(domReady, $, _, gettext, NotificationView, PromptView, DateUtils, BindCheckForCancel) { var $body; -var $modal; -var $modalCover; var $newComponentItem; var $changedInput; var $spinner; @@ -14,8 +12,6 @@ var $newComponentButton; domReady(function() { $body = $('body'); - $modal = $('.history-modal'); - $modalCover = $('.modal-cover'); $newComponentItem = $('.new-component-item'); $newComponentTypePicker = $('.new-component'); @@ -26,8 +22,6 @@ domReady(function() { $('.expand-collapse-icon').bind('click', toggleSubmodules); $('.visibility-options').bind('change', setVisibility); - $modal.bind('click', hideModal); - $modalCover.bind('click', hideModal); $body.on('click', '.embeddable-xml-input', function() { $(this).select(); @@ -94,16 +88,12 @@ domReady(function() { // tender feedback window scrolling $('a.show-tender').bind('click', smoothScrollTop); - // toggling footer additional support - $('.cta-show-sock').bind('click', toggleSock); - // toggling overview section details $(function() { if ($('.courseware-section').length > 0) { $('.toggle-button-sections').addClass('is-shown'); } }); - $('.toggle-button-sections').bind('click', toggleSections); // autosave when leaving input field $body.on('change', '.subsection-display-name-input', saveSubsection); @@ -126,24 +116,11 @@ domReady(function() { $('.sync-date').bind('click', syncReleaseDate); - // import form setup - $('.view-import .file-input').bind('change', showImportSubmit); - $('.view-import .choose-file-button, .view-import .choose-file-button-inline').bind('click', function(e) { - e.preventDefault(); - $('.view-import .file-input').click(); - }); - - $('.new-course-button').bind('click', addNewCourse); // section date setting $('.set-publish-date').bind('click', setSectionScheduleDate); $('.edit-section-start-cancel').bind('click', cancelSetSectionScheduleDate); - $('.edit-section-start-save').bind('click', saveSetSectionScheduleDate); - $body.on('click', '.section-published-date .edit-button', editSectionPublishDate); - $body.on('click', '.section-published-date .schedule-button', editSectionPublishDate); - $body.on('click', '.edit-subsection-publish-settings .save-button', saveSetSectionScheduleDate); - $body.on('click', '.edit-subsection-publish-settings .cancel-button', hideModal); $body.on('change', '.edit-subsection-publish-settings .start-date', function() { if ($('.edit-subsection-publish-settings').find('.start-time').val() == '') { $('.edit-subsection-publish-settings').find('.start-time').val('12:00am'); @@ -183,58 +160,6 @@ function linkNewWindow(e) { e.preventDefault(); } -function toggleSections(e) { - e.preventDefault(); - - $section = $('.courseware-section'); - sectionCount = $section.length; - $button = $(this); - $labelCollapsed = $(' ' + - gettext('Collapse All Sections') + ''); - $labelExpanded = $(' ' + - gettext('Expand All Sections') + ''); - - var buttonLabel = $button.hasClass('is-activated') ? $labelCollapsed : $labelExpanded; - $button.toggleClass('is-activated').html(buttonLabel); - - if ($button.hasClass('is-activated')) { - $section.addClass('collapsed'); - // first child in order to avoid the icons on the subsection lists which are not in the first child - $section.find('header .expand-collapse-icon').removeClass('collapse').addClass('expand'); - } else { - $section.removeClass('collapsed'); - // first child in order to avoid the icons on the subsection lists which are not in the first child - $section.find('header .expand-collapse-icon').removeClass('expand').addClass('collapse'); - } -} - -function editSectionPublishDate(e) { - e.preventDefault(); - $modal = $('.edit-subsection-publish-settings').show(); - $modal.attr('data-id', $(this).attr('data-id')); - $modal.find('.start-date').val($(this).attr('data-date')); - $modal.find('.start-time').val($(this).attr('data-time')); - if ($modal.find('.start-date').val() == '' && $modal.find('.start-time').val() == '') { - $modal.find('.save-button').hide(); - } - $modal.find('.section-name').html('"' + $(this).closest('.courseware-section').find('.section-name-span').text() + '"'); - $modalCover.show(); -} - -function showImportSubmit(e) { - var filepath = $(this).val(); - if (filepath.substr(filepath.length - 6, 6) == 'tar.gz') { - $('.error-block').hide(); - $('.file-name').html($(this).val().replace('C:\\fakepath\\', '')); - $('.file-name-block').show(); - $('.view-import .choose-file-button').hide(); - $('.submit-button').show(); - $('.progress').show(); - } else { - $('.error-block').html(gettext('File format not supported. Please upload a file with a tar.gz extension.')).show(); - } -} - function syncReleaseDate(e) { e.preventDefault(); $(this).closest('.notice').hide(); @@ -242,22 +167,6 @@ function syncReleaseDate(e) { $("#start_time").val(""); } -function getDatetime(datepickerInput, timepickerInput) { - // given a pair of inputs (datepicker and timepicker), return a JS Date - // object that corresponds to the datetime that they represent. Assume - // UTC timezone, NOT the timezone of the user's browser. - var date = $(datepickerInput).datepicker("getDate"); - var time = $(timepickerInput).timepicker("getTime"); - if(date && time) { - return new Date(Date.UTC( - date.getFullYear(), date.getMonth(), date.getDate(), - time.getHours(), time.getMinutes() - )); - } else { - return null; - } -} - function autosaveInput(e) { var self = this; if (this.saveTimer) { @@ -298,7 +207,7 @@ function saveSubsection() { // get datetimes for start and due, stick into metadata _(["start", "due"]).each(function(name) { - var datetime = getDatetime( + var datetime = DateUtils( document.getElementById(name+"_date"), document.getElementById(name+"_time") ); @@ -319,9 +228,6 @@ function saveSubsection() { success: function() { $spinner.delay(500).fadeOut(150); $changedInput = null; - }, - error: function() { - showToastMessage(gettext('There has been an error while saving your changes.')); } }); } @@ -410,44 +316,6 @@ function _deleteItem($el, type) { confirm.show(); } -function hideModal(e) { - if (e) { - e.preventDefault(); - } - // Unit editors do not want the modal cover to hide when users click outside - // of the editor. Users must press Cancel or Save to exit the editor. - // module_edit adds and removes the "is-fixed" class. - if (!$modalCover.hasClass("is-fixed")) { - $(".modal, .edit-subsection-publish-settings").hide(); - $modalCover.hide(); - } -} - -function toggleSock(e) { - e.preventDefault(); - - var $btnLabel = $(this).find('.copy'); - var $sock = $('.wrapper-sock'); - var $sockContent = $sock.find('.wrapper-inner'); - - $sock.toggleClass('is-shown'); - $sockContent.toggle('fast'); - - $.smoothScroll({ - offset: -200, - easing: 'swing', - speed: 1000, - scrollElement: null, - scrollTarget: $sock - }); - - if ($sock.hasClass('is-shown')) { - $btnLabel.text(gettext('Hide Studio Help')); - } else { - $btnLabel.text(gettext('Looking for Help with Studio?')); - } -} - function toggleSubmodules(e) { e.preventDefault(); $(this).toggleClass('expand').toggleClass('collapse'); @@ -459,15 +327,6 @@ function setVisibility(e) { $(e.target).closest('.option').addClass('checked'); } -function editComponent(e) { - e.preventDefault(); - $(this).closest('.xmodule_edit').addClass('editing').find('.component-editor').slideDown(150); -} - -function closeComponentEditor(e) { - e.preventDefault(); - $(this).closest('.xmodule_edit').removeClass('editing').find('.component-editor').slideUp(150); -} function showDateSetter(e) { e.preventDefault(); @@ -497,41 +356,7 @@ function hideAlert(e) { $(this).closest('.wrapper-alert').removeClass('is-shown'); } -function showToastMessage(message, $button, lifespan) { - var $toast = $('
'); - var $closeBtn = $('×'); - $toast.append($closeBtn); - var $content = $('
'); - $content.html(message); - $toast.append($content); - if ($button) { - $button.addClass('action-button'); - $button.bind('click', hideToastMessage); - $content.append($button); - } - $closeBtn.bind('click', hideToastMessage); - - if ($('.toast-notification')[0]) { - var targetY = $('.toast-notification').offset().top + $('.toast-notification').outerHeight(); - $toast.css('top', (targetY + 10) + 'px'); - } - - $body.prepend($toast); - $toast.fadeIn(200); - - if (lifespan) { - $toast.timer = setTimeout(function() { - $toast.fadeOut(300); - }, lifespan * 1000); - } -} - -function hideToastMessage(e) { - e.preventDefault(); - $(this).closest('.toast-notification').remove(); -} - -function addNewSection(e, isTemplate) { +function addNewSection(e) { e.preventDefault(); $(e.target).addClass('disabled'); @@ -542,19 +367,9 @@ function addNewSection(e, isTemplate) { $newSection.find('.new-section-name').focus().select(); $newSection.find('.section-name-form').bind('submit', saveNewSection); $cancelButton.bind('click', cancelNewSection); - $body.bind('keyup', { - $cancelButton: $cancelButton - }, checkForCancel); + BindCheckForCancel($cancelButton); } -function checkForCancel(e) { - if (e.which == 27) { - $body.unbind('keyup', checkForCancel); - e.data.$cancelButton.click(); - } -} - - function saveNewSection(e) { e.preventDefault(); @@ -571,7 +386,7 @@ function saveNewSection(e) { $.post('/create_item', { 'parent_location': parent, 'category': category, - 'display_name': display_name, + 'display_name': display_name }, function(data) { @@ -585,162 +400,6 @@ function cancelNewSection(e) { $(this).parents('section.new-section').remove(); } -function addNewCourse(e) { - e.preventDefault(); - $('.new-course-button').addClass('is-disabled'); - $('.new-course-save').addClass('is-disabled'); - var $newCourse = $('.wrapper-create-course').addClass('is-shown'); - var $cancelButton = $newCourse.find('.new-course-cancel'); - var $courseName = $('.new-course-name'); - $courseName.focus().select(); - $('.new-course-save').on('click', saveNewCourse); - $cancelButton.bind('click', cancelNewCourse); - $body.bind('keyup', { - $cancelButton: $cancelButton - }, checkForCancel); - - // Check that a course (org, number, run) doesn't use any special characters - var validateCourseItemEncoding = function(item) { - var required = validateRequiredField(item); - if(required) { - return required; - } - if(item !== encodeURIComponent(item)) { - return gettext('Please do not use any spaces or special characters in this field.'); - } - return ''; - }; - - // Ensure that org/course_num/run < 65 chars. - var validateTotalCourseItemsLength = function() { - var totalLength = _.reduce( - ['.new-course-org', '.new-course-number', '.new-course-run'], - function(sum, ele) { - return sum + $(ele).val().length; - }, 0 - ); - if(totalLength > 65) { - $('.wrap-error').addClass('is-shown'); - $('#course_creation_error').html('

' + gettext('The combined length of the organization, course number, and course run fields cannot be more than 65 characters.') + '

'); - $('.new-course-save').addClass('is-disabled'); - } - else { - $('.wrap-error').removeClass('is-shown'); - } - }; - - // Handle validation asynchronously - _.each( - ['.new-course-org', '.new-course-number', '.new-course-run'], - function(ele) { - var $ele = $(ele); - $ele.on('keyup', function(event) { - // Don't bother showing "required field" error when - // the user tabs into a new field; this is distracting - // and unnecessary - if(event.keyCode === 9) { - return; - } - var error = validateCourseItemEncoding($ele.val()); - setNewCourseFieldInErr($ele.parent('li'), error); - validateTotalCourseItemsLength(); - }); - } - ); - var $name = $('.new-course-name'); - $name.on('keyup', function() { - var error = validateRequiredField($name.val()); - setNewCourseFieldInErr($name.parent('li'), error); - validateTotalCourseItemsLength(); - }); -} - -function validateRequiredField(msg) { - return msg.length === 0 ? gettext('Required field.') : ''; -} - -function setNewCourseFieldInErr(el, msg) { - if(msg) { - el.addClass('error'); - el.children('span.tip-error').addClass('is-showing').removeClass('is-hiding').text(msg); - $('.new-course-save').addClass('is-disabled'); - } - else { - el.removeClass('error'); - el.children('span.tip-error').addClass('is-hiding').removeClass('is-showing'); - // One "error" div is always present, but hidden or shown - if($('.error').length === 1) { - $('.new-course-save').removeClass('is-disabled'); - } - } -}; - -function saveNewCourse(e) { - e.preventDefault(); - - // One final check for empty values - var errors = _.reduce( - ['.new-course-name', '.new-course-org', '.new-course-number', '.new-course-run'], - function(acc, ele) { - var $ele = $(ele); - var error = validateRequiredField($ele.val()); - setNewCourseFieldInErr($ele.parent('li'), error); - return error ? true : acc; - }, - false - ); - - if(errors) { - return; - } - - var $newCourseForm = $(this).closest('#create-course-form'); - var display_name = $newCourseForm.find('.new-course-name').val(); - var org = $newCourseForm.find('.new-course-org').val(); - var number = $newCourseForm.find('.new-course-number').val(); - var run = $newCourseForm.find('.new-course-run').val(); - - analytics.track('Created a Course', { - 'org': org, - 'number': number, - 'display_name': display_name, - 'run': run - }); - - $.post('/create_new_course', { - 'org': org, - 'number': number, - 'display_name': display_name, - 'run': run - }, - function(data) { - if (data.id !== undefined) { - window.location = '/' + data.id.replace(/.*:\/\//, ''); - } else if (data.ErrMsg !== undefined) { - $('.wrap-error').addClass('is-shown'); - $('#course_creation_error').html('

' + data.ErrMsg + '

'); - $('.new-course-save').addClass('is-disabled'); - } - } - ); -} - -function cancelNewCourse(e) { - e.preventDefault(); - $('.new-course-button').removeClass('is-disabled'); - $('.wrapper-create-course').removeClass('is-shown'); - // Clear out existing fields and errors - _.each( - ['.new-course-name', '.new-course-org', '.new-course-number', '.new-course-run'], - function(field) { - $(field).val(''); - } - ); - $('#course_creation_error').html(''); - $('.wrap-error').removeClass('is-shown'); - $('.new-course-save').off('click'); -} - function addNewSubsection(e) { e.preventDefault(); var $section = $(this).closest('.courseware-section'); @@ -758,9 +417,7 @@ function addNewSubsection(e) { $newSubsection.find('.new-subsection-form').bind('submit', saveNewSubsection); $cancelButton.bind('click', cancelNewSubsection); - $body.bind('keyup', { - $cancelButton: $cancelButton - }, checkForCancel); + BindCheckForCancel($cancelButton); } function saveNewSubsection(e) { @@ -806,65 +463,7 @@ function cancelSetSectionScheduleDate(e) { $(this).parent().siblings("h4").show(); } -function saveSetSectionScheduleDate(e) { - e.preventDefault(); - - var datetime = getDatetime( - $('.edit-subsection-publish-settings .start-date'), - $('.edit-subsection-publish-settings .start-time') - ); - - var id = $modal.attr('data-id'); - - analytics.track('Edited Section Release Date', { - 'course': course_location_analytics, - 'id': id, - 'start': datetime - }); - var saving = new NotificationView.Mini({ - title: gettext("Saving…") - }); - saving.show(); - // call into server to commit the new order - $.ajax({ - url: "/save_item", - type: "POST", - dataType: "json", - contentType: "application/json", - data: JSON.stringify({ - 'id': id, - 'metadata': { - 'start': datetime - } - }) - }).success(function() { - var pad2 = function(number) { - // pad a number to two places: useful for formatting months, days, hours, etc - // when displaying a date/time - return (number < 10 ? '0' : '') + number; - }; - - var $thisSection = $('.courseware-section[data-id="' + id + '"]'); - var html = _.template( - '' + - '' + gettext("Will Release:") + ' ' + - gettext("{month}/{day}/{year} at {hour}:{minute} UTC") + - '' + - '' + - gettext("Edit") + - '', - {year: datetime.getUTCFullYear(), month: pad2(datetime.getUTCMonth() + 1), day: pad2(datetime.getUTCDate()), - hour: pad2(datetime.getUTCHours()), minute: pad2(datetime.getUTCMinutes()), - id: id}, - {interpolate: /\{(.+?)\}/g}); - $thisSection.find('.section-published-date').html(html); - hideModal(); - saving.hide(); - }); -} - // Add to window object for unit test (overview_spec). - window.saveSetSectionScheduleDate = saveSetSectionScheduleDate; window.deleteSection = deleteSection; }); // end require() diff --git a/cms/static/js/index.js b/cms/static/js/index.js new file mode 100644 index 000000000000..f2d2fd3a9a51 --- /dev/null +++ b/cms/static/js/index.js @@ -0,0 +1,161 @@ +require(["domReady", "jquery", "underscore", "js/utils/check_for_cancel"], + function (domReady, $, _, BindCheckForCancel) { + var saveNewCourse = function (e) { + e.preventDefault(); + + // One final check for empty values + var errors = _.reduce( + ['.new-course-name', '.new-course-org', '.new-course-number', '.new-course-run'], + function (acc, ele) { + var $ele = $(ele); + var error = validateRequiredField($ele.val()); + setNewCourseFieldInErr($ele.parent('li'), error); + return error ? true : acc; + }, + false + ); + + if (errors) { + return; + } + + var $newCourseForm = $(this).closest('#create-course-form'); + var display_name = $newCourseForm.find('.new-course-name').val(); + var org = $newCourseForm.find('.new-course-org').val(); + var number = $newCourseForm.find('.new-course-number').val(); + var run = $newCourseForm.find('.new-course-run').val(); + + analytics.track('Created a Course', { + 'org': org, + 'number': number, + 'display_name': display_name, + 'run': run + }); + + $.post('/create_new_course', { + 'org': org, + 'number': number, + 'display_name': display_name, + 'run': run + }, + function (data) { + if (data.id !== undefined) { + window.location = '/' + data.id.replace(/.*:\/\//, ''); + } else if (data.ErrMsg !== undefined) { + $('.wrap-error').addClass('is-shown'); + $('#course_creation_error').html('

' + data.ErrMsg + '

'); + $('.new-course-save').addClass('is-disabled'); + } + } + ); + }; + + var cancelNewCourse = function (e) { + e.preventDefault(); + $('.new-course-button').removeClass('is-disabled'); + $('.wrapper-create-course').removeClass('is-shown'); + // Clear out existing fields and errors + _.each( + ['.new-course-name', '.new-course-org', '.new-course-number', '.new-course-run'], + function (field) { + $(field).val(''); + } + ); + $('#course_creation_error').html(''); + $('.wrap-error').removeClass('is-shown'); + $('.new-course-save').off('click'); + }; + + var addNewCourse = function (e) { + e.preventDefault(); + $('.new-course-button').addClass('is-disabled'); + $('.new-course-save').addClass('is-disabled'); + var $newCourse = $('.wrapper-create-course').addClass('is-shown'); + var $cancelButton = $newCourse.find('.new-course-cancel'); + var $courseName = $('.new-course-name'); + $courseName.focus().select(); + $('.new-course-save').on('click', saveNewCourse); + $cancelButton.bind('click', cancelNewCourse); + BindCheckForCancel($cancelButton); + + // Check that a course (org, number, run) doesn't use any special characters + var validateCourseItemEncoding = function (item) { + var required = validateRequiredField(item); + if (required) { + return required; + } + if (item !== encodeURIComponent(item)) { + return gettext('Please do not use any spaces or special characters in this field.'); + } + return ''; + }; + + // Ensure that org/course_num/run < 65 chars. + var validateTotalCourseItemsLength = function () { + var totalLength = _.reduce( + ['.new-course-org', '.new-course-number', '.new-course-run'], + function (sum, ele) { + return sum + $(ele).val().length; + }, 0 + ); + if (totalLength > 65) { + $('.wrap-error').addClass('is-shown'); + $('#course_creation_error').html('

' + gettext('The combined length of the organization, course number, and course run fields cannot be more than 65 characters.') + '

'); + $('.new-course-save').addClass('is-disabled'); + } + else { + $('.wrap-error').removeClass('is-shown'); + } + }; + + // Handle validation asynchronously + _.each( + ['.new-course-org', '.new-course-number', '.new-course-run'], + function (ele) { + var $ele = $(ele); + $ele.on('keyup', function (event) { + // Don't bother showing "required field" error when + // the user tabs into a new field; this is distracting + // and unnecessary + if (event.keyCode === 9) { + return; + } + var error = validateCourseItemEncoding($ele.val()); + setNewCourseFieldInErr($ele.parent('li'), error); + validateTotalCourseItemsLength(); + }); + } + ); + var $name = $('.new-course-name'); + $name.on('keyup', function () { + var error = validateRequiredField($name.val()); + setNewCourseFieldInErr($name.parent('li'), error); + validateTotalCourseItemsLength(); + }); + }; + + var validateRequiredField = function (msg) { + return msg.length === 0 ? gettext('Required field.') : ''; + }; + + var setNewCourseFieldInErr = function (el, msg) { + if(msg) { + el.addClass('error'); + el.children('span.tip-error').addClass('is-showing').removeClass('is-hiding').text(msg); + $('.new-course-save').addClass('is-disabled'); + } + else { + el.removeClass('error'); + el.children('span.tip-error').addClass('is-hiding').removeClass('is-showing'); + // One "error" div is always present, but hidden or shown + if($('.error').length === 1) { + $('.new-course-save').removeClass('is-disabled'); + } + } + }; + + + domReady(function () { + $('.new-course-button').bind('click', addNewCourse); + }); + }); \ No newline at end of file diff --git a/cms/static/js/sock.js b/cms/static/js/sock.js new file mode 100644 index 000000000000..03d366c97b27 --- /dev/null +++ b/cms/static/js/sock.js @@ -0,0 +1,32 @@ +require(["domReady", "jquery", "jquery.smoothScroll"], + function (domReady, $) { + var toggleSock = function (e) { + e.preventDefault(); + + var $btnLabel = $(this).find('.copy'); + var $sock = $('.wrapper-sock'); + var $sockContent = $sock.find('.wrapper-inner'); + + $sock.toggleClass('is-shown'); + $sockContent.toggle('fast'); + + $.smoothScroll({ + offset: -200, + easing: 'swing', + speed: 1000, + scrollElement: null, + scrollTarget: $sock + }); + + if ($sock.hasClass('is-shown')) { + $btnLabel.text(gettext('Hide Studio Help')); + } else { + $btnLabel.text(gettext('Looking for Help with Studio?')); + } + }; + + domReady(function () { + // toggling footer additional support + $('.cta-show-sock').bind('click', toggleSock); + }); + }); \ No newline at end of file diff --git a/cms/static/js/utils/check_for_cancel.js b/cms/static/js/utils/check_for_cancel.js new file mode 100644 index 000000000000..bb8ca86227f5 --- /dev/null +++ b/cms/static/js/utils/check_for_cancel.js @@ -0,0 +1,17 @@ +define(["jquery"], function($) { + var $body = $('body'); + var checkForCancel = function (e) { + if (e.which == 27) { + $body.unbind('keyup', checkForCancel); + e.data.$cancelButton.click(); + } + }; + + var bindCheckForCancel = function (cancelButton) { + $body.bind('keyup', { + $cancelButton: cancelButton + }, checkForCancel); + }; + + return bindCheckForCancel; +}); \ No newline at end of file diff --git a/cms/static/js/utils/get_date.js b/cms/static/js/utils/get_date.js new file mode 100644 index 000000000000..795441585c6d --- /dev/null +++ b/cms/static/js/utils/get_date.js @@ -0,0 +1,18 @@ +define(["jquery"], function($) { + var getDate = function (datepickerInput, timepickerInput) { + // given a pair of inputs (datepicker and timepicker), return a JS Date + // object that corresponds to the datetime.js that they represent. Assume + // UTC timezone, NOT the timezone of the user's browser. + var date = $(datepickerInput).datepicker("getDate"); + var time = $(timepickerInput).timepicker("getTime"); + if(date && time) { + return new Date(Date.UTC( + date.getFullYear(), date.getMonth(), date.getDate(), + time.getHours(), time.getMinutes() + )); + } else { + return null; + } + }; + return getDate; +}); \ No newline at end of file diff --git a/cms/static/js/utils/modal.js b/cms/static/js/utils/modal.js new file mode 100644 index 000000000000..617dc4988dd1 --- /dev/null +++ b/cms/static/js/utils/modal.js @@ -0,0 +1,81 @@ +define(["jquery"], function($) { + /** + * Hides the modal and modal cover, using the standard selectors. + * Note though that the class "is-fixed" on the modal cover + * prevents the closing operation. + */ + var hideModal = function(e) { + if (e) { + e.preventDefault(); + } + + var $modalCover = getModalCover(); + + // Unit editors do not want the modal cover to hide when users click outside + // of the editor. Users must press Cancel or Save to exit the editor. + // module_edit adds and removes the "is-fixed" class. + if (!$modalCover.hasClass("is-fixed")) { + getModal().hide(); + hideModalCover($modalCover); + } + }; + + /** + * Hides just the modal cover. Caller can pass in a specific + * element as the modal cover, otherwise the standard selector will be used. + * + * This method also unbinds the click handler on the modal cover. + */ + var hideModalCover = function (modalCover) { + if (modalCover == undefined) { + modalCover = getModalCover(); + } + modalCover.hide(); + modalCover.removeClass("is-fixed"); + modalCover.unbind('click'); + }; + + /** + * Shows the modal and modal cover, using the standard selectors. + */ + var showModal = function () { + getModal().show(); + showModalCover(); + }; + + /** + * Shows just the modal cover. The caller can optionally choose + * to have the class "is-fixed" added to the cover, and + * the user can also choose to specify a custom click handler + * for the modal cover. + * + * This method returns the modal cover element. + */ + var showModalCover = function(addFixed, clickHandler) { + var $modalCover = getModalCover(); + $modalCover.show(); + if (addFixed) { + $modalCover.addClass("is-fixed"); + } + if (clickHandler) { + $modalCover.bind('click', clickHandler); + } + else { + $modalCover.bind('click', hideModal); + } + return $modalCover; + }; + + var getModalCover = function () { + return $('.modal-cover'); + }; + + var getModal = function () { + return $(".modal, .showAsModal"); + }; + + return { + showModal: showModal, hideModal: hideModal, + showModalCover: showModalCover, hideModalCover: hideModalCover + }; +}); \ No newline at end of file diff --git a/cms/static/js/views/course_info_handout.js b/cms/static/js/views/course_info_handout.js index d22de768b404..95e5a7c76753 100644 --- a/cms/static/js/views/course_info_handout.js +++ b/cms/static/js/views/course_info_handout.js @@ -1,7 +1,6 @@ -define(["backbone", "underscore", "codemirror", "js/views/feedback_notification", "js/views/course_info_helper"], - function(Backbone, _, CodeMirror, NotificationView, CourseInfoHelper) { +define(["backbone", "underscore", "codemirror", "js/views/feedback_notification", "js/views/course_info_helper", "js/utils/modal"], + function(Backbone, _, CodeMirror, NotificationView, CourseInfoHelper, ModalUtils) { - var $modalCover = $(".modal-cover"); // the handouts view is dumb right now; it needs tied to a model and all that jazz var CourseInfoHandoutsView = Backbone.View.extend({ // collection is CourseUpdateCollection @@ -47,10 +46,7 @@ define(["backbone", "underscore", "codemirror", "js/views/feedback_notification" this.$codeMirror = CourseInfoHelper.editWithCodeMirror( self.model, 'data', self.options['base_asset_url'], this.$editor.get(0)); - $modalCover.show(); - $modalCover.bind('click', function() { - self.closeEditor(); - }); + this.$modalCover = ModalUtils.showModalCover(false, function() { self.closeEditor() }); }, onSave: function(event) { @@ -81,8 +77,7 @@ define(["backbone", "underscore", "codemirror", "js/views/feedback_notification" closeEditor: function() { this.$form.hide(); - $modalCover.unbind('click'); - $modalCover.hide(); + ModalUtils.hideModalCover(this.$modalCover); this.$form.find('.CodeMirror').remove(); this.$codeMirror = null; } diff --git a/cms/static/js/views/course_info_update.js b/cms/static/js/views/course_info_update.js index 256f63624a5d..b361c522ffe5 100644 --- a/cms/static/js/views/course_info_update.js +++ b/cms/static/js/views/course_info_update.js @@ -1,6 +1,6 @@ define(["backbone", "underscore", "codemirror", "js/models/course_update", - "js/views/feedback_prompt", "js/views/feedback_notification", "js/views/course_info_helper"], - function(Backbone, _, CodeMirror, CourseUpdateModel, PromptView, NotificationView, CourseInfoHelper) { + "js/views/feedback_prompt", "js/views/feedback_notification", "js/views/course_info_helper", "js/utils/modal"], + function(Backbone, _, CodeMirror, CourseUpdateModel, PromptView, NotificationView, CourseInfoHelper, ModalUtils) { var CourseInfoUpdateView = Backbone.View.extend({ // collection is CourseUpdateCollection @@ -17,8 +17,6 @@ define(["backbone", "underscore", "codemirror", "js/models/course_update", this.render(); // when the client refetches the updates as a whole, re-render them this.listenTo(this.collection, 'reset', this.render); - - this.$modalCover = $(".modal-cover"); }, render: function () { @@ -64,9 +62,8 @@ define(["backbone", "underscore", "codemirror", "js/models/course_update", $newForm.addClass('editing'); this.$currentPost = $newForm.closest('li'); - this.$modalCover.show(); - this.$modalCover.bind('click', function() { - self.closeEditor(true); + this.$modalCover = ModalUtils.showModalCover(false, function() { + self.closeEditor(true) }); $('.date').datepicker('destroy'); @@ -91,7 +88,7 @@ define(["backbone", "underscore", "codemirror", "js/models/course_update", ele.remove(); } }); - this.closeEditor(); + this.closeEditor(false); analytics.track('Saved Course Update', { 'course': course_location_analytics, @@ -121,10 +118,11 @@ define(["backbone", "underscore", "codemirror", "js/models/course_update", this.$codeMirror = CourseInfoHelper.editWithCodeMirror( targetModel, 'content', self.options['base_asset_url'], $textArea.get(0)); - this.$modalCover.show(); - this.$modalCover.bind('click', function() { - self.closeEditor(false); - }); + this.$modalCover = ModalUtils.showModalCover(false, + function() { + self.closeEditor(false) + } + ); }, onDelete: function(event) { @@ -198,8 +196,7 @@ define(["backbone", "underscore", "codemirror", "js/models/course_update", this.$currentPost.find('.CodeMirror').remove(); } - this.$modalCover.unbind('click'); - this.$modalCover.hide(); + ModalUtils.hideModalCover(this.$modalCover); this.$codeMirror = null; }, diff --git a/cms/static/js/views/import.js b/cms/static/js/views/import.js index 14d2c6a22e60..238e953a92a5 100644 --- a/cms/static/js/views/import.js +++ b/cms/static/js/views/import.js @@ -2,8 +2,8 @@ * Course import-related js. */ define( - ["jquery", "underscore", "gettext"], - function($, _, gettext) { + ["domReady", "jquery", "underscore", "gettext"], + function(domReady, $, _, gettext) { "use strict"; @@ -146,5 +146,28 @@ var CourseImport = { }; +var showImportSubmit = function (e) { + var filepath = $(this).val(); + if (filepath.substr(filepath.length - 6, 6) == 'tar.gz') { + $('.error-block').hide(); + $('.file-name').html($(this).val().replace('C:\\fakepath\\', '')); + $('.file-name-block').show(); + $('.view-import .choose-file-button').hide(); + $('.submit-button').show(); + $('.progress').show(); + } else { + $('.error-block').html(gettext('File format not supported. Please upload a file with a tar.gz extension.')).show(); + } +}; + +domReady(function () { + // import form setup + $('.view-import .file-input').bind('change', showImportSubmit); + $('.view-import .choose-file-button, .view-import .choose-file-button-inline').bind('click', function (e) { + e.preventDefault(); + $('.view-import .file-input').click(); + }); +}); + return CourseImport; }); diff --git a/cms/static/js/views/overview.js b/cms/static/js/views/overview.js index 6e18dfdee266..dc095dbcb0c4 100644 --- a/cms/static/js/views/overview.js +++ b/cms/static/js/views/overview.js @@ -1,5 +1,106 @@ -define(["domReady", "jquery", "jquery.ui", "gettext", "js/views/feedback_notification", "draggabilly"], - function (domReady, $, ui, gettext, NotificationView, Draggabilly) { +define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/feedback_notification", "draggabilly", "js/utils/modal", "js/utils/get_date"], + function (domReady, $, ui, _, gettext, NotificationView, Draggabilly, ModalUtils, DateUtils) { + + var modalSelector = '.edit-subsection-publish-settings'; + + var toggleSections = function(e) { + e.preventDefault(); + + var $section = $('.courseware-section'); + var $button = $(this); + var $labelCollapsed = $(' ' + + gettext('Collapse All Sections') + ''); + var $labelExpanded = $(' ' + + gettext('Expand All Sections') + ''); + + var buttonLabel = $button.hasClass('is-activated') ? $labelCollapsed : $labelExpanded; + $button.toggleClass('is-activated').html(buttonLabel); + + if ($button.hasClass('is-activated')) { + $section.addClass('collapsed'); + // first child in order to avoid the icons on the subsection lists which are not in the first child + $section.find('header .expand-collapse-icon').removeClass('collapse').addClass('expand'); + } else { + $section.removeClass('collapsed'); + // first child in order to avoid the icons on the subsection lists which are not in the first child + $section.find('header .expand-collapse-icon').removeClass('expand').addClass('collapse'); + } + }; + + + var editSectionPublishDate = function (e) { + e.preventDefault(); + var $modal = $(modalSelector); + $modal.attr('data-id', $(this).attr('data-id')); + $modal.find('.start-date').val($(this).attr('data-date')); + $modal.find('.start-time').val($(this).attr('data-time')); + if ($modal.find('.start-date').val() == '' && $modal.find('.start-time').val() == '') { + $modal.find('.save-button').hide(); + } + $modal.find('.section-name').html('"' + $(this).closest('.courseware-section').find('.section-name-span').text() + '"'); + ModalUtils.showModal(); + }; + + function saveSetSectionScheduleDate(e) { + e.preventDefault(); + + var datetime = DateUtils( + $('.edit-subsection-publish-settings .start-date'), + $('.edit-subsection-publish-settings .start-time') + ); + + var id = $(modalSelector).attr('data-id'); + + analytics.track('Edited Section Release Date', { + 'course': course_location_analytics, + 'id': id, + 'start': datetime + }); + + var saving = new NotificationView.Mini({ + title: gettext("Saving…") + }); + saving.show(); + // call into server to commit the new order + $.ajax({ + url: "/save_item", + type: "POST", + dataType: "json", + contentType: "application/json", + data: JSON.stringify({ + 'id': id, + 'metadata': { + 'start': datetime + } + }) + }).success(function() { + var pad2 = function(number) { + // pad a number to two places: useful for formatting months, days, hours, etc + // when displaying a date/time + return (number < 10 ? '0' : '') + number; + }; + + var $thisSection = $('.courseware-section[data-id="' + id + '"]'); + var html = _.template( + '' + + '' + gettext("Will Release:") + ' ' + + gettext("{month}/{day}/{year} at {hour}:{minute} UTC") + + '' + + '' + + gettext("Edit") + + '', + {year: datetime.getUTCFullYear(), month: pad2(datetime.getUTCMonth() + 1), day: pad2(datetime.getUTCDate()), + hour: pad2(datetime.getUTCHours()), minute: pad2(datetime.getUTCMinutes()), + id: id}, + {interpolate: /\{(.+?)\}/g}); + $thisSection.find('.section-published-date').html(html); + ModalUtils.hideModal(); + saving.hide(); + }); + } + // Add to window object for unit test (overview_spec). + window.saveSetSectionScheduleDate = saveSetSectionScheduleDate; + var overviewDragger = { droppableClasses: 'drop-target drop-target-prepend drop-target-before drop-target-after', @@ -295,6 +396,13 @@ define(["domReady", "jquery", "jquery.ui", "gettext", "js/views/feedback_notific }; domReady(function() { + $('.toggle-button-sections').bind('click', toggleSections); + var $body = $('body'); + $body.on('click', '.section-published-date .edit-button', editSectionPublishDate); + $body.on('click', '.section-published-date .schedule-button', editSectionPublishDate); + $body.on('click', '.edit-subsection-publish-settings .save-button', saveSetSectionScheduleDate); + $body.on('click', '.edit-subsection-publish-settings .cancel-button', ModalUtils.hideModal); + // Section overviewDragger.makeDraggable( '.courseware-section', diff --git a/cms/templates/asset_index.html b/cms/templates/asset_index.html index 9301e857f217..909fc73c5e9f 100644 --- a/cms/templates/asset_index.html +++ b/cms/templates/asset_index.html @@ -18,29 +18,25 @@ + <%include file="widgets/sock.html" /> % endif - <%include file="widgets/footer.html" /> +<%include file="widgets/footer.html" /> <%include file="widgets/tender.html" />
diff --git a/cms/templates/index.html b/cms/templates/index.html index df8459e448c5..e6d146355550 100644 --- a/cms/templates/index.html +++ b/cms/templates/index.html @@ -7,7 +7,7 @@ <%block name="jsextra">