Files
edx-platform/cms/static/js/views/xblock_outline.js
Eric Fischer 5bc6b31e29 eslint --fix
2017-12-08 14:38:41 -05:00

357 lines
16 KiB
JavaScript

/**
* The XBlockOutlineView is used to render an xblock and its children based upon the information
* provided in the XBlockInfo model. It is a recursive set of views where each XBlock has its own instance.
*
* The class provides several opportunities to override the default behavior in subclasses:
* - shouldRenderChildren defaults to true meaning that the view should also create child views
* - shouldExpandChildren defaults to true meaning that the view should show itself as expanded
* - refresh is called when a server change has been made and the view needs to be refreshed
*
* The view can be constructed with an initialState option which is a JSON structure representing
* the desired initial state. The parameters are as follows:
* - 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
*/
define(['jquery', 'underscore', 'gettext', 'js/views/baseview', 'common/js/components/utils/view_utils',
'js/views/utils/xblock_utils', 'js/views/xblock_string_field_editor',
'edx-ui-toolkit/js/utils/string-utils', 'edx-ui-toolkit/js/utils/html-utils'],
function($, _, gettext, BaseView, ViewUtils, XBlockViewUtils, XBlockStringFieldEditor, StringUtils, HtmlUtils) {
'use strict';
var XBlockOutlineView = BaseView.extend({
// takes XBlockInfo as a model
options: {
collapsedClass: 'is-collapsed'
},
templateName: 'xblock-outline',
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);
}
this.parentInfo = this.options.parentInfo;
this.parentView = this.options.parentView;
this.renderedChildren = false;
this.model.on('sync', this.onSync, this);
},
render: function() {
this.renderTemplate();
this.addButtonActions(this.$el);
this.addNameEditor();
// For cases in which we need to suppress the header controls during rendering, we'll
// need to add the current model's id/locator to the set of expanded locators
if (this.model.get('is_header_visible') !== null && !this.model.get('is_header_visible')) {
var locator = this.model.get('id');
if (!_.isUndefined(this.expandedLocators) && !this.expandedLocators.contains(locator)) {
this.expandedLocators.add(locator);
this.refresh();
}
}
if (this.shouldRenderChildren() && this.shouldExpandChildren()) {
this.renderChildren();
} else {
this.renderedChildren = false;
}
return this;
},
renderTemplate: function() {
var html = this.template(this.getTemplateContext());
if (this.parentInfo) {
this.setElement($(html));
} else {
HtmlUtils.setHtml(
this.$el,
HtmlUtils.HTML(html)
);
}
},
getTemplateContext: function() {
var xblockInfo = this.model,
childInfo = xblockInfo.get('child_info'),
parentInfo = this.parentInfo,
xblockType = XBlockViewUtils.getXBlockType(this.model.get('category'), this.parentInfo),
xblockTypeDisplayName = XBlockViewUtils.getXBlockType(this.model.get('category'), this.parentInfo, true),
parentType = parentInfo ? XBlockViewUtils.getXBlockType(parentInfo.get('category')) : null,
addChildName = null,
defaultNewChildName = null,
isCollapsed = this.shouldRenderChildren() && !this.shouldExpandChildren();
if (childInfo) {
addChildName = StringUtils.interpolate(gettext('New {component_type}'), {
component_type: childInfo.display_name
}, true);
defaultNewChildName = childInfo.display_name;
}
/* globals course */
return {
xblockInfo: xblockInfo,
visibilityClass: XBlockViewUtils.getXBlockVisibilityClass(xblockInfo.get('visibility_state')),
typeListClass: XBlockViewUtils.getXBlockListTypeClass(xblockType),
parentInfo: this.parentInfo,
xblockType: xblockType,
xblockTypeDisplayName: xblockTypeDisplayName,
parentType: parentType,
childType: childInfo ? XBlockViewUtils.getXBlockType(childInfo.category, xblockInfo) : null,
childCategory: childInfo ? childInfo.category : null,
addChildLabel: addChildName,
defaultNewChildName: defaultNewChildName,
isCollapsed: isCollapsed,
includesChildren: this.shouldRenderChildren(),
hasExplicitStaffLock: this.model.get('has_explicit_staff_lock'),
staffOnlyMessage: this.model.get('staff_only_message'),
course: course
};
},
renderChildren: function() {
var self = this,
parentInfo = this.model;
if (parentInfo.get('child_info')) {
_.each(this.model.get('child_info').children, function(childInfo) {
var childOutlineView = self.createChildView(childInfo, parentInfo);
childOutlineView.render();
self.addChildView(childOutlineView);
});
}
this.renderedChildren = true;
},
getListElement: function() {
return this.$('> .outline-content > ol');
},
addChildView: function(childView, xblockElement) {
if (xblockElement) {
childView.$el.insertAfter(xblockElement);
} else {
this.getListElement().append(childView.$el);
}
},
addNameEditor: function() {
var self = this,
xblockField = this.$('.wrapper-xblock-field'),
XBlockOutlineFieldEditor, nameEditor;
if (xblockField.length > 0) {
// Make a subclass of the standard xblock string field editor which refreshes
// the entire section that this view is contained in. This is necessary as
// changing the name could have caused the section to change state.
XBlockOutlineFieldEditor = XBlockStringFieldEditor.extend({
refresh: function() {
self.refresh();
}
});
nameEditor = new XBlockOutlineFieldEditor({
el: xblockField,
model: this.model
});
nameEditor.render();
}
},
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
this.ensureChildrenRendered();
BaseView.prototype.toggleExpandCollapse.call(this, event);
},
/**
* Verifies that the children are rendered (if they should be).
*/
ensureChildrenRendered: function() {
if (!this.renderedChildren && this.shouldRenderChildren()) {
this.renderChildren();
}
},
/**
* Adds handlers to the each button in the header's panel. This is managed outside of
* Backbone's own event registration so that the handlers don't get scoped to all the
* children of this view.
* @param element The root element of this view.
*/
addButtonActions: function(element) {
var self = this;
element.find('.delete-button').click(_.bind(this.handleDeleteEvent, this));
element.find('.duplicate-button').click(_.bind(this.handleDuplicateEvent, this));
element.find('.button-new').click(_.bind(this.handleAddEvent, this));
},
shouldRenderChildren: function() {
return true;
},
shouldExpandChildren: function() {
return true;
},
getChildViewClass: function() {
return XBlockOutlineView;
},
createChildView: function(childInfo, parentInfo, options) {
var viewClass = this.getChildViewClass();
return new viewClass(_.extend({
model: childInfo,
parentInfo: parentInfo,
parentView: this,
initialState: this.initialState,
expandedLocators: this.expandedLocators,
template: this.template
}, options));
},
onSync: function(event) {
var hasChangedAttributes = ViewUtils.hasChangedAttributes(
this.model,
['visibility_state', 'child_info', 'display_name', 'highlights']
);
if (hasChangedAttributes) {
this.onXBlockChange();
}
},
onXBlockChange: function() {
var oldElement = this.$el,
viewState = this.initialState;
this.render();
if (this.parentInfo) {
oldElement.replaceWith(this.$el);
}
if (viewState) {
this.setViewState(viewState);
}
},
setViewState: function(viewState) {
var locatorToShow = viewState.locator_to_show,
scrollOffset = viewState.scroll_offset || 0,
editDisplayName = viewState.edit_display_name,
locatorElement;
if (locatorToShow) {
if (locatorToShow === this.model.id) {
locatorElement = this.$el;
} else {
locatorElement = this.$('.outline-item[data-locator="' + locatorToShow + '"]');
}
if (locatorElement.length > 0) {
ViewUtils.setScrollOffset(locatorElement, scrollOffset);
} else {
console.error('Failed to show item with locator ' + locatorToShow + '');
}
if (editDisplayName) {
locatorElement.find('> div[class$="header"] .xblock-field-value-edit').click();
}
}
this.initialState = null;
},
/**
* Refresh the view's model from the server, which will cause the view to refresh.
* @returns {jQuery promise} A promise representing the refresh operation.
*/
refresh: function() {
return this.model.fetch();
},
onChildAdded: function(locator, category) {
// For units, redirect to the new page, and for everything else just refresh inline.
if (category === 'vertical') {
this.onUnitAdded(locator);
} else {
this.refresh();
}
},
onUnitAdded: function(locator) {
ViewUtils.redirect('/container/' + locator + '?action=new');
},
onChildDeleted: function() {
this.refresh();
},
handleDeleteEvent: function(event) {
var self = this,
parentView = this.parentView,
xblockType = XBlockViewUtils.getXBlockType(this.model.get('category'), parentView.model, true);
event.preventDefault();
XBlockViewUtils.deleteXBlock(this.model, xblockType).done(function() {
if (parentView) {
parentView.onChildDeleted(self, event);
}
});
},
/**
* Finds appropriate parent element for an xblock element.
* @param {jquery Element} xblockElement The xblock element to be duplicated.
* @param {String} xblockType The front-end terminology of the xblock category.
* @returns {jquery Element} Appropriate parent element of xblock element.
*/
getParentElement: function(xblockElement, xblockType) {
var xblockMap = {
unit: 'subsection',
subsection: 'section',
section: 'course'
},
parentXblockType = xblockMap[xblockType];
return xblockElement.closest('.outline-' + parentXblockType);
},
/**
* Duplicate event handler.
*/
handleDuplicateEvent: function(event) {
var self = this,
xblockType = XBlockViewUtils.getXBlockType(self.model.get('category'), self.parentView.model),
xblockElement = $(event.currentTarget).closest('.outline-item'),
parentElement = self.getParentElement(xblockElement, xblockType);
event.preventDefault();
XBlockViewUtils.duplicateXBlock(xblockElement, parentElement)
.done(function(data) {
if (self.parentView) {
self.parentView.onChildDuplicated(
data.locator,
xblockType,
xblockElement
);
}
});
},
handleAddEvent: function(event) {
var self = this,
$target = $(event.currentTarget),
category = $target.data('category');
event.preventDefault();
XBlockViewUtils.addXBlock($target).done(function(locator) {
self.onChildAdded(locator, category, event);
});
}
});
return XBlockOutlineView;
}); // end define();