From fbc886aab913ca1f26a89eca78de4d733d536b88 Mon Sep 17 00:00:00 2001 From: cahrens Date: Sun, 3 Aug 2014 22:09:15 -0400 Subject: [PATCH] Store the expanded locators on the page. STUD-2038 --- cms/static/js/utils/drag_and_drop.js | 6 +- cms/static/js/views/course_outline.js | 45 ++++++----- cms/static/js/views/pages/course_outline.js | 85 ++++++++++++++++++++- cms/static/js/views/xblock_outline.js | 25 +++++- 4 files changed, 133 insertions(+), 28 deletions(-) diff --git a/cms/static/js/utils/drag_and_drop.js b/cms/static/js/utils/drag_and_drop.js index 37911a95da..69d62fcdf9 100644 --- a/cms/static/js/utils/drag_and_drop.js +++ b/cms/static/js/utils/drag_and_drop.js @@ -277,7 +277,11 @@ define(["jquery", "jquery.ui", "underscore", "gettext", "js/views/feedback_notif refreshParent = function (element) { var refresh = element.data('refresh'); - if (_.isFunction(refresh)) { refresh(); } + // If drop was into a collapsed parent, the parent will have been + // expanded. Views using this class may need to track the + // collapse/expand state, so send it with the refresh callback. + var collapsed = element.hasClass(this.collapsedClass); + if (_.isFunction(refresh)) { refresh(collapsed); } }; // If the parent has changed, update the children of the old parent. diff --git a/cms/static/js/views/course_outline.js b/cms/static/js/views/course_outline.js index 22b222b50b..0faf546ec6 100644 --- a/cms/static/js/views/course_outline.js +++ b/cms/static/js/views/course_outline.js @@ -24,12 +24,7 @@ define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_ }, shouldExpandChildren: function() { - // Expand the children if this xblock's locator is in the initially expanded state - if (this.initialState && _.contains(this.initialState.expanded_locators, this.model.id)) { - return true; - } - // Only expand the course and its chapters (aka sections) initially - return this.model.isCourse() || this.model.isChapter(); + return this.expandedLocators.contains(this.model.get('id')); }, shouldRenderChildren: function() { @@ -42,22 +37,12 @@ define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_ model: xblockInfo, parentInfo: parentInfo, initialState: this.initialState, + expandedLocators: this.expandedLocators, template: this.template, parentView: parentView || this }); }, - getExpandedLocators: function() { - var expandedLocators = []; - this.$('.outline-item.is-collapsible').each(function(index, rawElement) { - var element = $(rawElement); - if (!element.hasClass('is-collapsed')) { - expandedLocators.push(element.data('locator')); - } - }); - return expandedLocators; - }, - /** * 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 @@ -76,13 +61,26 @@ define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_ }; view = getViewToRefresh(this); - expandedLocators = view.getExpandedLocators(); viewState = viewState || {}; - viewState.expanded_locators = expandedLocators.concat(viewState.expanded_locators || []); 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 @@ -113,6 +111,7 @@ define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_ sectionInfo.fetch().done(function() { sectionView = self.createChildView(sectionInfo, self.model, self); sectionView.initialState = initialState; + sectionView.expandedLocators = self.expandedLocators; sectionView.render(); self.addChildView(sectionView); sectionView.setViewState(initialState); @@ -136,10 +135,10 @@ define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_ }, createNewItemViewState: function(locator, scrollOffset) { + this.expandedLocators.add(locator); return { locator_to_show: locator, edit_display_name: true, - expanded_locators: [ locator ], scroll_offset: scrollOffset || 0 }; }, @@ -168,7 +167,7 @@ define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_ handleClass: '.section-drag-handle', droppableClass: 'ol.list-sections', parentLocationSelector: 'article.outline', - refresh: this.refresh.bind(this) + refresh: this.refreshWithCollapsedState.bind(this) }); } else if ($(element).hasClass("outline-subsection")) { @@ -177,7 +176,7 @@ define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_ handleClass: '.subsection-drag-handle', droppableClass: 'ol.list-subsections', parentLocationSelector: 'li.outline-section', - refresh: this.refresh.bind(this) + refresh: this.refreshWithCollapsedState.bind(this) }); } else if ($(element).hasClass("outline-unit")) { @@ -186,7 +185,7 @@ define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_ handleClass: '.unit-drag-handle', droppableClass: 'ol.list-units', parentLocationSelector: 'li.outline-subsection', - refresh: this.refresh.bind(this) + refresh: this.refreshWithCollapsedState.bind(this) }); } } diff --git a/cms/static/js/views/pages/course_outline.js b/cms/static/js/views/pages/course_outline.js index 58c0d263d5..a227642945 100644 --- a/cms/static/js/views/pages/course_outline.js +++ b/cms/static/js/views/pages/course_outline.js @@ -4,7 +4,9 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views/utils/xblock_utils", "js/views/course_outline"], function ($, _, gettext, BasePage, XBlockViewUtils, CourseOutlineView) { - var CourseOutlinePage = BasePage.extend({ + var expandedLocators, CourseOutlinePage; + + CourseOutlinePage = BasePage.extend({ // takes XBlockInfo as a model events: { @@ -36,12 +38,32 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views }, renderPage: function() { + var setInitialExpandState = function (xblockInfo, expandedLocators) { + if (xblockInfo.isCourse() || xblockInfo.isChapter()) { + expandedLocators.add(xblockInfo.get('id')); + } + }; + this.setCollapseExpandVisibility(); + this.expandedLocators = expandedLocators; + this.expandedLocators.clear(); + if (this.model.get('child_info')) { + _.each(this.model.get('child_info').children, function (childXBlockInfo) { + setInitialExpandState(childXBlockInfo, this.expandedLocators); + }, this); + } + setInitialExpandState(this.model, this.expandedLocators); + + if (this.initialState && this.initialState.expanded_locators) { + this.expandedLocators.addAll(this.initialState.expanded_locators); + } + this.outlineView = new CourseOutlineView({ el: this.$('.outline'), model: this.model, isRoot: true, - initialState: this.initialState + initialState: this.initialState, + expandedLocators: this.expandedLocators }); this.outlineView.render(); this.outlineView.setViewState(this.initialState || {}); @@ -65,8 +87,67 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views element.removeClass('is-collapsed'); } }); + if (this.model.get('child_info')) { + _.each(this.model.get('child_info').children, function (childXBlockInfo) { + if (collapse) { + this.expandedLocators.remove(childXBlockInfo.get('id')); + } + else { + this.expandedLocators.add(childXBlockInfo.get('id')); + } + }, this); + } } }); + /** + * Represents the set of locators that should be expanded for the page. + */ + expandedLocators = { + locators: [], + + /** + * Add the locator to the set if it is not already present. + */ + add: function (locator) { + if (!this.contains(locator)) { + this.locators.push(locator); + } + }, + + /** + * Accepts an array of locators and adds them all to the set if not already present. + */ + addAll: function(locators) { + _.each(locators, function(locator) { + this.add(locator); + }, this); + }, + + /** + * Remove the locator from the set if it is present. + */ + remove: function (locator) { + var index = this.locators.indexOf(locator); + if (index >= 0) { + this.locators.splice(index, 1); + } + }, + + /** + * Returns true iff the locator is present in the set. + */ + contains: function (locator) { + return this.locators.indexOf(locator) >= 0; + }, + + /** + * Clears all expanded locators from the set. + */ + clear: function () { + this.locators = []; + } + }; + return CourseOutlinePage; }); // end define(); diff --git a/cms/static/js/views/xblock_outline.js b/cms/static/js/views/xblock_outline.js index 7f0a86b827..15fafffb86 100644 --- a/cms/static/js/views/xblock_outline.js +++ b/cms/static/js/views/xblock_outline.js @@ -9,7 +9,6 @@ * * The view can be constructed with an initialState option which is a JSON structure representing * the desired initial state. The parameters are as follows: - * - expanded_locators - the locators that should be shown as expanded in addition to the defaults * - locator_to_show - the locator for the xblock which is the one being explicitly shown * - scroll_offset - the scroll offset to use for the locator being shown * - edit_display_name - true if the shown xblock's display name should be in inline edit mode @@ -30,6 +29,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/ initialize: function() { BaseView.prototype.initialize.call(this); this.initialState = this.options.initialState; + this.expandedLocators = this.options.expandedLocators; this.template = this.options.template; if (!this.template) { this.template = this.loadTemplate(this.templateName); @@ -37,7 +37,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/ this.parentInfo = this.options.parentInfo; this.parentView = this.options.parentView; this.renderedChildren = false; - this.model.on('sync', this.onXBlockChange, this); + this.model.on('sync', this.onSync, this); }, render: function() { @@ -47,6 +47,9 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/ if (this.shouldRenderChildren() && this.shouldExpandChildren()) { this.renderChildren(); } + else { + this.renderedChildren = false; + } return this; }, @@ -132,6 +135,17 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/ }, toggleExpandCollapse: function(event) { + // The course outline page tracks expanded locators. The unit location sidebar does not. + if (this.expandedLocators) { + var locator = this.model.get('id'); + var wasExpanded = this.expandedLocators.contains(locator); + if (wasExpanded) { + this.expandedLocators.remove(locator); + } + else { + this.expandedLocators.add(locator); + } + } // Ensure that the children have been rendered before expanding if (this.shouldRenderChildren() && !this.renderedChildren) { this.renderChildren(); @@ -164,6 +178,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/ model: xblockInfo, parentInfo: parentInfo, initialState: this.initialState, + expandedLocators: this.expandedLocators, template: this.template, parentView: parentView || this }); @@ -181,6 +196,12 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/ return xblockType; }, + onSync: function(event) { + if (ViewUtils.hasChangedAttributes(this.model, ['visibility_state', 'child_info', 'display_name'])) { + this.onXBlockChange(); + } + }, + onXBlockChange: function() { var oldElement = this.$el, viewState = this.initialState;