384 lines
18 KiB
JavaScript
384 lines
18 KiB
JavaScript
/**
|
|
* XBlockContainerPage is used to display Studio's container page for an xblock which has children.
|
|
* This page allows the user to understand and manipulate the xblock and its children.
|
|
*/
|
|
define(['jquery', 'underscore', 'backbone', 'gettext', 'js/views/pages/base_page',
|
|
'common/js/components/utils/view_utils', 'js/views/container', 'js/views/xblock',
|
|
'js/views/components/add_xblock', 'js/views/modals/edit_xblock', 'js/views/modals/move_xblock_modal',
|
|
'js/models/xblock_info', 'js/views/xblock_string_field_editor', 'js/views/xblock_access_editor',
|
|
'js/views/pages/container_subviews', 'js/views/unit_outline', 'js/views/utils/xblock_utils'],
|
|
function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView, AddXBlockComponent,
|
|
EditXBlockModal, MoveXBlockModal, XBlockInfo, XBlockStringFieldEditor, XBlockAccessEditor,
|
|
ContainerSubviews, UnitOutlineView, XBlockUtils) {
|
|
'use strict';
|
|
var XBlockContainerPage = BasePage.extend({
|
|
// takes XBlockInfo as a model
|
|
|
|
events: {
|
|
'click .edit-button': 'editXBlock',
|
|
'click .access-button': 'editVisibilitySettings',
|
|
'click .duplicate-button': 'duplicateXBlock',
|
|
'click .move-button': 'showMoveXBlockModal',
|
|
'click .delete-button': 'deleteXBlock',
|
|
'click .new-component-button': 'scrollToNewComponentButtons'
|
|
},
|
|
|
|
options: {
|
|
collapsedClass: 'is-collapsed',
|
|
canEdit: true // If not specified, assume user has permission to make changes
|
|
},
|
|
|
|
view: 'container_preview',
|
|
|
|
defaultViewClass: ContainerView,
|
|
|
|
// Overridable by subclasses-- determines whether the XBlock component
|
|
// addition menu is added on initialization. You may set this to false
|
|
// if your subclass handles it.
|
|
components_on_init: true,
|
|
|
|
initialize: function(options) {
|
|
BasePage.prototype.initialize.call(this, options);
|
|
this.viewClass = options.viewClass || this.defaultViewClass;
|
|
this.isLibraryPage = (this.model.attributes.category === 'library');
|
|
this.nameEditor = new XBlockStringFieldEditor({
|
|
el: this.$('.wrapper-xblock-field'),
|
|
model: this.model
|
|
});
|
|
this.nameEditor.render();
|
|
if (!this.isLibraryPage) {
|
|
this.accessEditor = new XBlockAccessEditor({
|
|
el: this.$('.wrapper-xblock-field')
|
|
});
|
|
this.accessEditor.render();
|
|
}
|
|
if (this.options.action === 'new') {
|
|
this.nameEditor.$('.xblock-field-value-edit').click();
|
|
}
|
|
this.xblockView = this.getXBlockView();
|
|
this.messageView = new ContainerSubviews.MessageView({
|
|
el: this.$('.container-message'),
|
|
model: this.model
|
|
});
|
|
this.messageView.render();
|
|
// Display access message on units and split test components
|
|
if (!this.isLibraryPage) {
|
|
this.containerAccessView = new ContainerSubviews.ContainerAccess({
|
|
el: this.$('.container-access'),
|
|
model: this.model
|
|
});
|
|
this.containerAccessView.render();
|
|
|
|
this.xblockPublisher = new ContainerSubviews.Publisher({
|
|
el: this.$('#publish-unit'),
|
|
model: this.model,
|
|
// When "Discard Changes" is clicked, the whole page must be re-rendered.
|
|
renderPage: this.render
|
|
});
|
|
this.xblockPublisher.render();
|
|
|
|
this.publishHistory = new ContainerSubviews.PublishHistory({
|
|
el: this.$('#publish-history'),
|
|
model: this.model
|
|
});
|
|
this.publishHistory.render();
|
|
|
|
this.viewLiveActions = new ContainerSubviews.ViewLiveButtonController({
|
|
el: this.$('.nav-actions'),
|
|
model: this.model
|
|
});
|
|
this.viewLiveActions.render();
|
|
|
|
this.unitOutlineView = new UnitOutlineView({
|
|
el: this.$('.wrapper-unit-overview'),
|
|
model: this.model
|
|
});
|
|
this.unitOutlineView.render();
|
|
}
|
|
|
|
this.listenTo(Backbone, 'move:onXBlockMoved', this.onXBlockMoved);
|
|
},
|
|
|
|
getViewParameters: function() {
|
|
return {
|
|
el: this.$('.wrapper-xblock'),
|
|
model: this.model,
|
|
view: this.view
|
|
};
|
|
},
|
|
|
|
getXBlockView: function() {
|
|
return new this.viewClass(this.getViewParameters());
|
|
},
|
|
|
|
render: function(options) {
|
|
var self = this,
|
|
xblockView = this.xblockView,
|
|
loadingElement = this.$('.ui-loading'),
|
|
unitLocationTree = this.$('.unit-location'),
|
|
hiddenCss = 'is-hidden';
|
|
|
|
loadingElement.removeClass(hiddenCss);
|
|
|
|
// Hide both blocks until we know which one to show
|
|
xblockView.$el.addClass(hiddenCss);
|
|
|
|
// Render the xblock
|
|
xblockView.render({
|
|
done: function() {
|
|
// Show the xblock and hide the loading indicator
|
|
xblockView.$el.removeClass(hiddenCss);
|
|
loadingElement.addClass(hiddenCss);
|
|
|
|
// Notify the runtime that the page has been successfully shown
|
|
xblockView.notifyRuntime('page-shown', self);
|
|
|
|
if (self.components_on_init) {
|
|
// Render the add buttons. Paged containers should do this on their own.
|
|
self.renderAddXBlockComponents();
|
|
}
|
|
|
|
// Refresh the views now that the xblock is visible
|
|
self.onXBlockRefresh(xblockView);
|
|
unitLocationTree.removeClass(hiddenCss);
|
|
|
|
// Re-enable Backbone events for any updated DOM elements
|
|
self.delegateEvents();
|
|
},
|
|
block_added: options && options.block_added
|
|
});
|
|
},
|
|
|
|
findXBlockElement: function(target) {
|
|
return $(target).closest('.studio-xblock-wrapper');
|
|
},
|
|
|
|
getURLRoot: function() {
|
|
return this.xblockView.model.urlRoot;
|
|
},
|
|
|
|
onXBlockRefresh: function(xblockView, block_added, is_duplicate) {
|
|
this.xblockView.refresh(xblockView, block_added, is_duplicate);
|
|
// Update publish and last modified information from the server.
|
|
this.model.fetch();
|
|
},
|
|
|
|
renderAddXBlockComponents: function() {
|
|
var self = this;
|
|
if (self.options.canEdit) {
|
|
this.$('.add-xblock-component').each(function(index, element) {
|
|
var component = new AddXBlockComponent({
|
|
el: element,
|
|
createComponent: _.bind(self.createComponent, self),
|
|
collection: self.options.templates
|
|
});
|
|
component.render();
|
|
});
|
|
} else {
|
|
this.$('.add-xblock-component').remove();
|
|
}
|
|
},
|
|
|
|
editXBlock: function(event, options) {
|
|
var xblockElement = this.findXBlockElement(event.target),
|
|
self = this,
|
|
modal = new EditXBlockModal(options);
|
|
event.preventDefault();
|
|
|
|
modal.edit(xblockElement, this.model, {
|
|
readOnlyView: !this.options.canEdit,
|
|
refresh: function() {
|
|
self.refreshXBlock(xblockElement, false);
|
|
}
|
|
});
|
|
},
|
|
|
|
editVisibilitySettings: function(event) {
|
|
this.editXBlock(event, {
|
|
view: 'visibility_view',
|
|
// Translators: "title" is the name of the current component or unit being edited.
|
|
titleFormat: gettext('Editing access for: %(title)s'),
|
|
viewSpecificClasses: '',
|
|
modalSize: 'med'
|
|
});
|
|
},
|
|
|
|
duplicateXBlock: function(event) {
|
|
event.preventDefault();
|
|
this.duplicateComponent(this.findXBlockElement(event.target));
|
|
},
|
|
|
|
showMoveXBlockModal: function(event) {
|
|
var xblockElement = this.findXBlockElement(event.target),
|
|
parentXBlockElement = xblockElement.parents('.studio-xblock-wrapper'),
|
|
modal = new MoveXBlockModal({
|
|
sourceXBlockInfo: XBlockUtils.findXBlockInfo(xblockElement, this.model),
|
|
sourceParentXBlockInfo: XBlockUtils.findXBlockInfo(parentXBlockElement, this.model),
|
|
XBlockURLRoot: this.getURLRoot(),
|
|
outlineURL: this.options.outlineURL
|
|
});
|
|
|
|
event.preventDefault();
|
|
modal.show();
|
|
},
|
|
|
|
deleteXBlock: function(event) {
|
|
event.preventDefault();
|
|
this.deleteComponent(this.findXBlockElement(event.target));
|
|
},
|
|
|
|
createPlaceholderElement: function() {
|
|
return $('<div/>', {class: 'studio-xblock-wrapper'});
|
|
},
|
|
|
|
createComponent: function(template, target) {
|
|
// A placeholder element is created in the correct location for the new xblock
|
|
// and then onNewXBlock will replace it with a rendering of the xblock. Note that
|
|
// for xblocks that can't be replaced inline, the entire parent will be refreshed.
|
|
var parentElement = this.findXBlockElement(target),
|
|
parentLocator = parentElement.data('locator'),
|
|
buttonPanel = target.closest('.add-xblock-component'),
|
|
listPanel = buttonPanel.prev(),
|
|
scrollOffset = ViewUtils.getScrollOffset(buttonPanel),
|
|
$placeholderEl = $(this.createPlaceholderElement()),
|
|
requestData = _.extend(template, {
|
|
parent_locator: parentLocator
|
|
}),
|
|
placeholderElement;
|
|
placeholderElement = $placeholderEl.appendTo(listPanel);
|
|
return $.postJSON(this.getURLRoot() + '/', requestData,
|
|
_.bind(this.onNewXBlock, this, placeholderElement, scrollOffset, false))
|
|
.fail(function() {
|
|
// Remove the placeholder if the update failed
|
|
placeholderElement.remove();
|
|
});
|
|
},
|
|
|
|
duplicateComponent: function(xblockElement) {
|
|
// A placeholder element is created in the correct location for the duplicate xblock
|
|
// and then onNewXBlock will replace it with a rendering of the xblock. Note that
|
|
// for xblocks that can't be replaced inline, the entire parent will be refreshed.
|
|
var self = this,
|
|
parentElement = self.findXBlockElement(xblockElement.parent()),
|
|
scrollOffset = ViewUtils.getScrollOffset(xblockElement),
|
|
$placeholderEl = $(self.createPlaceholderElement()),
|
|
placeholderElement;
|
|
|
|
placeholderElement = $placeholderEl.insertAfter(xblockElement);
|
|
XBlockUtils.duplicateXBlock(xblockElement, parentElement)
|
|
.done(function(data) {
|
|
self.onNewXBlock(placeholderElement, scrollOffset, true, data);
|
|
})
|
|
.fail(function() {
|
|
// Remove the placeholder if the update failed
|
|
placeholderElement.remove();
|
|
});
|
|
},
|
|
|
|
deleteComponent: function(xblockElement) {
|
|
var self = this,
|
|
xblockInfo = new XBlockInfo({
|
|
id: xblockElement.data('locator')
|
|
});
|
|
XBlockUtils.deleteXBlock(xblockInfo).done(function() {
|
|
self.onDelete(xblockElement);
|
|
});
|
|
},
|
|
|
|
onDelete: function(xblockElement) {
|
|
// get the parent so we can remove this component from its parent.
|
|
var xblockView = this.xblockView,
|
|
parent = this.findXBlockElement(xblockElement.parent());
|
|
xblockElement.remove();
|
|
|
|
// Inform the runtime that the child has been deleted in case
|
|
// other views are listening to deletion events.
|
|
xblockView.acknowledgeXBlockDeletion(parent.data('locator'));
|
|
|
|
// Update publish and last modified information from the server.
|
|
this.model.fetch();
|
|
},
|
|
|
|
/*
|
|
After move operation is complete, updates the xblock information from server .
|
|
*/
|
|
onXBlockMoved: function() {
|
|
this.model.fetch();
|
|
},
|
|
|
|
onNewXBlock: function(xblockElement, scrollOffset, is_duplicate, data) {
|
|
ViewUtils.setScrollOffset(xblockElement, scrollOffset);
|
|
xblockElement.data('locator', data.locator);
|
|
return this.refreshXBlock(xblockElement, true, is_duplicate);
|
|
},
|
|
|
|
/**
|
|
* Refreshes the specified xblock's display. If the xblock is an inline child of a
|
|
* reorderable container then the element will be refreshed inline. If not, then the
|
|
* parent container will be refreshed instead.
|
|
* @param element An element representing the xblock to be refreshed.
|
|
* @param block_added Flag to indicate that new block has been just added.
|
|
*/
|
|
refreshXBlock: function(element, block_added, is_duplicate) {
|
|
var xblockElement = this.findXBlockElement(element),
|
|
parentElement = xblockElement.parent(),
|
|
rootLocator = this.xblockView.model.id;
|
|
if (xblockElement.length === 0 || xblockElement.data('locator') === rootLocator) {
|
|
this.render({refresh: true, block_added: block_added});
|
|
} else if (parentElement.hasClass('reorderable-container')) {
|
|
this.refreshChildXBlock(xblockElement, block_added, is_duplicate);
|
|
} else {
|
|
this.refreshXBlock(this.findXBlockElement(parentElement));
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Refresh an xblock element inline on the page, using the specified xblockInfo.
|
|
* Note that the element is removed and replaced with the newly rendered xblock.
|
|
* @param xblockElement The xblock element to be refreshed.
|
|
* @param block_added Specifies if a block has been added, rather than just needs
|
|
* refreshing.
|
|
* @returns {jQuery promise} A promise representing the complete operation.
|
|
*/
|
|
refreshChildXBlock: function(xblockElement, block_added, is_duplicate) {
|
|
var self = this,
|
|
xblockInfo,
|
|
TemporaryXBlockView,
|
|
temporaryView;
|
|
xblockInfo = new XBlockInfo({
|
|
id: xblockElement.data('locator')
|
|
});
|
|
// There is only one Backbone view created on the container page, which is
|
|
// for the container xblock itself. Any child xblocks rendered inside the
|
|
// container do not get a Backbone view. Thus, create a temporary view
|
|
// to render the content, and then replace the original element with the result.
|
|
TemporaryXBlockView = XBlockView.extend({
|
|
updateHtml: function(element, html) {
|
|
// Replace the element with the new HTML content, rather than adding
|
|
// it as child elements.
|
|
this.$el = $(html).replaceAll(element); // xss-lint: disable=javascript-jquery-insertion
|
|
}
|
|
});
|
|
temporaryView = new TemporaryXBlockView({
|
|
model: xblockInfo,
|
|
view: self.xblockView.new_child_view,
|
|
el: xblockElement
|
|
});
|
|
return temporaryView.render({
|
|
success: function() {
|
|
self.onXBlockRefresh(temporaryView, block_added, is_duplicate);
|
|
temporaryView.unbind(); // Remove the temporary view
|
|
},
|
|
initRuntimeData: this
|
|
});
|
|
},
|
|
|
|
scrollToNewComponentButtons: function(event) {
|
|
event.preventDefault();
|
|
$.scrollTo(this.$('.add-xblock-component'), {duration: 250});
|
|
}
|
|
});
|
|
|
|
return XBlockContainerPage;
|
|
}); // end define();
|