/** * The CourseOutlineView is used to render the contents of the course for the Course Outline page. * It is a recursive set of views, where each XBlock has its own instance, and each of the children * are shown as child CourseOutlineViews. * * This class extends XBlockOutlineView to add unique capabilities needed by the course outline: * - sections are initially expanded but subsections and other children are shown as collapsed * - changes cause a refresh of the entire section rather than just the view for the changed xblock * - adding units will automatically redirect to the unit page rather than showing them inline */ define(['jquery', 'underscore', 'js/views/xblock_outline', 'common/js/components/utils/view_utils', 'js/views/utils/xblock_utils', 'js/models/xblock_outline_info', 'js/views/modals/course_outline_modals', 'js/utils/drag_and_drop'], function( $, _, XBlockOutlineView, ViewUtils, XBlockViewUtils, XBlockOutlineInfo, CourseOutlineModalsFactory, ContentDragger ) { var CourseOutlineView = XBlockOutlineView.extend({ // takes XBlockOutlineInfo as a model templateName: 'course-outline', render: function() { var renderResult = XBlockOutlineView.prototype.render.call(this); this.makeContentDraggable(this.el); return renderResult; }, shouldExpandChildren: function() { return this.expandedLocators.contains(this.model.get('id')); }, shouldRenderChildren: function() { // Render all nodes up to verticals but not below return !this.model.isVertical(); }, getChildViewClass: function() { return CourseOutlineView; }, /** * Refresh the containing section (if there is one) or else refresh the entire course. * Note that the refresh will preserve the expanded state of this view and all of its * children. * @param viewState The desired initial state of the view, or null if none. * @returns {jQuery promise} A promise representing the refresh operation. */ refresh: function(viewState) { var getViewToRefresh, view, expandedLocators; getViewToRefresh = function(view) { if (view.model.isChapter() || !view.parentView) { return view; } return getViewToRefresh(view.parentView); }; view = getViewToRefresh(this); viewState = viewState || {}; view.initialState = viewState; return view.model.fetch({}); }, /** * Updates the collapse/expand state for this outline element, and then calls refresh. * @param isCollapsed true if the element should be collapsed, else false */ refreshWithCollapsedState: function(isCollapsed) { var locator = this.model.get('id'); if (isCollapsed) { this.expandedLocators.remove(locator); } else { this.expandedLocators.add(locator); } this.refresh(); }, onChildAdded: function(locator, category, event) { if (category === 'vertical') { // For units, redirect to the new unit's page in inline edit mode this.onUnitAdded(locator); } else if (category === 'chapter' && this.model.hasChildren()) { this.onSectionAdded(locator); } else { // For all other block types, refresh the view and do the following: // - show the new block expanded // - ensure it is scrolled into view // - make its name editable this.refresh(this.createNewItemViewState(locator, ViewUtils.getScrollOffset($(event.target)))); } }, /** * Perform specific actions for duplicated xblock. * @param {String} locator The locator of the new duplicated xblock. * @param {String} xblockType The front-end terminology of the xblock category. * @param {jquery Element} xblockElement The xblock element to be duplicated. */ onChildDuplicated: function(locator, xblockType, xblockElement) { var scrollOffset = ViewUtils.getScrollOffset(xblockElement); if (xblockType === 'section') { this.onSectionAdded(locator, xblockElement, scrollOffset); } else { // For all other block types, refresh the view and do the following: // - show the new block expanded // - ensure it is scrolled into view // - make its name editable this.refresh(this.createNewItemViewState(locator, scrollOffset)); } }, onSectionAdded: function(locator, xblockElement, scrollOffset) { var self = this, initialState = self.createNewItemViewState(locator, scrollOffset), sectionInfo, sectionView; // For new chapters in a non-empty view, add a new child view and render it // to avoid the expense of refreshing the entire page. if (this.model.hasChildren()) { sectionInfo = new XBlockOutlineInfo({ id: locator, category: 'chapter' }); // Fetch the full xblock info for the section and then create a view for it sectionInfo.fetch().done(function() { sectionView = self.createChildView(sectionInfo, self.model, {parentView: self}); sectionView.initialState = initialState; sectionView.expandedLocators = self.expandedLocators; sectionView.render(); self.addChildView(sectionView, xblockElement); sectionView.setViewState(initialState); }); } else { this.refresh(initialState); } }, onChildDeleted: function(childView) { var xblockInfo = this.model, children = xblockInfo.get('child_info') && xblockInfo.get('child_info').children; // If deleting a section that isn't the final one, just remove it for efficiency // as it cannot visually effect the other sections. if (childView.model.isChapter() && children && children.length > 1) { childView.$el.remove(); children.splice(children.indexOf(childView.model), 1); } else { this.refresh(); } }, createNewItemViewState: function(locator, scrollOffset) { this.expandedLocators.add(locator); return { locator_to_show: locator, edit_display_name: true, scroll_offset: scrollOffset || 0 }; }, editXBlock: function() { var modal; var enableProctoredExams = false; var enableTimedExams = false; if (this.model.get('category') === 'sequential') { if (this.parentView.parentView.model.has('enable_proctored_exams')) { enableProctoredExams = this.parentView.parentView.model.get('enable_proctored_exams'); } if (this.parentView.parentView.model.has('enable_timed_exams')) { enableTimedExams = this.parentView.parentView.model.get('enable_timed_exams'); } } modal = CourseOutlineModalsFactory.getModal('edit', this.model, { onSave: this.refresh.bind(this), parentInfo: this.parentInfo, enable_proctored_exams: enableProctoredExams, enable_timed_exams: enableTimedExams, xblockType: XBlockViewUtils.getXBlockType( this.model.get('category'), this.parentView.model, true ) }); if (modal) { modal.show(); } }, publishXBlock: function() { var modal = CourseOutlineModalsFactory.getModal('publish', this.model, { onSave: this.refresh.bind(this), xblockType: XBlockViewUtils.getXBlockType( this.model.get('category'), this.parentView.model, true ) }); if (modal) { modal.show(); } }, highlightsXBlock: function() { var modal = CourseOutlineModalsFactory.getModal('highlights', this.model, { onSave: this.refresh.bind(this), xblockType: XBlockViewUtils.getXBlockType( this.model.get('category'), this.parentView.model, true ) }); if (modal) { window.analytics.track('edx.bi.highlights.modal_open'); modal.show(); } }, addButtonActions: function(element) { XBlockOutlineView.prototype.addButtonActions.apply(this, arguments); element.find('.configure-button').click(function(event) { event.preventDefault(); this.editXBlock(); }.bind(this)); element.find('.publish-button').click(function(event) { event.preventDefault(); this.publishXBlock(); }.bind(this)); element.find('.highlights-button').on('click keydown', function(event) { if (event.type === 'click' || event.which === 13 || event.which === 32) { event.preventDefault(); this.highlightsXBlock(); } }.bind(this)); }, makeContentDraggable: function(element) { if ($(element).hasClass('outline-section')) { ContentDragger.makeDraggable(element, { type: '.outline-section', handleClass: '.section-drag-handle', droppableClass: 'ol.list-sections', parentLocationSelector: 'article.outline', refresh: this.refreshWithCollapsedState.bind(this), ensureChildrenRendered: this.ensureChildrenRendered.bind(this) }); } else if ($(element).hasClass('outline-subsection')) { ContentDragger.makeDraggable(element, { type: '.outline-subsection', handleClass: '.subsection-drag-handle', droppableClass: 'ol.list-subsections', parentLocationSelector: 'li.outline-section', refresh: this.refreshWithCollapsedState.bind(this), ensureChildrenRendered: this.ensureChildrenRendered.bind(this) }); } else if ($(element).hasClass('outline-unit')) { ContentDragger.makeDraggable(element, { type: '.outline-unit', handleClass: '.unit-drag-handle', droppableClass: 'ol.list-units', parentLocationSelector: 'li.outline-subsection', refresh: this.refreshWithCollapsedState.bind(this), ensureChildrenRendered: this.ensureChildrenRendered.bind(this) }); } } }); return CourseOutlineView; }); // end define();