diff --git a/cms/static/coffee/spec/main.coffee b/cms/static/coffee/spec/main.coffee
index 59df0f722b..e3df6fd672 100644
--- a/cms/static/coffee/spec/main.coffee
+++ b/cms/static/coffee/spec/main.coffee
@@ -227,6 +227,7 @@ define([
"js/spec/views/unit_outline_spec",
"js/spec/views/xblock_spec",
"js/spec/views/xblock_editor_spec",
+ "js/spec/views/xblock_string_field_editor_spec",
"js/spec/views/pages/container_spec",
"js/spec/views/pages/container_subviews_spec",
diff --git a/cms/static/js/spec/views/pages/container_spec.js b/cms/static/js/spec/views/pages/container_spec.js
index c1db0bb0e1..cd93a86fc6 100644
--- a/cms/static/js/spec/views/pages/container_spec.js
+++ b/cms/static/js/spec/views/pages/container_spec.js
@@ -95,7 +95,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
describe("Editing the container", function() {
var updatedDisplayName = 'Updated Test Container',
- expectEditCanceled, getDisplayNameWrapper;
+ getDisplayNameWrapper;
afterEach(function() {
edit_helpers.cancelModalIfShowing();
@@ -105,24 +105,6 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
return containerPage.$('.wrapper-xblock-field');
};
- expectEditCanceled = function(test, options) {
- var initialRequests, displayNameWrapper, displayNameInput;
- renderContainerPage(test, mockContainerXBlockHtml);
- displayNameWrapper = getDisplayNameWrapper();
- initialRequests = requests.length;
- displayNameInput = edit_helpers.inlineEdit(displayNameWrapper, options.newTitle);
- if (options.pressEscape) {
- displayNameInput.simulate("keydown", { keyCode: $.simulate.keyCode.ESCAPE });
- displayNameInput.simulate("keyup", { keyCode: $.simulate.keyCode.ESCAPE });
- } else {
- displayNameInput.change();
- }
- // No requests should be made when the edit is cancelled client-side
- expect(initialRequests).toBe(requests.length);
- edit_helpers.verifyInlineEditChange(displayNameWrapper, initialDisplayName);
- expect(containerPage.model.get('display_name')).toBe(initialDisplayName);
- };
-
it('can edit itself', function() {
var editButtons, displayNameElement;
renderContainerPage(this, mockContainerXBlockHtml);
@@ -173,46 +155,6 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
edit_helpers.verifyInlineEditChange(displayNameWrapper, updatedDisplayName);
expect(containerPage.model.get('display_name')).toBe(updatedDisplayName);
});
-
- it('does not change the title when a display name update fails', function() {
- var initialRequests, displayNameInput, displayNameWrapper;
- renderContainerPage(this, mockContainerXBlockHtml);
- displayNameWrapper = getDisplayNameWrapper();
- displayNameInput = edit_helpers.inlineEdit(displayNameWrapper, updatedDisplayName);
- initialRequests = requests.length;
- displayNameInput.change();
- create_sinon.respondWithError(requests);
- // No fetch operation should occur.
- expect(initialRequests + 1).toBe(requests.length);
- edit_helpers.verifyInlineEditChange(displayNameWrapper, initialDisplayName, updatedDisplayName);
- expect(containerPage.model.get('display_name')).toBe(initialDisplayName);
- });
-
- it('trims whitespace from the display name', function() {
- var displayNameInput, displayNameWrapper;
- renderContainerPage(this, mockContainerXBlockHtml);
- displayNameWrapper = getDisplayNameWrapper();
- displayNameInput = edit_helpers.inlineEdit(displayNameWrapper, updatedDisplayName + ' ');
- displayNameInput.change();
- // This is the response for the change operation.
- create_sinon.respondWithJson(requests, { });
- // This is the response for the subsequent fetch operation.
- create_sinon.respondWithJson(requests, {"display_name": updatedDisplayName});
- edit_helpers.verifyInlineEditChange(displayNameWrapper, updatedDisplayName);
- expect(containerPage.model.get('display_name')).toBe(updatedDisplayName);
- });
-
- it('does not change the title when input is the empty string', function() {
- expectEditCanceled(this, {newTitle: ''});
- });
-
- it('does not change the title when input is whitespace-only', function() {
- expectEditCanceled(this, {newTitle: ' '});
- });
-
- it('can cancel an inline edit', function() {
- expectEditCanceled(this, {newTitle: updatedDisplayName, pressEscape: true});
- });
});
describe("Editing an xblock", function() {
diff --git a/cms/static/js/spec/views/xblock_string_field_editor_spec.js b/cms/static/js/spec/views/xblock_string_field_editor_spec.js
new file mode 100644
index 0000000000..1b7e9c8d01
--- /dev/null
+++ b/cms/static/js/spec/views/xblock_string_field_editor_spec.js
@@ -0,0 +1,155 @@
+define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers", "js/spec_helpers/edit_helpers", "js/models/xblock_info", "js/views/xblock_string_field_editor"],
+ function ($, create_sinon, view_helpers, edit_helpers, XBlockInfo, XBlockStringFieldEditor) {
+ describe("XBlockStringFieldEditorView", function () {
+ var initialDisplayName, updatedDisplayName, getXBlockInfo, getFieldEditorView;
+
+ getXBlockInfo = function (displayName) {
+ return new XBlockInfo(
+ {
+ display_name: displayName,
+ id: "my_xblock"
+ },
+ { parse: true }
+ );
+ };
+
+ getFieldEditorView = function (xblockInfo) {
+ if (xblockInfo === undefined) {
+ xblockInfo = getXBlockInfo(initialDisplayName);
+ }
+ return new XBlockStringFieldEditor({
+ model: xblockInfo,
+ el: $('.wrapper-xblock-field')
+ });
+ };
+
+ beforeEach(function () {
+ initialDisplayName = "Default Display Name";
+ updatedDisplayName = "Updated Display Name";
+ view_helpers.installTemplate('xblock-string-field-editor');
+ appendSetFixtures(
+ '
' +
+ '' +
+ '
'
+ );
+ });
+
+ describe('Editing', function () {
+ var expectPostedNewDisplayName, expectEditCanceled;
+
+ expectPostedNewDisplayName = function (requests, displayName) {
+ create_sinon.expectJsonRequest(requests, 'POST', '/xblock/my_xblock', {
+ metadata: {
+ display_name: displayName
+ }
+ });
+ };
+
+ expectEditCanceled = function (test, fieldEditorView, options) {
+ var requests, initialRequests, displayNameInput;
+ requests = create_sinon.requests(test);
+ initialRequests = requests.length;
+ displayNameInput = edit_helpers.inlineEdit(fieldEditorView.$el, options.newTitle);
+ if (options.pressEscape) {
+ displayNameInput.simulate("keydown", { keyCode: $.simulate.keyCode.ESCAPE });
+ displayNameInput.simulate("keyup", { keyCode: $.simulate.keyCode.ESCAPE });
+ } else if (options.clickCancel) {
+ fieldEditorView.$('button[name=cancel]').click();
+ } else {
+ displayNameInput.change();
+ }
+ // No requests should be made when the edit is cancelled client-side
+ expect(initialRequests).toBe(requests.length);
+ edit_helpers.verifyInlineEditChange(fieldEditorView.$el, initialDisplayName);
+ expect(fieldEditorView.model.get('display_name')).toBe(initialDisplayName);
+ };
+
+ it('can inline edit the display name', function () {
+ var requests, fieldEditorView;
+ requests = create_sinon.requests(this);
+ fieldEditorView = getFieldEditorView().render();
+ edit_helpers.inlineEdit(fieldEditorView.$el, updatedDisplayName);
+ fieldEditorView.$('button[name=submit]').click();
+ expectPostedNewDisplayName(requests, updatedDisplayName);
+ // This is the response for the change operation.
+ create_sinon.respondWithJson(requests, { });
+ // This is the response for the subsequent fetch operation.
+ create_sinon.respondWithJson(requests, {display_name: updatedDisplayName});
+ edit_helpers.verifyInlineEditChange(fieldEditorView.$el, updatedDisplayName);
+ });
+
+ it('does not change the title when a display name update fails', function () {
+ var requests, fieldEditorView, initialRequests;
+ requests = create_sinon.requests(this);
+ initialRequests = requests.length;
+ fieldEditorView = getFieldEditorView().render();
+ edit_helpers.inlineEdit(fieldEditorView.$el, updatedDisplayName);
+ fieldEditorView.$('button[name=submit]').click();
+ expectPostedNewDisplayName(requests, updatedDisplayName);
+ create_sinon.respondWithError(requests);
+ // No fetch operation should occur.
+ expect(initialRequests + 1).toBe(requests.length);
+ edit_helpers.verifyInlineEditChange(fieldEditorView.$el, initialDisplayName, updatedDisplayName);
+ });
+
+ it('trims whitespace from the display name', function () {
+ var requests, fieldEditorView;
+ requests = create_sinon.requests(this);
+ fieldEditorView = getFieldEditorView().render();
+ updatedDisplayName += ' ';
+ edit_helpers.inlineEdit(fieldEditorView.$el, updatedDisplayName);
+ fieldEditorView.$('button[name=submit]').click();
+ expectPostedNewDisplayName(requests, updatedDisplayName.trim());
+ // This is the response for the change operation.
+ create_sinon.respondWithJson(requests, { });
+ // This is the response for the subsequent fetch operation.
+ create_sinon.respondWithJson(requests, {display_name: updatedDisplayName.trim()});
+ edit_helpers.verifyInlineEditChange(fieldEditorView.$el, updatedDisplayName.trim());
+ });
+
+ it('does not change the title when input is the empty string', function () {
+ var fieldEditorView = getFieldEditorView().render();
+ expectEditCanceled(this, fieldEditorView, {newTitle: ''});
+ });
+
+ it('does not change the title when input is whitespace-only', function () {
+ var fieldEditorView = getFieldEditorView().render();
+ expectEditCanceled(this, fieldEditorView, {newTitle: ' '});
+ });
+
+ it('can cancel an inline edit by pressing escape', function () {
+ var fieldEditorView = getFieldEditorView().render();
+ expectEditCanceled(this, fieldEditorView, {newTitle: updatedDisplayName, pressEscape: true});
+ });
+
+ it('can cancel an inline edit by clicking cancel', function () {
+ var fieldEditorView = getFieldEditorView().render();
+ expectEditCanceled(this, fieldEditorView, {newTitle: updatedDisplayName, clickCancel: true});
+ });
+ });
+
+ describe('Rendering', function () {
+ var expectInputMatchesModelDisplayName = function (displayName) {
+ var fieldEditorView = getFieldEditorView(getXBlockInfo(displayName)).render();
+ expect(fieldEditorView.$('.xblock-field-input').val()).toBe(displayName);
+ };
+
+ it('renders single quotes in input field', function () {
+ expectInputMatchesModelDisplayName('Updated \'Display Name\'');
+ });
+
+ it('renders double quotes in input field', function () {
+ expectInputMatchesModelDisplayName('Updated "Display Name"');
+ });
+
+ it('renders open angle bracket in input field', function () {
+ expectInputMatchesModelDisplayName(updatedDisplayName + '<');
+ });
+
+ it('renders close angle bracket in input field', function () {
+ expectInputMatchesModelDisplayName('>' + updatedDisplayName);
+ });
+ });
+ });
+ });
diff --git a/cms/static/js/views/xblock_string_field_editor.js b/cms/static/js/views/xblock_string_field_editor.js
index 033fdd5497..4002bb0be9 100644
--- a/cms/static/js/views/xblock_string_field_editor.js
+++ b/cms/static/js/views/xblock_string_field_editor.js
@@ -11,7 +11,8 @@ define(["js/views/baseview", "js/views/utils/xblock_utils"],
var XBlockStringFieldEditor = BaseView.extend({
events: {
'click .xblock-field-value-edit': 'showInput',
- 'click button[type=submit]': 'onClickSubmit',
+ 'click button[name=submit]': 'onClickSubmit',
+ 'click button[name=cancel]': 'onClickCancel',
'change .xblock-field-input': 'updateField',
'focusout .xblock-field-input': 'onInputFocusLost',
'keyup .xblock-field-input': 'handleKeyUp'
@@ -29,7 +30,7 @@ define(["js/views/baseview", "js/views/utils/xblock_utils"],
render: function() {
this.$el.append(this.template({
- value: this.model.get(this.fieldName),
+ value: this.model.escape(this.fieldName),
fieldName: this.fieldName,
fieldDisplayName: this.fieldDisplayName
}));
@@ -56,6 +57,11 @@ define(["js/views/baseview", "js/views/utils/xblock_utils"],
this.updateField();
},
+ onClickCancel: function(event) {
+ event.preventDefault();
+ this.cancelInput();
+ },
+
onChangeField: function() {
var value = this.model.get(this.fieldName);
this.getLabel().text(value);