246 lines
12 KiB
JavaScript
246 lines
12 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", "gettext", "js/views/baseview", "js/views/container",
|
|
"js/views/xblock", "js/views/components/add_xblock", "js/views/modals/edit_xblock", "js/models/xblock_info"],
|
|
function ($, _, gettext, BaseView, ContainerView, XBlockView, AddXBlockComponent, EditXBlockModal, XBlockInfo) {
|
|
var XBlockContainerPage = BaseView.extend({
|
|
// takes XBlockInfo as a model
|
|
|
|
view: 'container_preview',
|
|
|
|
initialize: function() {
|
|
BaseView.prototype.initialize.call(this);
|
|
this.xblockView = new ContainerView({
|
|
el: this.$('.wrapper-xblock'),
|
|
model: this.model,
|
|
view: this.view
|
|
});
|
|
},
|
|
|
|
render: function(options) {
|
|
var self = this,
|
|
xblockView = this.xblockView,
|
|
loadingElement = this.$('.ui-loading');
|
|
loadingElement.removeClass('is-hidden');
|
|
|
|
// Hide both blocks until we know which one to show
|
|
xblockView.$el.addClass('is-hidden');
|
|
|
|
if (!options || !options.refresh) {
|
|
// Add actions to any top level buttons, e.g. "Edit" of the container itself.
|
|
// Do not add the actions on "refresh" though, as the handlers are already registered.
|
|
self.addButtonActions(this.$el);
|
|
}
|
|
|
|
// Render the xblock
|
|
xblockView.render({
|
|
success: function(xblock) {
|
|
xblockView.xblock.runtime.notify("page-shown", self);
|
|
xblockView.$el.removeClass('is-hidden');
|
|
self.renderAddXBlockComponents();
|
|
self.onXBlockRefresh(xblockView);
|
|
self.refreshTitle();
|
|
loadingElement.addClass('is-hidden');
|
|
self.delegateEvents();
|
|
}
|
|
});
|
|
},
|
|
|
|
findXBlockElement: function(target) {
|
|
return $(target).closest('.studio-xblock-wrapper');
|
|
},
|
|
|
|
getURLRoot: function() {
|
|
return this.xblockView.model.urlRoot;
|
|
},
|
|
|
|
refreshTitle: function() {
|
|
var title = this.$('.xblock-header .header-details .xblock-display-name').first().text().trim();
|
|
this.$('.page-header-title').text(title);
|
|
this.$('.page-header .subtitle a').last().text(title);
|
|
},
|
|
|
|
onXBlockRefresh: function(xblockView) {
|
|
this.addButtonActions(xblockView.$el);
|
|
this.xblockView.refresh();
|
|
},
|
|
|
|
renderAddXBlockComponents: function() {
|
|
var self = this;
|
|
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();
|
|
});
|
|
},
|
|
|
|
addButtonActions: function(element) {
|
|
var self = this;
|
|
element.find('.edit-button').click(function(event) {
|
|
event.preventDefault();
|
|
self.editComponent(self.findXBlockElement(event.target));
|
|
});
|
|
element.find('.duplicate-button').click(function(event) {
|
|
event.preventDefault();
|
|
self.duplicateComponent(self.findXBlockElement(event.target));
|
|
});
|
|
element.find('.delete-button').click(function(event) {
|
|
event.preventDefault();
|
|
self.deleteComponent(self.findXBlockElement(event.target));
|
|
});
|
|
},
|
|
|
|
editComponent: function(xblockElement) {
|
|
var self = this,
|
|
modal = new EditXBlockModal({ });
|
|
modal.edit(xblockElement, this.model, {
|
|
refresh: function() {
|
|
self.refreshXBlock(xblockElement);
|
|
}
|
|
});
|
|
},
|
|
|
|
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 = this.getScrollOffset(buttonPanel),
|
|
placeholderElement = $('<div class="studio-xblock-wrapper"></div>').appendTo(listPanel),
|
|
requestData = _.extend(template, {
|
|
parent_locator: parentLocator
|
|
});
|
|
return $.postJSON(this.getURLRoot() + '/', requestData,
|
|
_.bind(this.onNewXBlock, this, placeholderElement, scrollOffset))
|
|
.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,
|
|
parent = xblockElement.parent();
|
|
this.runOperationShowingMessage(gettext('Duplicating…'),
|
|
function() {
|
|
var scrollOffset = self.getScrollOffset(xblockElement),
|
|
placeholderElement = $('<div class="studio-xblock-wrapper"></div>').insertAfter(xblockElement),
|
|
parentElement = self.findXBlockElement(parent),
|
|
requestData = {
|
|
duplicate_source_locator: xblockElement.data('locator'),
|
|
parent_locator: parentElement.data('locator')
|
|
};
|
|
return $.postJSON(self.getURLRoot() + '/', requestData,
|
|
_.bind(self.onNewXBlock, self, placeholderElement, scrollOffset))
|
|
.fail(function() {
|
|
// Remove the placeholder if the update failed
|
|
placeholderElement.remove();
|
|
});
|
|
});
|
|
},
|
|
|
|
deleteComponent: function(xblockElement) {
|
|
var self = this;
|
|
this.confirmThenRunOperation(gettext('Delete this component?'),
|
|
gettext('Deleting this component is permanent and cannot be undone.'),
|
|
gettext('Yes, delete this component'),
|
|
function() {
|
|
self.runOperationShowingMessage(gettext('Deleting…'),
|
|
function() {
|
|
return $.ajax({
|
|
type: 'DELETE',
|
|
url: self.getURLRoot() + "/" +
|
|
xblockElement.data('locator')
|
|
}).success(_.bind(self.onDelete, self, xblockElement));
|
|
});
|
|
});
|
|
},
|
|
|
|
onDelete: function(xblockElement) {
|
|
// get the parent so we can remove this component from its parent.
|
|
var xblockView = this.xblockView,
|
|
xblock = xblockView.xblock,
|
|
parent = this.findXBlockElement(xblockElement.parent());
|
|
xblockElement.remove();
|
|
xblockView.updateChildren(parent);
|
|
xblock.runtime.notify('deleted-child', parent.data('locator'));
|
|
},
|
|
|
|
onNewXBlock: function(xblockElement, scrollOffset, data) {
|
|
this.setScrollOffset(xblockElement, scrollOffset);
|
|
xblockElement.data('locator', data.locator);
|
|
return this.refreshXBlock(xblockElement);
|
|
},
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
refreshXBlock: function(element) {
|
|
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});
|
|
} else if (parentElement.hasClass('reorderable-container')) {
|
|
this.refreshChildXBlock(xblockElement);
|
|
} 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.
|
|
* @returns {promise} A promise representing the complete operation.
|
|
*/
|
|
refreshChildXBlock: function(xblockElement) {
|
|
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);
|
|
}
|
|
});
|
|
temporaryView = new TemporaryXBlockView({
|
|
model: xblockInfo,
|
|
view: 'reorderable_container_child_preview',
|
|
el: xblockElement
|
|
});
|
|
return temporaryView.render({
|
|
success: function() {
|
|
self.onXBlockRefresh(temporaryView);
|
|
temporaryView.unbind(); // Remove the temporary view
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
return XBlockContainerPage;
|
|
}); // end define();
|