Allow creation of components on container page
This commit implements STUD-1490, allowing creation of components on the container page. It also enables the delete and duplicate buttons now that new content can be created that would benefit. Note that it also creates shared functionality for adding components, and refactors the unit page to use it too.
This commit is contained in:
5
cms/static/js/collections/component_template.js
Normal file
5
cms/static/js/collections/component_template.js
Normal file
@@ -0,0 +1,5 @@
|
||||
define(["backbone", "js/models/component_template"], function(Backbone, ComponentTemplate) {
|
||||
return Backbone.Collection.extend({
|
||||
model : ComponentTemplate
|
||||
});
|
||||
});
|
||||
31
cms/static/js/models/component_template.js
Normal file
31
cms/static/js/models/component_template.js
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Simple model for adding a component of a given type (for example, "video" or "html").
|
||||
*/
|
||||
define(["backbone"], function (Backbone) {
|
||||
return Backbone.Model.extend({
|
||||
defaults: {
|
||||
type: "",
|
||||
// Each entry in the template array is an Object with the following keys:
|
||||
// display_name
|
||||
// category (may or may not match "type")
|
||||
// boilerplate_name (may be null)
|
||||
// is_common (only used for problems)
|
||||
templates: []
|
||||
},
|
||||
parse: function (response) {
|
||||
this.type = response.type;
|
||||
this.templates = response.templates;
|
||||
|
||||
// Sort the templates.
|
||||
this.templates.sort(function (a, b) {
|
||||
// The entry without a boilerplate always goes first
|
||||
if (!a.boilerplate_name || (a.display_name < b.display_name)) {
|
||||
return -1;
|
||||
}
|
||||
else {
|
||||
return (a.display_name > b.display_name) ? 1 : 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
define(["jquery", "underscore", "js/views/baseview", "js/utils/handle_iframe_binding", "sinon"],
|
||||
function ($, _, BaseView, IframeBinding, sinon) {
|
||||
define(["jquery", "underscore", "js/views/baseview", "js/utils/handle_iframe_binding", "sinon",
|
||||
"js/spec_helpers/edit_helpers"],
|
||||
function ($, _, BaseView, IframeBinding, sinon, view_helpers) {
|
||||
|
||||
describe("BaseView", function() {
|
||||
var baseViewPrototype;
|
||||
@@ -79,8 +80,7 @@ define(["jquery", "underscore", "js/views/baseview", "js/utils/handle_iframe_bin
|
||||
|
||||
describe("disabled element while running", function() {
|
||||
it("adds 'is-disabled' class to element while action is running and removes it after", function() {
|
||||
var viewWithLink,
|
||||
link,
|
||||
var link,
|
||||
deferred = new $.Deferred(),
|
||||
promise = deferred.promise(),
|
||||
view = new BaseView();
|
||||
@@ -89,11 +89,37 @@ define(["jquery", "underscore", "js/views/baseview", "js/utils/handle_iframe_bin
|
||||
|
||||
link = $("#link");
|
||||
expect(link).not.toHaveClass("is-disabled");
|
||||
view.disableElementWhileRunning(link, function(){return promise});
|
||||
view.disableElementWhileRunning(link, function() { return promise; });
|
||||
expect(link).toHaveClass("is-disabled");
|
||||
deferred.resolve();
|
||||
expect(link).not.toHaveClass("is-disabled");
|
||||
});
|
||||
});
|
||||
|
||||
describe("progress notification", function() {
|
||||
it("shows progress notification and removes it upon success", function() {
|
||||
var testMessage = "Testing...",
|
||||
deferred = new $.Deferred(),
|
||||
promise = deferred.promise(),
|
||||
view = new BaseView(),
|
||||
notificationSpy = view_helpers.createNotificationSpy();
|
||||
view.runOperationShowingMessage(testMessage, function() { return promise; });
|
||||
view_helpers.verifyNotificationShowing(notificationSpy, /Testing/);
|
||||
deferred.resolve();
|
||||
view_helpers.verifyNotificationHidden(notificationSpy);
|
||||
});
|
||||
|
||||
it("shows progress notification and leaves it showing upon failure", function() {
|
||||
var testMessage = "Testing...",
|
||||
deferred = new $.Deferred(),
|
||||
promise = deferred.promise(),
|
||||
view = new BaseView(),
|
||||
notificationSpy = view_helpers.createNotificationSpy();
|
||||
view.runOperationShowingMessage(testMessage, function() { return promise; });
|
||||
view_helpers.verifyNotificationShowing(notificationSpy, /Testing/);
|
||||
deferred.fail();
|
||||
view_helpers.verifyNotificationShowing(notificationSpy, /Testing/);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
define([ "jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers",
|
||||
"js/views/container", "js/models/xblock_info", "js/views/feedback_notification", "jquery.simulate",
|
||||
"js/views/container", "js/models/xblock_info", "jquery.simulate",
|
||||
"xmodule", "coffee/src/main", "xblock/cms.runtime.v1"],
|
||||
function ($, create_sinon, view_helpers, ContainerView, XBlockInfo, Notification) {
|
||||
function ($, create_sinon, view_helpers, ContainerView, XBlockInfo) {
|
||||
|
||||
describe("Container View", function () {
|
||||
|
||||
@@ -9,7 +9,7 @@ define([ "jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers
|
||||
|
||||
var model, containerView, mockContainerHTML, respondWithMockXBlockFragment, init, getComponent,
|
||||
getDragHandle, dragComponentVertically, dragComponentAbove,
|
||||
verifyRequest, verifyNumReorderCalls, respondToRequest,
|
||||
verifyRequest, verifyNumReorderCalls, respondToRequest, notificationSpy,
|
||||
|
||||
rootLocator = 'testCourse/branch/draft/split_test/splitFFF',
|
||||
containerTestUrl = '/xblock/' + rootLocator,
|
||||
@@ -35,7 +35,8 @@ define([ "jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers
|
||||
|
||||
beforeEach(function () {
|
||||
view_helpers.installViewTemplates();
|
||||
appendSetFixtures('<div class="wrapper-xblock level-page" data-locator="' + rootLocator + '"></div>');
|
||||
appendSetFixtures('<div class="wrapper-xblock level-page studio-xblock-wrapper" data-locator="' + rootLocator + '"></div>');
|
||||
notificationSpy = view_helpers.createNotificationSpy();
|
||||
model = new XBlockInfo({
|
||||
id: rootLocator,
|
||||
display_name: 'Test AB Test',
|
||||
@@ -63,16 +64,29 @@ define([ "jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers
|
||||
});
|
||||
|
||||
$('body').append(containerView.$el);
|
||||
|
||||
// Give the whole container enough height to contain everything.
|
||||
$('.xblock[data-locator=locator-container]').css('height', 2000);
|
||||
|
||||
// Give the groups enough height to contain their child vertical elements.
|
||||
$('.is-draggable[data-locator=locator-group-A]').css('height', 800);
|
||||
$('.is-draggable[data-locator=locator-group-B]').css('height', 800);
|
||||
|
||||
|
||||
// Give the leaf elements some height to mimic actual components. Otherwise
|
||||
// drag and drop fails as the elements on bunched on top of each other.
|
||||
$('.level-element').css('height', 200);
|
||||
|
||||
return requests;
|
||||
};
|
||||
|
||||
getComponent = function(locator) {
|
||||
return containerView.$('[data-locator="' + locator + '"]');
|
||||
return containerView.$('.studio-xblock-wrapper[data-locator="' + locator + '"]');
|
||||
};
|
||||
|
||||
getDragHandle = function(locator) {
|
||||
var component = getComponent(locator);
|
||||
return component.prev();
|
||||
return $(component.find('.drag-handle')[0]);
|
||||
};
|
||||
|
||||
dragComponentVertically = function (locator, dy) {
|
||||
@@ -166,31 +180,17 @@ define([ "jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers
|
||||
});
|
||||
|
||||
describe("Shows a saving message", function () {
|
||||
var savingSpies;
|
||||
|
||||
beforeEach(function () {
|
||||
savingSpies = spyOnConstructor(Notification, "Mini",
|
||||
["show", "hide"]);
|
||||
savingSpies.show.andReturn(savingSpies);
|
||||
});
|
||||
|
||||
it('hides saving message upon success', function () {
|
||||
var requests, savingOptions;
|
||||
requests = init(this);
|
||||
|
||||
// Drag the first component in Group B to the first group.
|
||||
dragComponentAbove(groupBComponent1, groupAComponent1);
|
||||
|
||||
expect(savingSpies.constructor).toHaveBeenCalled();
|
||||
expect(savingSpies.show).toHaveBeenCalled();
|
||||
expect(savingSpies.hide).not.toHaveBeenCalled();
|
||||
savingOptions = savingSpies.constructor.mostRecentCall.args[0];
|
||||
expect(savingOptions.title).toMatch(/Saving/);
|
||||
|
||||
view_helpers.verifyNotificationShowing(notificationSpy, 'Saving');
|
||||
respondToRequest(requests, 0, 200);
|
||||
expect(savingSpies.hide).not.toHaveBeenCalled();
|
||||
view_helpers.verifyNotificationShowing(notificationSpy, 'Saving');
|
||||
respondToRequest(requests, 1, 200);
|
||||
expect(savingSpies.hide).toHaveBeenCalled();
|
||||
view_helpers.verifyNotificationHidden(notificationSpy);
|
||||
});
|
||||
|
||||
it('does not hide saving message if failure', function () {
|
||||
@@ -198,13 +198,9 @@ define([ "jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers
|
||||
|
||||
// Drag the first component in Group B to the first group.
|
||||
dragComponentAbove(groupBComponent1, groupAComponent1);
|
||||
|
||||
expect(savingSpies.constructor).toHaveBeenCalled();
|
||||
expect(savingSpies.show).toHaveBeenCalled();
|
||||
expect(savingSpies.hide).not.toHaveBeenCalled();
|
||||
|
||||
view_helpers.verifyNotificationShowing(notificationSpy, 'Saving');
|
||||
respondToRequest(requests, 0, 500);
|
||||
expect(savingSpies.hide).not.toHaveBeenCalled();
|
||||
view_helpers.verifyNotificationShowing(notificationSpy, 'Saving');
|
||||
|
||||
// Since the first reorder call failed, the removal will not be called.
|
||||
verifyNumReorderCalls(requests, 1);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers",
|
||||
"js/views/feedback_notification", "js/views/feedback_prompt",
|
||||
"js/views/pages/container", "js/models/xblock_info"],
|
||||
function ($, create_sinon, edit_helpers, Notification, Prompt, ContainerPage, XBlockInfo) {
|
||||
define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers",
|
||||
"js/views/feedback_prompt", "js/views/pages/container", "js/models/xblock_info"],
|
||||
function ($, _, create_sinon, edit_helpers, Prompt, ContainerPage, XBlockInfo) {
|
||||
|
||||
describe("ContainerPage", function() {
|
||||
var lastRequest, renderContainerPage, expectComponents, respondWithHtml,
|
||||
model, containerPage, requests,
|
||||
mockContainerPage = readFixtures('mock/mock-container-page.underscore'),
|
||||
ABTestFixture = readFixtures('mock/mock-container-xblock.underscore');
|
||||
mockContainerXBlockHtml = readFixtures('mock/mock-container-xblock.underscore'),
|
||||
mockXBlockEditorHtml = readFixtures('mock/mock-xblock-editor.underscore');
|
||||
|
||||
beforeEach(function () {
|
||||
edit_helpers.installEditTemplates();
|
||||
@@ -20,6 +20,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers"
|
||||
});
|
||||
containerPage = new ContainerPage({
|
||||
model: model,
|
||||
templates: edit_helpers.mockComponentTemplates,
|
||||
el: $('#content')
|
||||
});
|
||||
});
|
||||
@@ -43,7 +44,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers"
|
||||
|
||||
expectComponents = function (container, locators) {
|
||||
// verify expected components (in expected order) by their locators
|
||||
var components = $(container).find('[data-locator]');
|
||||
var components = $(container).find('.studio-xblock-wrapper');
|
||||
expect(components.length).toBe(locators.length);
|
||||
_.each(locators, function(locator, locator_index) {
|
||||
expect($(components[locator_index]).data('locator')).toBe(locator);
|
||||
@@ -51,8 +52,6 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers"
|
||||
};
|
||||
|
||||
describe("Basic display", function() {
|
||||
var mockContainerXBlockHtml = readFixtures('mock/mock-container-xblock.underscore');
|
||||
|
||||
it('can render itself', function() {
|
||||
renderContainerPage(mockContainerXBlockHtml, this);
|
||||
expect(containerPage.$el.select('.xblock-header')).toBeTruthy();
|
||||
@@ -69,9 +68,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers"
|
||||
});
|
||||
|
||||
describe("Editing an xblock", function() {
|
||||
var mockContainerXBlockHtml,
|
||||
mockXBlockEditorHtml,
|
||||
newDisplayName = 'New Display Name';
|
||||
var newDisplayName = 'New Display Name';
|
||||
|
||||
beforeEach(function () {
|
||||
edit_helpers.installMockXBlock({
|
||||
@@ -87,9 +84,6 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers"
|
||||
edit_helpers.cancelModalIfShowing();
|
||||
});
|
||||
|
||||
mockContainerXBlockHtml = readFixtures('mock/mock-container-xblock.underscore');
|
||||
mockXBlockEditorHtml = readFixtures('mock/mock-xblock-editor.underscore');
|
||||
|
||||
it('can show an edit modal for a child xblock', function() {
|
||||
var editButtons;
|
||||
renderContainerPage(mockContainerXBlockHtml, this);
|
||||
@@ -110,8 +104,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers"
|
||||
});
|
||||
|
||||
describe("Editing an xmodule", function() {
|
||||
var mockContainerXBlockHtml,
|
||||
mockXModuleEditor,
|
||||
var mockXModuleEditor = readFixtures('mock/mock-xmodule-editor.underscore'),
|
||||
newDisplayName = 'New Display Name';
|
||||
|
||||
beforeEach(function () {
|
||||
@@ -128,9 +121,6 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers"
|
||||
edit_helpers.cancelModalIfShowing();
|
||||
});
|
||||
|
||||
mockContainerXBlockHtml = readFixtures('mock/mock-container-xblock.underscore');
|
||||
mockXModuleEditor = readFixtures('mock/mock-xmodule-editor.underscore');
|
||||
|
||||
it('can save changes to settings', function() {
|
||||
var editButtons, modal, mockUpdatedXBlockHtml;
|
||||
mockUpdatedXBlockHtml = readFixtures('mock/mock-updated-xblock.underscore');
|
||||
@@ -165,43 +155,32 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers"
|
||||
});
|
||||
|
||||
describe("Empty container", function() {
|
||||
var mockContainerXBlockHtml = readFixtures('mock/mock-empty-container-xblock.underscore');
|
||||
var mockEmptyContainerXBlockHtml = readFixtures('mock/mock-empty-container-xblock.underscore');
|
||||
|
||||
it('shows the "no children" message', function() {
|
||||
renderContainerPage(mockContainerXBlockHtml, this);
|
||||
renderContainerPage(mockEmptyContainerXBlockHtml, this);
|
||||
expect(containerPage.$('.no-container-content')).not.toHaveClass('is-hidden');
|
||||
expect(containerPage.$('.wrapper-xblock')).toHaveClass('is-hidden');
|
||||
});
|
||||
});
|
||||
|
||||
describe("xblock operations", function() {
|
||||
var getGroupElement, expectNumComponents, expectNotificationToBeShown,
|
||||
var getGroupElement, expectNumComponents,
|
||||
NUM_GROUPS = 2, NUM_COMPONENTS_PER_GROUP = 3, GROUP_TO_TEST = "A",
|
||||
notificationSpies,
|
||||
allComponentsInGroup = _.map(
|
||||
_.range(NUM_COMPONENTS_PER_GROUP),
|
||||
function(index) { return 'locator-component-' + GROUP_TO_TEST + (index + 1); }
|
||||
);
|
||||
|
||||
beforeEach(function () {
|
||||
notificationSpies = spyOnConstructor(Notification, "Mini", ["show", "hide"]);
|
||||
notificationSpies.show.andReturn(notificationSpies);
|
||||
});
|
||||
|
||||
getGroupElement = function() {
|
||||
return containerPage.$("[data-locator='locator-group-" + GROUP_TO_TEST + "']");
|
||||
};
|
||||
|
||||
expectNumComponents = function(numComponents) {
|
||||
expect(containerPage.$('.wrapper-xblock.level-element').length).toBe(
|
||||
numComponents * NUM_GROUPS
|
||||
);
|
||||
};
|
||||
expectNotificationToBeShown = function(expectedTitle) {
|
||||
expect(notificationSpies.constructor).toHaveBeenCalled();
|
||||
expect(notificationSpies.show).toHaveBeenCalled();
|
||||
expect(notificationSpies.hide).not.toHaveBeenCalled();
|
||||
expect(notificationSpies.constructor.mostRecentCall.args[0].title).toMatch(expectedTitle);
|
||||
};
|
||||
|
||||
describe("Deleting an xblock", function() {
|
||||
var clickDelete, deleteComponent, deleteComponentWithSuccess,
|
||||
@@ -212,7 +191,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers"
|
||||
promptSpies.show.andReturn(this.promptSpies);
|
||||
});
|
||||
|
||||
clickDelete = function(componentIndex) {
|
||||
clickDelete = function(componentIndex, clickNo) {
|
||||
|
||||
// find all delete buttons for the given group
|
||||
var deleteButtons = getGroupElement().find(".delete-button");
|
||||
@@ -226,21 +205,18 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers"
|
||||
|
||||
// no components should be deleted yet
|
||||
expectNumComponents(NUM_COMPONENTS_PER_GROUP);
|
||||
|
||||
// click 'Yes' or 'No' on delete confirmation
|
||||
if (clickNo) {
|
||||
promptSpies.constructor.mostRecentCall.args[0].actions.secondary.click(promptSpies);
|
||||
} else {
|
||||
promptSpies.constructor.mostRecentCall.args[0].actions.primary.click(promptSpies);
|
||||
}
|
||||
};
|
||||
|
||||
deleteComponent = function(componentIndex, responseCode) {
|
||||
|
||||
// click delete button for given component
|
||||
deleteComponent = function(componentIndex) {
|
||||
clickDelete(componentIndex);
|
||||
|
||||
// click 'Yes' on delete confirmation
|
||||
promptSpies.constructor.mostRecentCall.args[0].actions.primary.click(promptSpies);
|
||||
|
||||
// expect 'deleting' notification to be shown
|
||||
expectNotificationToBeShown(/Deleting/);
|
||||
|
||||
// respond to request with given response code
|
||||
lastRequest().respond(responseCode, {}, "");
|
||||
create_sinon.respondWithJson(requests, {});
|
||||
|
||||
// expect request URL to contain given component's id
|
||||
expect(lastRequest().url).toMatch(
|
||||
@@ -249,12 +225,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers"
|
||||
};
|
||||
|
||||
deleteComponentWithSuccess = function(componentIndex) {
|
||||
|
||||
// delete component with an 'OK' response code
|
||||
deleteComponent(componentIndex, 200);
|
||||
|
||||
// expect 'deleting' notification to be hidden
|
||||
expect(notificationSpies.hide).toHaveBeenCalled();
|
||||
deleteComponent(componentIndex);
|
||||
|
||||
// verify the new list of components within the group
|
||||
expectComponents(
|
||||
@@ -263,32 +234,29 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers"
|
||||
);
|
||||
};
|
||||
|
||||
it("deletes first xblock", function() {
|
||||
renderContainerPage(ABTestFixture, this);
|
||||
it("can delete the first xblock", function() {
|
||||
renderContainerPage(mockContainerXBlockHtml, this);
|
||||
deleteComponentWithSuccess(0);
|
||||
});
|
||||
|
||||
it("deletes middle xblock", function() {
|
||||
renderContainerPage(ABTestFixture, this);
|
||||
it("can delete a middle xblock", function() {
|
||||
renderContainerPage(mockContainerXBlockHtml, this);
|
||||
deleteComponentWithSuccess(1);
|
||||
});
|
||||
|
||||
it("deletes last xblock", function() {
|
||||
renderContainerPage(ABTestFixture, this);
|
||||
it("can delete the last xblock", function() {
|
||||
renderContainerPage(mockContainerXBlockHtml, this);
|
||||
deleteComponentWithSuccess(NUM_COMPONENTS_PER_GROUP - 1);
|
||||
});
|
||||
|
||||
it('does not delete xblock when clicking No in prompt', function () {
|
||||
it('does not delete when clicking No in prompt', function () {
|
||||
var numRequests;
|
||||
|
||||
renderContainerPage(ABTestFixture, this);
|
||||
renderContainerPage(mockContainerXBlockHtml, this);
|
||||
numRequests = requests.length;
|
||||
|
||||
// click delete on the first component
|
||||
clickDelete(0);
|
||||
|
||||
// click 'No' on delete confirmation
|
||||
promptSpies.constructor.mostRecentCall.args[0].actions.secondary.click(promptSpies);
|
||||
// click delete on the first component but press no
|
||||
clickDelete(0, true);
|
||||
|
||||
// all components should still exist
|
||||
expectComponents(getGroupElement(), allComponentsInGroup);
|
||||
@@ -297,11 +265,23 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers"
|
||||
expect(requests.length).toBe(numRequests);
|
||||
});
|
||||
|
||||
it('does not delete xblock upon failure', function () {
|
||||
renderContainerPage(ABTestFixture, this);
|
||||
deleteComponent(0, 500);
|
||||
it('shows a notification during the delete operation', function() {
|
||||
var notificationSpy = edit_helpers.createNotificationSpy();
|
||||
renderContainerPage(mockContainerXBlockHtml, this);
|
||||
clickDelete(0);
|
||||
edit_helpers.verifyNotificationShowing(notificationSpy, /Deleting/);
|
||||
create_sinon.respondWithJson(requests, {});
|
||||
edit_helpers.verifyNotificationHidden(notificationSpy);
|
||||
});
|
||||
|
||||
it('does not delete an xblock upon failure', function () {
|
||||
var notificationSpy = edit_helpers.createNotificationSpy();
|
||||
renderContainerPage(mockContainerXBlockHtml, this);
|
||||
clickDelete(0);
|
||||
edit_helpers.verifyNotificationShowing(notificationSpy, /Deleting/);
|
||||
create_sinon.respondWithError(requests);
|
||||
edit_helpers.verifyNotificationShowing(notificationSpy, /Deleting/);
|
||||
expectComponents(getGroupElement(), allComponentsInGroup);
|
||||
expect(notificationSpies.hide).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -329,16 +309,8 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers"
|
||||
// click duplicate button for given component
|
||||
clickDuplicate(componentIndex);
|
||||
|
||||
// expect 'duplicating' notification to be shown
|
||||
expectNotificationToBeShown(/Duplicating/);
|
||||
|
||||
// verify content of request
|
||||
request = lastRequest();
|
||||
request.respond(
|
||||
responseCode,
|
||||
{ "Content-Type": "application/json" },
|
||||
JSON.stringify({'locator': 'locator-duplicated-component'})
|
||||
);
|
||||
expect(request.url).toEqual("/xblock");
|
||||
expect(request.method).toEqual("POST");
|
||||
expect(JSON.parse(request.requestBody)).toEqual(
|
||||
@@ -349,6 +321,13 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers"
|
||||
'"}'
|
||||
)
|
||||
);
|
||||
|
||||
// send the response
|
||||
request.respond(
|
||||
responseCode,
|
||||
{ "Content-Type": "application/json" },
|
||||
JSON.stringify({'locator': 'locator-duplicated-component'})
|
||||
);
|
||||
};
|
||||
|
||||
duplicateComponentWithSuccess = function(componentIndex) {
|
||||
@@ -356,34 +335,117 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers"
|
||||
// duplicate component with an 'OK' response code
|
||||
duplicateComponentWithResponse(componentIndex, 200);
|
||||
|
||||
// expect 'duplicating' notification to be hidden
|
||||
expect(notificationSpies.hide).toHaveBeenCalled();
|
||||
|
||||
// expect parent container to be refreshed
|
||||
expect(refreshXBlockSpies).toHaveBeenCalled();
|
||||
};
|
||||
|
||||
it("duplicates first xblock", function() {
|
||||
renderContainerPage(ABTestFixture, this);
|
||||
it("can duplicate the first xblock", function() {
|
||||
renderContainerPage(mockContainerXBlockHtml, this);
|
||||
duplicateComponentWithSuccess(0);
|
||||
});
|
||||
|
||||
it("duplicates middle xblock", function() {
|
||||
renderContainerPage(ABTestFixture, this);
|
||||
it("can duplicate a middle xblock", function() {
|
||||
renderContainerPage(mockContainerXBlockHtml, this);
|
||||
duplicateComponentWithSuccess(1);
|
||||
});
|
||||
|
||||
it("duplicates last xblock", function() {
|
||||
renderContainerPage(ABTestFixture, this);
|
||||
it("can duplicate the last xblock", function() {
|
||||
renderContainerPage(mockContainerXBlockHtml, this);
|
||||
duplicateComponentWithSuccess(NUM_COMPONENTS_PER_GROUP - 1);
|
||||
});
|
||||
|
||||
it('does not duplicate xblock upon failure', function () {
|
||||
renderContainerPage(ABTestFixture, this);
|
||||
duplicateComponentWithResponse(0, 500);
|
||||
it('shows a notification when duplicating', function () {
|
||||
var notificationSpy = edit_helpers.createNotificationSpy();
|
||||
renderContainerPage(mockContainerXBlockHtml, this);
|
||||
clickDuplicate(0);
|
||||
edit_helpers.verifyNotificationShowing(notificationSpy, /Duplicating/);
|
||||
create_sinon.respondWithJson(requests, {"locator": "new_item"});
|
||||
edit_helpers.verifyNotificationHidden(notificationSpy);
|
||||
});
|
||||
|
||||
it('does not duplicate an xblock upon failure', function () {
|
||||
var notificationSpy = edit_helpers.createNotificationSpy();
|
||||
renderContainerPage(mockContainerXBlockHtml, this);
|
||||
clickDuplicate(0);
|
||||
edit_helpers.verifyNotificationShowing(notificationSpy, /Duplicating/);
|
||||
create_sinon.respondWithError(requests);
|
||||
expectComponents(getGroupElement(), allComponentsInGroup);
|
||||
expect(notificationSpies.hide).not.toHaveBeenCalled();
|
||||
expect(refreshXBlockSpies).not.toHaveBeenCalled();
|
||||
edit_helpers.verifyNotificationShowing(notificationSpy, /Duplicating/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createNewComponent ', function () {
|
||||
var clickNewComponent, verifyComponents;
|
||||
|
||||
clickNewComponent = function (index) {
|
||||
containerPage.$(".new-component .new-component-type a.single-template")[index].click();
|
||||
};
|
||||
|
||||
it('sends the correct JSON to the server', function () {
|
||||
renderContainerPage(mockContainerXBlockHtml, this);
|
||||
clickNewComponent(0);
|
||||
edit_helpers.verifyXBlockRequest(requests, {
|
||||
"category": "discussion",
|
||||
"type": "discussion",
|
||||
"parent_locator": "locator-group-A"
|
||||
});
|
||||
});
|
||||
|
||||
it('shows a notification while creating', function () {
|
||||
var notificationSpy = edit_helpers.createNotificationSpy();
|
||||
renderContainerPage(mockContainerXBlockHtml, this);
|
||||
clickNewComponent(0);
|
||||
edit_helpers.verifyNotificationShowing(notificationSpy, /Adding/);
|
||||
create_sinon.respondWithJson(requests, { });
|
||||
edit_helpers.verifyNotificationHidden(notificationSpy);
|
||||
});
|
||||
|
||||
it('does not insert component upon failure', function () {
|
||||
var requestCount;
|
||||
renderContainerPage(mockContainerXBlockHtml, this);
|
||||
clickNewComponent(0);
|
||||
requestCount = requests.length;
|
||||
create_sinon.respondWithError(requests);
|
||||
// No new requests should be made to refresh the view
|
||||
expect(requests.length).toBe(requestCount);
|
||||
expectComponents(getGroupElement(), allComponentsInGroup);
|
||||
});
|
||||
|
||||
describe('Template Picker', function() {
|
||||
var showTemplatePicker, verifyCreateHtmlComponent,
|
||||
mockXBlockHtml = readFixtures('mock/mock-xblock.underscore');
|
||||
|
||||
showTemplatePicker = function() {
|
||||
containerPage.$('.new-component .new-component-type a.multiple-templates')[0].click();
|
||||
};
|
||||
|
||||
verifyCreateHtmlComponent = function(test, templateIndex, expectedRequest) {
|
||||
var xblockCount;
|
||||
renderContainerPage(mockContainerXBlockHtml, test);
|
||||
showTemplatePicker();
|
||||
xblockCount = containerPage.$('.studio-xblock-wrapper').length;
|
||||
containerPage.$('.new-component-html a')[templateIndex].click();
|
||||
edit_helpers.verifyXBlockRequest(requests, expectedRequest);
|
||||
create_sinon.respondWithJson(requests, {"locator": "new_item"});
|
||||
respondWithHtml(mockXBlockHtml);
|
||||
expect(containerPage.$('.studio-xblock-wrapper').length).toBe(xblockCount + 1);
|
||||
};
|
||||
|
||||
it('can add an HTML component without a template', function() {
|
||||
verifyCreateHtmlComponent(this, 0, {
|
||||
"category": "html",
|
||||
"parent_locator": "locator-group-A"
|
||||
});
|
||||
});
|
||||
|
||||
it('can add an HTML component with a template', function() {
|
||||
verifyCreateHtmlComponent(this, 1, {
|
||||
"category": "html",
|
||||
"boilerplate" : "announcement.yaml",
|
||||
"parent_locator": "locator-group-A"
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,240 +1,178 @@
|
||||
define(["coffee/src/views/unit", "js/models/module_info", "js/spec_helpers/create_sinon", "js/views/feedback_notification",
|
||||
"jasmine-stealth"],
|
||||
function (UnitEditView, ModuleModel, create_sinon, NotificationView) {
|
||||
var verifyJSON = function (requests, json) {
|
||||
var request = requests[requests.length - 1];
|
||||
expect(request.url).toEqual("/xblock");
|
||||
expect(request.method).toEqual("POST");
|
||||
// There was a problem with order of returned parameters in strings.
|
||||
// Changed to compare objects instead strings.
|
||||
expect(JSON.parse(request.requestBody)).toEqual(JSON.parse(json));
|
||||
define(["jquery", "underscore", "jasmine", "coffee/src/views/unit", "js/models/module_info",
|
||||
"js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers", "jasmine-stealth"],
|
||||
function ($, _, jasmine, UnitEditView, ModuleModel, create_sinon, edit_helpers) {
|
||||
var requests, unitView, initialize, respondWithHtml, verifyComponents, i;
|
||||
|
||||
respondWithHtml = function(html, requestIndex) {
|
||||
create_sinon.respondWithJson(
|
||||
requests,
|
||||
{ html: html, "resources": [] },
|
||||
requestIndex
|
||||
);
|
||||
};
|
||||
|
||||
var verifyComponents = function (unit, locators) {
|
||||
initialize = function(test) {
|
||||
var mockXBlockHtml = readFixtures('mock/mock-unit-page-xblock.underscore'),
|
||||
model;
|
||||
requests = create_sinon.requests(test);
|
||||
model = new ModuleModel({
|
||||
id: 'unit_locator',
|
||||
state: 'draft'
|
||||
});
|
||||
unitView = new UnitEditView({
|
||||
el: $('.main-wrapper'),
|
||||
templates: edit_helpers.mockComponentTemplates,
|
||||
model: model
|
||||
});
|
||||
|
||||
// Respond with renderings for the two xblocks in the unit
|
||||
respondWithHtml(mockXBlockHtml, 0);
|
||||
respondWithHtml(mockXBlockHtml, 1);
|
||||
};
|
||||
|
||||
verifyComponents = function (unit, locators) {
|
||||
var components = unit.$(".component");
|
||||
expect(components.length).toBe(locators.length);
|
||||
for (var i=0; i < locators.length; i++) {
|
||||
for (i = 0; i < locators.length; i++) {
|
||||
expect($(components[i]).data('locator')).toBe(locators[i]);
|
||||
}
|
||||
};
|
||||
|
||||
var verifyNotification = function (notificationSpy, text, requests) {
|
||||
expect(notificationSpy.constructor).toHaveBeenCalled();
|
||||
expect(notificationSpy.show).toHaveBeenCalled();
|
||||
expect(notificationSpy.hide).not.toHaveBeenCalled();
|
||||
var options = notificationSpy.constructor.mostRecentCall.args[0];
|
||||
expect(options.title).toMatch(text);
|
||||
create_sinon.respondWithJson(requests, {"locator": "new_item"});
|
||||
expect(notificationSpy.hide).toHaveBeenCalled();
|
||||
};
|
||||
beforeEach(function() {
|
||||
edit_helpers.installMockXBlock();
|
||||
|
||||
describe('duplicateComponent ', function () {
|
||||
var duplicateFixture =
|
||||
'<div class="main-wrapper edit-state-draft" data-locator="unit_locator"> \
|
||||
<ol class="components"> \
|
||||
<li class="component" data-locator="loc_1"> \
|
||||
<div class="wrapper wrapper-component-editor"/> \
|
||||
<ul class="component-actions"> \
|
||||
<a href="#" data-tooltip="Duplicate" class="duplicate-button action-button"><i class="icon-copy"></i><span class="sr"></span>Duplicate</span></a> \
|
||||
</ul> \
|
||||
</li> \
|
||||
<li class="component" data-locator="loc_2"> \
|
||||
<div class="wrapper wrapper-component-editor"/> \
|
||||
<ul class="component-actions"> \
|
||||
<a href="#" data-tooltip="Duplicate" class="duplicate-button action-button"><i class="icon-copy"></i><span class="sr"></span>Duplicate</span></a> \
|
||||
</ul> \
|
||||
</li> \
|
||||
</ol> \
|
||||
</div>';
|
||||
|
||||
var unit;
|
||||
var clickDuplicate = function (index) {
|
||||
unit.$(".duplicate-button")[index].click();
|
||||
};
|
||||
beforeEach(function () {
|
||||
setFixtures(duplicateFixture);
|
||||
unit = new UnitEditView({
|
||||
el: $('.main-wrapper'),
|
||||
model: new ModuleModel({
|
||||
id: 'unit_locator',
|
||||
state: 'draft'
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
it('sends the correct JSON to the server', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
clickDuplicate(0);
|
||||
verifyJSON(requests, '{"duplicate_source_locator":"loc_1","parent_locator":"unit_locator"}');
|
||||
});
|
||||
|
||||
it('inserts duplicated component immediately after source upon success', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
clickDuplicate(0);
|
||||
create_sinon.respondWithJson(requests, {"locator": "duplicated_item"});
|
||||
verifyComponents(unit, ['loc_1', 'duplicated_item', 'loc_2']);
|
||||
});
|
||||
|
||||
it('inserts duplicated component at end if source at end', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
clickDuplicate(1);
|
||||
create_sinon.respondWithJson(requests, {"locator": "duplicated_item"});
|
||||
verifyComponents(unit, ['loc_1', 'loc_2', 'duplicated_item']);
|
||||
});
|
||||
|
||||
it('shows a notification while duplicating', function () {
|
||||
var notificationSpy = spyOnConstructor(NotificationView, "Mini", ["show", "hide"]);
|
||||
notificationSpy.show.andReturn(notificationSpy);
|
||||
|
||||
var requests = create_sinon.requests(this);
|
||||
clickDuplicate(0);
|
||||
verifyNotification(notificationSpy, /Duplicating/, requests);
|
||||
});
|
||||
|
||||
it('does not insert duplicated component upon failure', function () {
|
||||
var server = create_sinon.server(500, this);
|
||||
clickDuplicate(0);
|
||||
server.respond();
|
||||
verifyComponents(unit, ['loc_1', 'loc_2']);
|
||||
});
|
||||
// needed to stub out the ajax
|
||||
window.analytics = jasmine.createSpyObj('analytics', ['track']);
|
||||
window.course_location_analytics = jasmine.createSpy('course_location_analytics');
|
||||
window.unit_location_analytics = jasmine.createSpy('unit_location_analytics');
|
||||
});
|
||||
describe('saveNewComponent ', function () {
|
||||
var newComponentFixture =
|
||||
'<div class="main-wrapper edit-state-draft" data-locator="unit_locator"> \
|
||||
<ol class="components"> \
|
||||
<li class="component" data-locator="loc_1"> \
|
||||
<div class="wrapper wrapper-component-editor"/> \
|
||||
</li> \
|
||||
<li class="component" data-locator="loc_2"> \
|
||||
<div class="wrapper wrapper-component-editor"/> \
|
||||
</li> \
|
||||
<li class="new-component-item adding"> \
|
||||
<div class="new-component"> \
|
||||
<ul class="new-component-type"> \
|
||||
<li> \
|
||||
<a href="#" class="single-template" data-type="discussion" data-category="discussion"/> \
|
||||
</li> \
|
||||
</ul> \
|
||||
</div> \
|
||||
</li> \
|
||||
</ol> \
|
||||
</div>';
|
||||
|
||||
var unit;
|
||||
var clickNewComponent = function () {
|
||||
unit.$(".new-component .new-component-type a.single-template").click();
|
||||
};
|
||||
beforeEach(function () {
|
||||
setFixtures(newComponentFixture);
|
||||
unit = new UnitEditView({
|
||||
el: $('.main-wrapper'),
|
||||
model: new ModuleModel({
|
||||
id: 'unit_locator',
|
||||
state: 'draft'
|
||||
})
|
||||
});
|
||||
});
|
||||
it('sends the correct JSON to the server', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
clickNewComponent();
|
||||
verifyJSON(requests, '{"category":"discussion","type":"discussion","parent_locator":"unit_locator"}');
|
||||
});
|
||||
|
||||
it('inserts new component at end', function () {
|
||||
var requests = create_sinon.requests(this);
|
||||
clickNewComponent();
|
||||
create_sinon.respondWithJson(requests, {"locator": "new_item"});
|
||||
verifyComponents(unit, ['loc_1', 'loc_2', 'new_item']);
|
||||
});
|
||||
|
||||
it('shows a notification while creating', function () {
|
||||
var notificationSpy = spyOnConstructor(NotificationView, "Mini", ["show", "hide"]);
|
||||
notificationSpy.show.andReturn(notificationSpy);
|
||||
var requests = create_sinon.requests(this);
|
||||
clickNewComponent();
|
||||
verifyNotification(notificationSpy, /Adding/, requests);
|
||||
});
|
||||
|
||||
it('does not insert duplicated component upon failure', function () {
|
||||
var server = create_sinon.server(500, this);
|
||||
clickNewComponent();
|
||||
server.respond();
|
||||
verifyComponents(unit, ['loc_1', 'loc_2']);
|
||||
});
|
||||
afterEach(function () {
|
||||
edit_helpers.uninstallMockXBlock();
|
||||
});
|
||||
describe("Disabled edit/publish links during ajax call", function() {
|
||||
var unit,
|
||||
link,
|
||||
draft_states = [
|
||||
{
|
||||
state: "draft",
|
||||
selector: ".publish-draft"
|
||||
},
|
||||
{
|
||||
state: "public",
|
||||
selector: ".create-draft"
|
||||
}
|
||||
],
|
||||
editLinkFixture =
|
||||
'<div class="main-wrapper edit-state-draft" data-locator="unit_locator"> \
|
||||
<div class="unit-settings window"> \
|
||||
<h4 class="header">Unit Settings</h4> \
|
||||
<div class="window-contents"> \
|
||||
<div class="row published-alert"> \
|
||||
<p class="edit-draft-message"> \
|
||||
<a href="#" class="create-draft">edit a draft</a> \
|
||||
</p> \
|
||||
<p class="publish-draft-message"> \
|
||||
<a href="#" class="publish-draft">replace it with this draft</a> \
|
||||
</p> \
|
||||
</div> \
|
||||
</div> \
|
||||
</div> \
|
||||
</div>';
|
||||
function test_link_disabled_during_ajax_call(draft_state) {
|
||||
beforeEach(function () {
|
||||
setFixtures(editLinkFixture);
|
||||
unit = new UnitEditView({
|
||||
el: $('.main-wrapper'),
|
||||
model: new ModuleModel({
|
||||
id: 'unit_locator',
|
||||
state: draft_state['state']
|
||||
})
|
||||
|
||||
describe("UnitEditView", function() {
|
||||
beforeEach(function() {
|
||||
edit_helpers.installEditTemplates();
|
||||
appendSetFixtures(readFixtures('mock/mock-unit-page.underscore'));
|
||||
});
|
||||
|
||||
describe('duplicateComponent', function() {
|
||||
var clickDuplicate;
|
||||
|
||||
clickDuplicate = function (index) {
|
||||
unitView.$(".duplicate-button")[index].click();
|
||||
};
|
||||
|
||||
it('sends the correct JSON to the server', function () {
|
||||
initialize(this);
|
||||
clickDuplicate(0);
|
||||
edit_helpers.verifyXBlockRequest(requests, {
|
||||
"duplicate_source_locator": "loc_1",
|
||||
"parent_locator": "unit_locator"
|
||||
});
|
||||
// needed to stub out the ajax
|
||||
window.analytics = jasmine.createSpyObj('analytics', ['track']);
|
||||
window.course_location_analytics = jasmine.createSpy('course_location_analytics');
|
||||
window.unit_location_analytics = jasmine.createSpy('unit_location_analytics');
|
||||
});
|
||||
|
||||
it("reenables the " + draft_state['selector'] + " link once the ajax call returns", function() {
|
||||
runs(function(){
|
||||
spyOn($, "ajax").andCallThrough();
|
||||
spyOn($.fn, 'addClass').andCallThrough();
|
||||
spyOn($.fn, 'removeClass').andCallThrough();
|
||||
link = $(draft_state['selector']);
|
||||
it('inserts duplicated component immediately after source upon success', function () {
|
||||
initialize(this);
|
||||
clickDuplicate(0);
|
||||
create_sinon.respondWithJson(requests, {"locator": "duplicated_item"});
|
||||
verifyComponents(unitView, ['loc_1', 'duplicated_item', 'loc_2']);
|
||||
});
|
||||
|
||||
it('inserts duplicated component at end if source at end', function () {
|
||||
initialize(this);
|
||||
clickDuplicate(1);
|
||||
create_sinon.respondWithJson(requests, {"locator": "duplicated_item"});
|
||||
verifyComponents(unitView, ['loc_1', 'loc_2', 'duplicated_item']);
|
||||
});
|
||||
|
||||
it('shows a notification while duplicating', function () {
|
||||
var notificationSpy = edit_helpers.createNotificationSpy();
|
||||
initialize(this);
|
||||
clickDuplicate(0);
|
||||
edit_helpers.verifyNotificationShowing(notificationSpy, /Duplicating/);
|
||||
create_sinon.respondWithJson(requests, {"locator": "new_item"});
|
||||
edit_helpers.verifyNotificationHidden(notificationSpy);
|
||||
});
|
||||
|
||||
it('does not insert duplicated component upon failure', function () {
|
||||
initialize(this);
|
||||
clickDuplicate(0);
|
||||
create_sinon.respondWithError(requests);
|
||||
verifyComponents(unitView, ['loc_1', 'loc_2']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createNewComponent ', function () {
|
||||
var clickNewComponent;
|
||||
|
||||
clickNewComponent = function () {
|
||||
unitView.$(".new-component .new-component-type a.single-template").click();
|
||||
};
|
||||
|
||||
it('sends the correct JSON to the server', function () {
|
||||
initialize(this);
|
||||
clickNewComponent();
|
||||
edit_helpers.verifyXBlockRequest(requests, {
|
||||
"category": "discussion",
|
||||
"type": "discussion",
|
||||
"parent_locator": "unit_locator"
|
||||
});
|
||||
});
|
||||
|
||||
it('inserts new component at end', function () {
|
||||
initialize(this);
|
||||
clickNewComponent();
|
||||
create_sinon.respondWithJson(requests, {"locator": "new_item"});
|
||||
verifyComponents(unitView, ['loc_1', 'loc_2', 'new_item']);
|
||||
});
|
||||
|
||||
it('shows a notification while creating', function () {
|
||||
var notificationSpy = edit_helpers.createNotificationSpy();
|
||||
initialize(this);
|
||||
clickNewComponent();
|
||||
edit_helpers.verifyNotificationShowing(notificationSpy, /Adding/);
|
||||
create_sinon.respondWithJson(requests, {"locator": "new_item"});
|
||||
edit_helpers.verifyNotificationHidden(notificationSpy);
|
||||
});
|
||||
|
||||
it('does not insert new component upon failure', function () {
|
||||
initialize(this);
|
||||
clickNewComponent();
|
||||
create_sinon.respondWithError(requests);
|
||||
verifyComponents(unitView, ['loc_1', 'loc_2']);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Disabled edit/publish links during ajax call", function() {
|
||||
var link, i,
|
||||
draft_states = [
|
||||
{
|
||||
state: "draft",
|
||||
selector: ".publish-draft"
|
||||
},
|
||||
{
|
||||
state: "public",
|
||||
selector: ".create-draft"
|
||||
}
|
||||
];
|
||||
|
||||
function test_link_disabled_during_ajax_call(draft_state) {
|
||||
it("re-enables the " + draft_state.selector + " link once the ajax call returns", function() {
|
||||
initialize(this);
|
||||
link = $(draft_state.selector);
|
||||
expect(link).not.toHaveClass('is-disabled');
|
||||
link.click();
|
||||
expect(link).toHaveClass('is-disabled');
|
||||
create_sinon.respondWithError(requests);
|
||||
expect(link).not.toHaveClass('is-disabled');
|
||||
});
|
||||
waitsFor(function(){
|
||||
// wait for "is-disabled" to be removed as a class
|
||||
return !($(draft_state['selector']).hasClass("is-disabled"));
|
||||
}, 500);
|
||||
runs(function(){
|
||||
// check that the `is-disabled` class was added and removed
|
||||
expect($.fn.addClass).toHaveBeenCalledWith("is-disabled");
|
||||
expect($.fn.removeClass).toHaveBeenCalledWith("is-disabled");
|
||||
}
|
||||
|
||||
// make sure the link finishes without the `is-disabled` class
|
||||
expect(link).not.toHaveClass("is-disabled");
|
||||
|
||||
// affirm that ajax was called
|
||||
expect($.ajax).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
};
|
||||
for (var i = 0; i < draft_states.length; i++) {
|
||||
test_link_disabled_during_ajax_call(draft_states[i]);
|
||||
};
|
||||
for (i = 0; i < draft_states.length; i++) {
|
||||
test_link_disabled_during_ajax_call(draft_states[i]);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -29,7 +29,7 @@ define([ "jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helper
|
||||
var mockXBlockEditorHtml;
|
||||
|
||||
beforeEach(function () {
|
||||
edit_helpers.installMockXBlock(mockSaveResponse);
|
||||
edit_helpers.installMockXBlock();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
define(["sinon"], function(sinon) {
|
||||
define(["sinon", "underscore"], function(sinon, _) {
|
||||
var fakeServer, fakeRequests, respondWithJson, respondWithError;
|
||||
|
||||
/* These utility methods are used by Jasmine tests to create a mock server or
|
||||
@@ -46,14 +46,18 @@ define(["sinon"], function(sinon) {
|
||||
};
|
||||
|
||||
respondWithJson = function(requests, jsonResponse, requestIndex) {
|
||||
requestIndex = requestIndex || requests.length - 1;
|
||||
if (_.isUndefined(requestIndex)) {
|
||||
requestIndex = requests.length - 1;
|
||||
}
|
||||
requests[requestIndex].respond(200,
|
||||
{ "Content-Type": "application/json" },
|
||||
JSON.stringify(jsonResponse));
|
||||
};
|
||||
|
||||
respondWithError = function(requests, requestIndex) {
|
||||
requestIndex = requestIndex || requests.length - 1;
|
||||
if (_.isUndefined(requestIndex)) {
|
||||
requestIndex = requests.length - 1;
|
||||
}
|
||||
requests[requestIndex].respond(500,
|
||||
{ "Content-Type": "application/json" },
|
||||
JSON.stringify({ }));
|
||||
|
||||
@@ -2,22 +2,14 @@
|
||||
* Provides helper methods for invoking Studio editors in Jasmine tests.
|
||||
*/
|
||||
define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers/modal_helpers",
|
||||
"js/views/modals/edit_xblock", "xmodule", "coffee/src/main", "xblock/cms.runtime.v1"],
|
||||
function($, _, create_sinon, modal_helpers, EditXBlockModal) {
|
||||
"js/views/modals/edit_xblock", "js/collections/component_template",
|
||||
"xmodule", "coffee/src/main", "xblock/cms.runtime.v1"],
|
||||
function($, _, create_sinon, modal_helpers, EditXBlockModal, ComponentTemplates) {
|
||||
|
||||
var editorTemplate = readFixtures('metadata-editor.underscore'),
|
||||
numberEntryTemplate = readFixtures('metadata-number-entry.underscore'),
|
||||
stringEntryTemplate = readFixtures('metadata-string-entry.underscore'),
|
||||
editXBlockModalTemplate = readFixtures('edit-xblock-modal.underscore'),
|
||||
editorModeButtonTemplate = readFixtures('editor-mode-button.underscore'),
|
||||
installMockXBlock,
|
||||
uninstallMockXBlock,
|
||||
installMockXModule,
|
||||
uninstallMockXModule,
|
||||
installEditTemplates,
|
||||
showEditModal;
|
||||
var installMockXBlock, uninstallMockXBlock, installMockXModule, uninstallMockXModule,
|
||||
mockComponentTemplates, installEditTemplates, showEditModal, verifyXBlockRequest;
|
||||
|
||||
installMockXBlock = function(mockResult) {
|
||||
installMockXBlock = function() {
|
||||
window.MockXBlock = function(runtime, element) {
|
||||
return {
|
||||
runtime: runtime
|
||||
@@ -41,17 +33,52 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers
|
||||
window.MockDescriptor = null;
|
||||
};
|
||||
|
||||
mockComponentTemplates = new ComponentTemplates([
|
||||
{
|
||||
templates: [
|
||||
{
|
||||
category: 'discussion',
|
||||
display_name: 'Discussion'
|
||||
}],
|
||||
type: 'discussion'
|
||||
}, {
|
||||
"templates": [
|
||||
{
|
||||
"category": "html",
|
||||
"boilerplate_name": null,
|
||||
"display_name": "Text"
|
||||
}, {
|
||||
"category": "html",
|
||||
"boilerplate_name": "announcement.yaml",
|
||||
"display_name": "Announcement"
|
||||
}, {
|
||||
"category": "html",
|
||||
"boilerplate_name": "raw.yaml",
|
||||
"display_name": "Raw HTML"
|
||||
}],
|
||||
"type": "html"
|
||||
}],
|
||||
{
|
||||
parse: true
|
||||
});
|
||||
|
||||
installEditTemplates = function(append) {
|
||||
modal_helpers.installModalTemplates(append);
|
||||
|
||||
// Add templates needed by the add XBlock menu
|
||||
modal_helpers.installTemplate('add-xblock-component');
|
||||
modal_helpers.installTemplate('add-xblock-component-button');
|
||||
modal_helpers.installTemplate('add-xblock-component-menu');
|
||||
modal_helpers.installTemplate('add-xblock-component-menu-problem');
|
||||
|
||||
// Add templates needed by the edit XBlock modal
|
||||
appendSetFixtures($("<script>", { id: "edit-xblock-modal-tpl", type: "text/template" }).text(editXBlockModalTemplate));
|
||||
appendSetFixtures($("<script>", { id: "editor-mode-button-tpl", type: "text/template" }).text(editorModeButtonTemplate));
|
||||
modal_helpers.installTemplate('edit-xblock-modal');
|
||||
modal_helpers.installTemplate('editor-mode-button');
|
||||
|
||||
// Add templates needed by the settings editor
|
||||
appendSetFixtures($("<script>", {id: "metadata-editor-tpl", type: "text/template"}).text(editorTemplate));
|
||||
appendSetFixtures($("<script>", {id: "metadata-number-entry", type: "text/template"}).text(numberEntryTemplate));
|
||||
appendSetFixtures($("<script>", {id: "metadata-string-entry", type: "text/template"}).text(stringEntryTemplate));
|
||||
modal_helpers.installTemplate('metadata-editor');
|
||||
modal_helpers.installTemplate('metadata-number-entry');
|
||||
modal_helpers.installTemplate('metadata-string-entry');
|
||||
};
|
||||
|
||||
showEditModal = function(requests, xblockElement, model, mockHtml, options) {
|
||||
@@ -64,12 +91,22 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers
|
||||
return modal;
|
||||
};
|
||||
|
||||
verifyXBlockRequest = function (requests, expectedJson) {
|
||||
var request = requests[requests.length - 1],
|
||||
actualJson = JSON.parse(request.requestBody);
|
||||
expect(request.url).toEqual("/xblock");
|
||||
expect(request.method).toEqual("POST");
|
||||
expect(actualJson).toEqual(expectedJson);
|
||||
};
|
||||
|
||||
return $.extend(modal_helpers, {
|
||||
'installMockXBlock': installMockXBlock,
|
||||
'uninstallMockXBlock': uninstallMockXBlock,
|
||||
'installMockXModule': installMockXModule,
|
||||
'uninstallMockXModule': uninstallMockXModule,
|
||||
'mockComponentTemplates': mockComponentTemplates,
|
||||
'installEditTemplates': installEditTemplates,
|
||||
'showEditModal': showEditModal
|
||||
'showEditModal': showEditModal,
|
||||
'verifyXBlockRequest': verifyXBlockRequest
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,10 +3,7 @@
|
||||
*/
|
||||
define(["jquery", "js/spec_helpers/view_helpers"],
|
||||
function($, view_helpers) {
|
||||
var basicModalTemplate = readFixtures('basic-modal.underscore'),
|
||||
modalButtonTemplate = readFixtures('modal-button.underscore'),
|
||||
feedbackTemplate = readFixtures('system-feedback.underscore'),
|
||||
installModalTemplates,
|
||||
var installModalTemplates,
|
||||
getModalElement,
|
||||
isShowingModal,
|
||||
hideModalIfShowing,
|
||||
@@ -15,8 +12,8 @@ define(["jquery", "js/spec_helpers/view_helpers"],
|
||||
|
||||
installModalTemplates = function(append) {
|
||||
view_helpers.installViewTemplates(append);
|
||||
appendSetFixtures($("<script>", { id: "basic-modal-tpl", type: "text/template" }).text(basicModalTemplate));
|
||||
appendSetFixtures($("<script>", { id: "modal-button-tpl", type: "text/template" }).text(modalButtonTemplate));
|
||||
view_helpers.installTemplate('basic-modal');
|
||||
view_helpers.installTemplate('modal-button');
|
||||
};
|
||||
|
||||
getModalElement = function(modal) {
|
||||
|
||||
@@ -1,20 +1,49 @@
|
||||
/**
|
||||
* Provides helper methods for invoking Studio modal windows in Jasmine tests.
|
||||
*/
|
||||
define(["jquery"],
|
||||
function($) {
|
||||
var feedbackTemplate = readFixtures('system-feedback.underscore'),
|
||||
installViewTemplates;
|
||||
define(["jquery", "js/views/feedback_notification", "js/spec_helpers/create_sinon"],
|
||||
function($, NotificationView, create_sinon) {
|
||||
var installTemplate, installViewTemplates, createNotificationSpy, verifyNotificationShowing,
|
||||
verifyNotificationHidden;
|
||||
|
||||
installViewTemplates = function(append) {
|
||||
if (append) {
|
||||
appendSetFixtures($("<script>", { id: "system-feedback-tpl", type: "text/template" }).text(feedbackTemplate));
|
||||
installTemplate = function(templateName, isFirst) {
|
||||
var template = readFixtures(templateName + '.underscore'),
|
||||
templateId = templateName + '-tpl';
|
||||
if (isFirst) {
|
||||
setFixtures($("<script>", { id: templateId, type: "text/template" }).text(template));
|
||||
} else {
|
||||
setFixtures($("<script>", { id: "system-feedback-tpl", type: "text/template" }).text(feedbackTemplate));
|
||||
appendSetFixtures($("<script>", { id: templateId, type: "text/template" }).text(template));
|
||||
}
|
||||
};
|
||||
|
||||
installViewTemplates = function(append) {
|
||||
installTemplate('system-feedback', !append);
|
||||
appendSetFixtures('<div id="page-notification"></div>');
|
||||
};
|
||||
|
||||
createNotificationSpy = function() {
|
||||
var notificationSpy = spyOnConstructor(NotificationView, "Mini", ["show", "hide"]);
|
||||
notificationSpy.show.andReturn(notificationSpy);
|
||||
return notificationSpy;
|
||||
};
|
||||
|
||||
verifyNotificationShowing = function(notificationSpy, text) {
|
||||
expect(notificationSpy.constructor).toHaveBeenCalled();
|
||||
expect(notificationSpy.show).toHaveBeenCalled();
|
||||
expect(notificationSpy.hide).not.toHaveBeenCalled();
|
||||
var options = notificationSpy.constructor.mostRecentCall.args[0];
|
||||
expect(options.title).toMatch(text);
|
||||
};
|
||||
|
||||
verifyNotificationHidden = function(notificationSpy) {
|
||||
expect(notificationSpy.hide).toHaveBeenCalled();
|
||||
};
|
||||
|
||||
return {
|
||||
'installViewTemplates': installViewTemplates
|
||||
'installTemplate': installTemplate,
|
||||
'installViewTemplates': installViewTemplates,
|
||||
'createNotificationSpy': createNotificationSpy,
|
||||
'verifyNotificationShowing': verifyNotificationShowing,
|
||||
'verifyNotificationHidden': verifyNotificationHidden
|
||||
};
|
||||
});
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
* getUpdateUrl: a utility method that returns the xblock update URL, appending
|
||||
* the location if passed in.
|
||||
*/
|
||||
define([], function () {
|
||||
define(["underscore"], function (_) {
|
||||
var urlRoot = '/xblock';
|
||||
|
||||
var getUpdateUrl = function (locator) {
|
||||
if (locator === undefined) {
|
||||
if (_.isUndefined(locator)) {
|
||||
return urlRoot;
|
||||
}
|
||||
else {
|
||||
|
||||
20
cms/static/js/utils/templates.js
Normal file
20
cms/static/js/utils/templates.js
Normal file
@@ -0,0 +1,20 @@
|
||||
define(["jquery", "underscore"], function($, _) {
|
||||
|
||||
/**
|
||||
* Loads the named template from the page, or logs an error if it fails.
|
||||
* @param name The name of the template.
|
||||
* @returns The loaded template.
|
||||
*/
|
||||
var loadTemplate = function(name) {
|
||||
var templateSelector = "#" + name + "-tpl",
|
||||
templateText = $(templateSelector).text();
|
||||
if (!templateText) {
|
||||
console.error("Failed to load " + name + " template");
|
||||
}
|
||||
return _.template(templateText);
|
||||
};
|
||||
|
||||
return {
|
||||
loadTemplate: loadTemplate
|
||||
};
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
define(["jquery", "underscore", "backbone", "js/utils/handle_iframe_binding"],
|
||||
function ($, _, Backbone, IframeUtils) {
|
||||
define(["jquery", "underscore", "backbone", "gettext", "js/utils/handle_iframe_binding", "js/utils/templates",
|
||||
"js/views/feedback_notification", "js/views/feedback_prompt"],
|
||||
function ($, _, Backbone, gettext, IframeUtils, TemplateUtils, NotificationView, PromptView) {
|
||||
/*
|
||||
This view is extended from backbone to provide useful functionality for all Studio views.
|
||||
This functionality includes:
|
||||
@@ -60,16 +61,60 @@ define(["jquery", "underscore", "backbone", "js/utils/handle_iframe_binding"],
|
||||
$('.ui-loading').hide();
|
||||
},
|
||||
|
||||
/**
|
||||
* Confirms with the user whether to run an operation or not, and then runs it if desired.
|
||||
*/
|
||||
confirmThenRunOperation: function(title, message, actionLabel, operation) {
|
||||
var self = this;
|
||||
return new PromptView.Warning({
|
||||
title: title,
|
||||
message: message,
|
||||
actions: {
|
||||
primary: {
|
||||
text: actionLabel,
|
||||
click: function(prompt) {
|
||||
prompt.hide();
|
||||
operation();
|
||||
}
|
||||
},
|
||||
secondary: {
|
||||
text: gettext('Cancel'),
|
||||
click: function(prompt) {
|
||||
return prompt.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
}).show();
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows a progress message for the duration of an asynchronous operation.
|
||||
* Note: this does not remove the notification upon failure because an error
|
||||
* will be shown that shouldn't be removed.
|
||||
* @param message The message to show.
|
||||
* @param operation A function that returns a promise representing the operation.
|
||||
*/
|
||||
runOperationShowingMessage: function(message, operation) {
|
||||
var notificationView;
|
||||
notificationView = new NotificationView.Mini({
|
||||
title: gettext(message)
|
||||
});
|
||||
notificationView.show();
|
||||
return operation().done(function() {
|
||||
notificationView.hide();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Disables a given element when a given operation is running.
|
||||
* @param {jQuery} element: the element to be disabled.
|
||||
* @param operation: the operation during whose duration the
|
||||
* element should be disabled. The operation should return
|
||||
* a jquery promise.
|
||||
* a JQuery promise.
|
||||
*/
|
||||
disableElementWhileRunning: function(element, operation) {
|
||||
element.addClass("is-disabled");
|
||||
operation().always(function() {
|
||||
return operation().always(function() {
|
||||
element.removeClass("is-disabled");
|
||||
});
|
||||
},
|
||||
@@ -80,12 +125,38 @@ define(["jquery", "underscore", "backbone", "js/utils/handle_iframe_binding"],
|
||||
* @returns The loaded template.
|
||||
*/
|
||||
loadTemplate: function(name) {
|
||||
var templateSelector = "#" + name + "-tpl",
|
||||
templateText = $(templateSelector).text();
|
||||
if (!templateText) {
|
||||
console.error("Failed to load " + name + " template");
|
||||
}
|
||||
return _.template(templateText);
|
||||
return TemplateUtils.loadTemplate(name);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the relative position that the element is scrolled from the top of the view port.
|
||||
* @param element The element in question.
|
||||
*/
|
||||
getScrollOffset: function(element) {
|
||||
var elementTop = element.offset().top;
|
||||
return elementTop - $(window).scrollTop();
|
||||
},
|
||||
|
||||
/**
|
||||
* Scrolls the window so that the element is scrolled down to the specified relative position
|
||||
* from the top of the view port.
|
||||
* @param element The element in question.
|
||||
* @param offset The amount by which the element should be scrolled from the top of the view port.
|
||||
*/
|
||||
setScrollOffset: function(element, offset) {
|
||||
var elementTop = element.offset().top,
|
||||
newScrollTop = elementTop - offset;
|
||||
this.setScrollTop(newScrollTop);
|
||||
},
|
||||
|
||||
/**
|
||||
* Performs an animated scroll so that the window has the specified scroll top.
|
||||
* @param scrollTop The desired scroll top for the window.
|
||||
*/
|
||||
setScrollTop: function(scrollTop) {
|
||||
$('html, body').animate({
|
||||
scrollTop: scrollTop
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
74
cms/static/js/views/components/add_xblock.js
Normal file
74
cms/static/js/views/components/add_xblock.js
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* This is a simple component that renders add buttons for all available XBlock template types.
|
||||
*/
|
||||
define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/components/add_xblock_button",
|
||||
"js/views/components/add_xblock_menu"],
|
||||
function ($, _, gettext, BaseView, AddXBlockButton, AddXBlockMenu) {
|
||||
var AddXBlockComponent = BaseView.extend({
|
||||
events: {
|
||||
'click .new-component .new-component-type a.multiple-templates': 'showComponentTemplates',
|
||||
'click .new-component .new-component-type a.single-template': 'createNewComponent',
|
||||
'click .new-component .cancel-button': 'closeNewComponent',
|
||||
'click .new-component-templates .new-component-template a': 'createNewComponent',
|
||||
'click .new-component-templates .cancel-button': 'closeNewComponent'
|
||||
},
|
||||
|
||||
initialize: function(options) {
|
||||
BaseView.prototype.initialize.call(this, options);
|
||||
this.template = this.loadTemplate('add-xblock-component');
|
||||
},
|
||||
|
||||
render: function () {
|
||||
if (!this.$el.html()) {
|
||||
var that = this;
|
||||
this.$el.html(this.template({}));
|
||||
this.collection.each(
|
||||
function (componentModel) {
|
||||
var view, menu;
|
||||
|
||||
view = new AddXBlockButton({model: componentModel});
|
||||
that.$el.find('.new-component-type').append(view.render().el);
|
||||
|
||||
menu = new AddXBlockMenu({model: componentModel});
|
||||
that.$el.append(menu.render().el);
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
showComponentTemplates: function(event) {
|
||||
var type;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
type = $(event.currentTarget).data('type');
|
||||
this.$('.new-component').slideUp(250);
|
||||
this.$('.new-component-' + type).slideDown(250);
|
||||
},
|
||||
|
||||
closeNewComponent: function(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.$('.new-component').slideDown(250);
|
||||
this.$('.new-component-templates').slideUp(250);
|
||||
},
|
||||
|
||||
createNewComponent: function(event) {
|
||||
var self = this,
|
||||
element = $(event.currentTarget),
|
||||
saveData = element.data(),
|
||||
oldOffset = this.getScrollOffset(this.$el);
|
||||
event.preventDefault();
|
||||
this.closeNewComponent(event);
|
||||
this.runOperationShowingMessage(
|
||||
gettext('Adding…'),
|
||||
_.bind(this.options.createComponent, this, saveData, element)
|
||||
).always(function() {
|
||||
// Restore the scroll position of the buttons so that the new
|
||||
// component appears above them.
|
||||
self.setScrollOffset(self.$el, oldOffset);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return AddXBlockComponent;
|
||||
}); // end define();
|
||||
13
cms/static/js/views/components/add_xblock_button.js
Normal file
13
cms/static/js/views/components/add_xblock_button.js
Normal file
@@ -0,0 +1,13 @@
|
||||
define(["js/views/baseview"],
|
||||
function (BaseView) {
|
||||
|
||||
return BaseView.extend({
|
||||
tagName: "li",
|
||||
initialize: function () {
|
||||
BaseView.prototype.initialize.call(this);
|
||||
this.template = this.loadTemplate("add-xblock-component-button");
|
||||
this.$el.html(this.template({type: this.model.type, templates: this.model.templates}));
|
||||
}
|
||||
});
|
||||
|
||||
}); // end define();
|
||||
19
cms/static/js/views/components/add_xblock_menu.js
Normal file
19
cms/static/js/views/components/add_xblock_menu.js
Normal file
@@ -0,0 +1,19 @@
|
||||
define(["jquery", "js/views/baseview"],
|
||||
function ($, BaseView) {
|
||||
|
||||
return BaseView.extend({
|
||||
className: function () {
|
||||
return "new-component-templates new-component-" + this.model.type;
|
||||
},
|
||||
initialize: function () {
|
||||
BaseView.prototype.initialize.call(this);
|
||||
var template_name = this.model.type === "problem" ? "add-xblock-component-menu-problem" :
|
||||
"add-xblock-component-menu";
|
||||
this.template = this.loadTemplate(template_name);
|
||||
this.$el.html(this.template({type: this.model.type, templates: this.model.templates}));
|
||||
// Make the tabs on problems into "real tabs"
|
||||
this.$('.tab-group').tabs();
|
||||
}
|
||||
});
|
||||
|
||||
}); // end define();
|
||||
@@ -1,10 +1,13 @@
|
||||
define(["jquery", "underscore", "js/views/xblock", "js/utils/module", "gettext", "js/views/feedback_notification"],
|
||||
function ($, _, XBlockView, ModuleUtils, gettext, NotificationView) {
|
||||
var reorderableClass = '.reorderable-container',
|
||||
studioXBlockWrapperClass = '.studio-xblock-wrapper';
|
||||
|
||||
var ContainerView = XBlockView.extend({
|
||||
|
||||
xblockReady: function () {
|
||||
XBlockView.prototype.xblockReady.call(this);
|
||||
var verticalContainer = this.$('.vertical-container'),
|
||||
var reorderableContainer = this.$(reorderableClass),
|
||||
alreadySortable = this.$('.ui-sortable'),
|
||||
newParent,
|
||||
oldParent,
|
||||
@@ -12,13 +15,13 @@ define(["jquery", "underscore", "js/views/xblock", "js/utils/module", "gettext",
|
||||
|
||||
alreadySortable.sortable("destroy");
|
||||
|
||||
verticalContainer.sortable({
|
||||
reorderableContainer.sortable({
|
||||
handle: '.drag-handle',
|
||||
|
||||
stop: function (event, ui) {
|
||||
var saving, hideSaving, removeFromParent;
|
||||
|
||||
if (oldParent === undefined) {
|
||||
if (_.isUndefined(oldParent)) {
|
||||
// If no actual change occurred,
|
||||
// oldParent will never have been set.
|
||||
return;
|
||||
@@ -55,7 +58,7 @@ define(["jquery", "underscore", "js/views/xblock", "js/utils/module", "gettext",
|
||||
// be null if the change is related to the list the element
|
||||
// was originally in (the case of a move within the same container
|
||||
// or the deletion from a container when moving to a new container).
|
||||
var parent = $(event.target).closest('.wrapper-xblock');
|
||||
var parent = $(event.target).closest(studioXBlockWrapperClass);
|
||||
if (ui.sender) {
|
||||
// Move to a new container (the addition part).
|
||||
newParent = parent;
|
||||
@@ -69,8 +72,8 @@ define(["jquery", "underscore", "js/views/xblock", "js/utils/module", "gettext",
|
||||
placeholder: 'component-placeholder',
|
||||
forcePlaceholderSize: true,
|
||||
axis: 'y',
|
||||
items: '> .vertical-element',
|
||||
connectWith: ".vertical-container",
|
||||
items: '> .is-draggable',
|
||||
connectWith: reorderableClass,
|
||||
tolerance: "pointer"
|
||||
|
||||
});
|
||||
@@ -79,10 +82,10 @@ define(["jquery", "underscore", "js/views/xblock", "js/utils/module", "gettext",
|
||||
reorder: function (targetParent, successCallback) {
|
||||
var children, childLocators;
|
||||
|
||||
// Find descendants with class "wrapper-xblock" whose parent == targetParent.
|
||||
// Find descendants with class "studio-xblock-wrapper" whose parent === targetParent.
|
||||
// This is necessary to filter our grandchildren, great-grandchildren, etc.
|
||||
children = targetParent.find('.wrapper-xblock').filter(function () {
|
||||
var parent = $(this).parent().closest('.wrapper-xblock');
|
||||
children = targetParent.find(studioXBlockWrapperClass).filter(function () {
|
||||
var parent = $(this).parent().closest(studioXBlockWrapperClass);
|
||||
return parent.data('locator') === targetParent.data('locator');
|
||||
});
|
||||
|
||||
@@ -107,7 +110,10 @@ define(["jquery", "underscore", "js/views/xblock", "js/utils/module", "gettext",
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function() {
|
||||
this.$(reorderableClass).sortable('refresh');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
define(["js/views/baseview", "underscore", "underscore.string", "jquery"], function(BaseView, _, str, $) {
|
||||
var SystemFeedback = BaseView.extend({
|
||||
options: {
|
||||
title: "",
|
||||
message: "",
|
||||
intent: null, // "warning", "confirmation", "error", "announcement", "step-required", etc
|
||||
type: null, // "alert", "notification", or "prompt": set by subclass
|
||||
shown: true, // is this view currently being shown?
|
||||
icon: true, // should we render an icon related to the message intent?
|
||||
closeIcon: true, // should we render a close button in the top right corner?
|
||||
minShown: 0, // length of time after this view has been shown before it can be hidden (milliseconds)
|
||||
maxShown: Infinity // length of time after this view has been shown before it will be automatically hidden (milliseconds)
|
||||
define(["jquery", "underscore", "underscore.string", "backbone", "js/utils/templates"],
|
||||
function($, _, str, Backbone, TemplateUtils) {
|
||||
var SystemFeedback = Backbone.View.extend({
|
||||
options: {
|
||||
title: "",
|
||||
message: "",
|
||||
intent: null, // "warning", "confirmation", "error", "announcement", "step-required", etc
|
||||
type: null, // "alert", "notification", or "prompt": set by subclass
|
||||
shown: true, // is this view currently being shown?
|
||||
icon: true, // should we render an icon related to the message intent?
|
||||
closeIcon: true, // should we render a close button in the top right corner?
|
||||
minShown: 0, // length of time after this view has been shown before it can be hidden (milliseconds)
|
||||
maxShown: Infinity // length of time after this view has been shown before it will be automatically hidden (milliseconds)
|
||||
|
||||
/* Could also have an "actions" hash: here is an example demonstrating
|
||||
the expected structure. For each action, by default the framework
|
||||
@@ -38,100 +39,108 @@ define(["js/views/baseview", "underscore", "underscore.string", "jquery"], funct
|
||||
]
|
||||
}
|
||||
*/
|
||||
},
|
||||
initialize: function() {
|
||||
if(!this.options.type) {
|
||||
throw "SystemFeedback: type required (given " +
|
||||
JSON.stringify(this.options) + ")";
|
||||
}
|
||||
if(!this.options.intent) {
|
||||
throw "SystemFeedback: intent required (given " +
|
||||
JSON.stringify(this.options) + ")";
|
||||
}
|
||||
this.template = this.loadTemplate("system-feedback");
|
||||
this.setElement($("#page-"+this.options.type));
|
||||
// handle single "secondary" action
|
||||
if (this.options.actions && this.options.actions.secondary &&
|
||||
!_.isArray(this.options.actions.secondary)) {
|
||||
this.options.actions.secondary = [this.options.actions.secondary];
|
||||
}
|
||||
return this;
|
||||
},
|
||||
// public API: show() and hide()
|
||||
show: function() {
|
||||
clearTimeout(this.hideTimeout);
|
||||
this.options.shown = true;
|
||||
this.shownAt = new Date();
|
||||
this.render();
|
||||
if($.isNumeric(this.options.maxShown)) {
|
||||
this.hideTimeout = setTimeout(_.bind(this.hide, this),
|
||||
this.options.maxShown);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
hide: function() {
|
||||
if(this.shownAt && $.isNumeric(this.options.minShown) &&
|
||||
this.options.minShown > new Date() - this.shownAt)
|
||||
{
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
if (!this.options.type) {
|
||||
throw "SystemFeedback: type required (given " +
|
||||
JSON.stringify(this.options) + ")";
|
||||
}
|
||||
if (!this.options.intent) {
|
||||
throw "SystemFeedback: intent required (given " +
|
||||
JSON.stringify(this.options) + ")";
|
||||
}
|
||||
this.template = TemplateUtils.loadTemplate("system-feedback");
|
||||
this.setElement($("#page-" + this.options.type));
|
||||
// handle single "secondary" action
|
||||
if (this.options.actions && this.options.actions.secondary &&
|
||||
!_.isArray(this.options.actions.secondary)) {
|
||||
this.options.actions.secondary = [this.options.actions.secondary];
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
// public API: show() and hide()
|
||||
show: function() {
|
||||
clearTimeout(this.hideTimeout);
|
||||
this.hideTimeout = setTimeout(_.bind(this.hide, this),
|
||||
this.options.minShown - (new Date() - this.shownAt));
|
||||
} else {
|
||||
this.options.shown = false;
|
||||
delete this.shownAt;
|
||||
this.options.shown = true;
|
||||
this.shownAt = new Date();
|
||||
this.render();
|
||||
if ($.isNumeric(this.options.maxShown)) {
|
||||
this.hideTimeout = setTimeout(_.bind(this.hide, this),
|
||||
this.options.maxShown);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
hide: function() {
|
||||
if (this.shownAt && $.isNumeric(this.options.minShown) &&
|
||||
this.options.minShown > new Date() - this.shownAt) {
|
||||
clearTimeout(this.hideTimeout);
|
||||
this.hideTimeout = setTimeout(_.bind(this.hide, this),
|
||||
this.options.minShown - (new Date() - this.shownAt));
|
||||
} else {
|
||||
this.options.shown = false;
|
||||
delete this.shownAt;
|
||||
this.render();
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
// the rest of the API should be considered semi-private
|
||||
events: {
|
||||
"click .action-close": "hide",
|
||||
"click .action-primary": "primaryClick",
|
||||
"click .action-secondary": "secondaryClick"
|
||||
},
|
||||
|
||||
render: function() {
|
||||
// there can be only one active view of a given type at a time: only
|
||||
// one alert, only one notification, only one prompt. Therefore, we'll
|
||||
// use a singleton approach.
|
||||
var singleton = SystemFeedback["active_" + this.options.type];
|
||||
if (singleton && singleton !== this) {
|
||||
singleton.stopListening();
|
||||
singleton.undelegateEvents();
|
||||
}
|
||||
this.$el.html(this.template(this.options));
|
||||
SystemFeedback["active_" + this.options.type] = this;
|
||||
return this;
|
||||
},
|
||||
|
||||
primaryClick: function(event) {
|
||||
var actions, primary;
|
||||
actions = this.options.actions;
|
||||
if (!actions) { return; }
|
||||
primary = actions.primary;
|
||||
if (!primary) { return; }
|
||||
if (primary.preventDefault !== false) {
|
||||
event.preventDefault();
|
||||
}
|
||||
if (primary.click) {
|
||||
primary.click.call(event.target, this, event);
|
||||
}
|
||||
},
|
||||
|
||||
secondaryClick: function(event) {
|
||||
var actions, secondaryList, secondary, i;
|
||||
actions = this.options.actions;
|
||||
if (!actions) { return; }
|
||||
secondaryList = actions.secondary;
|
||||
if (!secondaryList) { return; }
|
||||
// which secondary action was clicked?
|
||||
i = 0; // default to the first secondary action (easier for testing)
|
||||
if (event && event.target) {
|
||||
i = _.indexOf(this.$(".action-secondary"), event.target);
|
||||
}
|
||||
secondary = secondaryList[i];
|
||||
if (secondary.preventDefault !== false) {
|
||||
event.preventDefault();
|
||||
}
|
||||
if (secondary.click) {
|
||||
secondary.click.call(event.target, this, event);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
},
|
||||
// the rest of the API should be considered semi-private
|
||||
events: {
|
||||
"click .action-close": "hide",
|
||||
"click .action-primary": "primaryClick",
|
||||
"click .action-secondary": "secondaryClick"
|
||||
},
|
||||
render: function() {
|
||||
// there can be only one active view of a given type at a time: only
|
||||
// one alert, only one notification, only one prompt. Therefore, we'll
|
||||
// use a singleton approach.
|
||||
var singleton = SystemFeedback["active_"+this.options.type];
|
||||
if(singleton && singleton !== this) {
|
||||
singleton.stopListening();
|
||||
singleton.undelegateEvents();
|
||||
}
|
||||
this.$el.html(this.template(this.options));
|
||||
SystemFeedback["active_"+this.options.type] = this;
|
||||
return this;
|
||||
},
|
||||
primaryClick: function(event) {
|
||||
var actions = this.options.actions;
|
||||
if(!actions) { return; }
|
||||
var primary = actions.primary;
|
||||
if(!primary) { return; }
|
||||
if(primary.preventDefault !== false) {
|
||||
event.preventDefault();
|
||||
}
|
||||
if(primary.click) {
|
||||
primary.click.call(event.target, this, event);
|
||||
}
|
||||
},
|
||||
secondaryClick: function(event) {
|
||||
var actions = this.options.actions;
|
||||
if(!actions) { return; }
|
||||
var secondaryList = actions.secondary;
|
||||
if(!secondaryList) { return; }
|
||||
// which secondary action was clicked?
|
||||
var i = 0; // default to the first secondary action (easier for testing)
|
||||
if(event && event.target) {
|
||||
i = _.indexOf(this.$(".action-secondary"), event.target);
|
||||
}
|
||||
var secondary = secondaryList[i];
|
||||
if(secondary.preventDefault !== false) {
|
||||
event.preventDefault();
|
||||
}
|
||||
if(secondary.click) {
|
||||
secondary.click.call(event.target, this, event);
|
||||
}
|
||||
}
|
||||
});
|
||||
return SystemFeedback;
|
||||
});
|
||||
return SystemFeedback;
|
||||
});
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
/**
|
||||
* XBlockContainerView is used to display an xblock which has children, and allows the
|
||||
* user to interact with the children.
|
||||
* 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/feedback_notification", "js/views/feedback_prompt", "js/views/baseview", "js/views/container", "js/views/xblock", "js/views/modals/edit_xblock", "js/models/xblock_info"],
|
||||
function ($, _, gettext, NotificationView, PromptView, BaseView, ContainerView, XBlockView, EditXBlockModal, XBlockInfo) {
|
||||
|
||||
var XBlockContainerView = BaseView.extend({
|
||||
define(["jquery", "underscore", "gettext", "js/views/feedback_notification",
|
||||
"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, NotificationView, BaseView, ContainerView, XBlockView, AddXBlockComponent,
|
||||
EditXBlockModal, XBlockInfo) {
|
||||
var XBlockContainerPage = BaseView.extend({
|
||||
// takes XBlockInfo as a model
|
||||
|
||||
view: 'container_preview',
|
||||
@@ -39,7 +41,8 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification", "js
|
||||
success: function(xblock) {
|
||||
if (xblockView.hasChildXBlocks()) {
|
||||
xblockView.$el.removeClass('is-hidden');
|
||||
self.addButtonActions(xblockView.$el);
|
||||
self.renderAddXBlockComponents();
|
||||
self.onXBlockRefresh(xblockView);
|
||||
} else {
|
||||
noContentElement.removeClass('is-hidden');
|
||||
}
|
||||
@@ -50,137 +53,176 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification", "js
|
||||
},
|
||||
|
||||
findXBlockElement: function(target) {
|
||||
return $(target).closest('[data-locator]');
|
||||
return $(target).closest('.studio-xblock-wrapper');
|
||||
},
|
||||
|
||||
getURLRoot: function() {
|
||||
return this.xblockView.model.urlRoot;
|
||||
},
|
||||
|
||||
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) {
|
||||
var modal,
|
||||
target = event.target,
|
||||
xblockElement = self.findXBlockElement(target);
|
||||
event.preventDefault();
|
||||
modal = new EditXBlockModal({ });
|
||||
modal.edit(xblockElement, self.model,
|
||||
{
|
||||
refresh: function(xblockInfo) {
|
||||
self.refreshXBlock(xblockInfo, xblockElement);
|
||||
}
|
||||
});
|
||||
self.editComponent(self.findXBlockElement(event.target));
|
||||
});
|
||||
element.find('.duplicate-button').click(function(event) {
|
||||
event.preventDefault();
|
||||
self.duplicateComponent(
|
||||
self.findXBlockElement(event.target)
|
||||
);
|
||||
self.duplicateComponent(self.findXBlockElement(event.target));
|
||||
});
|
||||
element.find('.delete-button').click(function(event) {
|
||||
event.preventDefault();
|
||||
self.deleteComponent(
|
||||
self.findXBlockElement(event.target)
|
||||
);
|
||||
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></div>').appendTo(listPanel),
|
||||
requestData = _.extend(template, {
|
||||
parent_locator: parentLocator
|
||||
});
|
||||
return $.postJSON(this.getURLRoot(), requestData,
|
||||
_.bind(this.onNewXBlock, this, placeholderElement, scrollOffset));
|
||||
},
|
||||
|
||||
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()),
|
||||
duplicating = new NotificationView.Mini({
|
||||
title: gettext('Duplicating…')
|
||||
parent = xblockElement.parent();
|
||||
this.runOperationShowingMessage(gettext('Duplicating…'),
|
||||
function() {
|
||||
var scrollOffset = self.getScrollOffset(xblockElement),
|
||||
placeholderElement = $('<div></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));
|
||||
});
|
||||
|
||||
duplicating.show();
|
||||
return $.postJSON(self.getURLRoot(), {
|
||||
duplicate_source_locator: xblockElement.data('locator'),
|
||||
parent_locator: parentElement.data('locator')
|
||||
}, function(data) {
|
||||
// copy the element
|
||||
var duplicatedElement = xblockElement.clone(false);
|
||||
|
||||
// place it after the original element
|
||||
xblockElement.after(duplicatedElement);
|
||||
|
||||
// update its locator id
|
||||
duplicatedElement.attr('data-locator', data.locator);
|
||||
|
||||
// have it refresh itself
|
||||
self.refreshXBlockElement(duplicatedElement);
|
||||
|
||||
// hide the notification
|
||||
duplicating.hide();
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
deleteComponent: function(xblockElement) {
|
||||
var self = this, deleting;
|
||||
return new PromptView.Warning({
|
||||
title: gettext('Delete this component?'),
|
||||
message: gettext('Deleting this component is permanent and cannot be undone.'),
|
||||
actions: {
|
||||
primary: {
|
||||
text: gettext('Yes, delete this component'),
|
||||
click: function(prompt) {
|
||||
prompt.hide();
|
||||
deleting = new NotificationView.Mini({
|
||||
title: gettext('Deleting…')
|
||||
});
|
||||
deleting.show();
|
||||
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') + "?" +
|
||||
$.param({recurse: true, all_versions: true})
|
||||
url: self.getURLRoot() + "/" +
|
||||
xblockElement.data('locator') + "?" +
|
||||
$.param({recurse: true, all_versions: true})
|
||||
}).success(function() {
|
||||
deleting.hide();
|
||||
xblockElement.remove();
|
||||
});
|
||||
}
|
||||
},
|
||||
secondary: {
|
||||
text: gettext('Cancel'),
|
||||
click: function(prompt) {
|
||||
return prompt.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
}).show();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
refreshXBlockElement: function(xblockElement) {
|
||||
this.refreshXBlock(
|
||||
new XBlockInfo({
|
||||
id: xblockElement.data('locator')
|
||||
}),
|
||||
xblockElement
|
||||
);
|
||||
onNewXBlock: function(xblockElement, scrollOffset, data) {
|
||||
this.setScrollOffset(xblockElement, scrollOffset);
|
||||
xblockElement.data('locator', data.locator);
|
||||
return this.refreshXBlock(xblockElement);
|
||||
},
|
||||
|
||||
refreshXBlock: function(xblockInfo, xblockElement) {
|
||||
var self = this, temporaryView;
|
||||
/**
|
||||
* 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 xblockElement The element representing the xblock to be refreshed.
|
||||
*/
|
||||
refreshXBlock: function(xblockElement) {
|
||||
var parentElement = xblockElement.parent(),
|
||||
rootLocator = this.xblockView.model.id,
|
||||
xblockLocator = xblockElement.data('locator');
|
||||
if (xblockLocator === rootLocator) {
|
||||
this.render();
|
||||
} 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 XBlock
|
||||
// around the child element so that it can be refreshed.
|
||||
temporaryView = new XBlockView({
|
||||
el: xblockElement,
|
||||
model: xblockInfo,
|
||||
view: this.view
|
||||
// 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.render({
|
||||
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
|
||||
self.addButtonActions(xblockElement);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return XBlockContainerView;
|
||||
return XBlockContainerPage;
|
||||
}); // end define();
|
||||
|
||||
@@ -74,12 +74,23 @@ define(["jquery", "underscore", "js/views/baseview", "xblock/runtime.v1"],
|
||||
if (!element) {
|
||||
element = this.$el;
|
||||
}
|
||||
// First render the HTML as the scripts might depend upon it
|
||||
element.html(html);
|
||||
// Now asynchronously add the resources to the page
|
||||
|
||||
// Render the HTML first as the scripts might depend upon it, and then
|
||||
// asynchronously add the resources to the page.
|
||||
this.updateHtml(element, html);
|
||||
return this.addXBlockFragmentResources(resources);
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates an element to have the specified HTML. The default method sets the HTML
|
||||
* as child content, but this can be overridden.
|
||||
* @param element The element to be updated
|
||||
* @param html The desired HTML.
|
||||
*/
|
||||
updateHtml: function(element, html) {
|
||||
element.html(html);
|
||||
},
|
||||
|
||||
/**
|
||||
* Dynamically loads all of an XBlock's dependent resources. This is an asynchronous
|
||||
* process so a promise is returned.
|
||||
|
||||
Reference in New Issue
Block a user