diff --git a/common/static/common/templates/discussion/customwmd-prompt.underscore b/common/static/common/templates/discussion/customwmd-prompt.underscore
new file mode 100644
index 0000000000..9a13d32509
--- /dev/null
+++ b/common/static/common/templates/discussion/customwmd-prompt.underscore
@@ -0,0 +1,51 @@
+
<%- title %>
+
+
diff --git a/common/test/acceptance/pages/lms/discussion.py b/common/test/acceptance/pages/lms/discussion.py
index 6a8275fb5d..f4bbb5a339 100644
--- a/common/test/acceptance/pages/lms/discussion.py
+++ b/common/test/acceptance/pages/lms/discussion.py
@@ -195,17 +195,58 @@ class DiscussionThreadPage(PageObject, DiscussionPageMixin):
"""Replace the contents of the response editor"""
self._find_within(".response_{} .discussion-response .wmd-input".format(response_id)).fill(new_body)
+ def verify_link_editor_error_messages_shown(self):
+ """
+ Confirm that the error messages are displayed in the editor.
+ """
+ def errors_visible():
+ """
+ Returns True if both errors are visible, False otherwise.
+ """
+ return (
+ self.q(css="#new-url-input-field-message.has-error").visible and
+ self.q(css="#new-url-desc-input-field-message.has-error").visible
+ )
+
+ self.wait_for(errors_visible, "Form errors should be visible.")
+
+ def add_content_via_editor_button(self, content_type, response_id, url, description, is_decorative=False):
+ """Replace the contents of the response editor"""
+ self._find_within(
+ "#wmd-{}-button-edit-post-body-{}".format(
+ content_type,
+ response_id,
+ )
+ ).click()
+ self.q(css='#new-url-input').fill(url)
+ self.q(css='#new-url-desc-input').fill(description)
+
+ if is_decorative:
+ self.q(css='#img-is-decorative').click()
+
+ self.q(css='input[value="OK"]').click()
+
def submit_response_edit(self, response_id, new_response_body):
"""Click the submit button on the response editor"""
- self._find_within(".response_{} .discussion-response .post-update".format(response_id)).first.click()
- EmptyPromise(
- lambda: (
+
+ def submit_response_check_func():
+ """
+ Tries to click "Update post" and returns True if the post
+ was successfully updated, False otherwise.
+ """
+ self._find_within(
+ ".response_{} .discussion-response .post-update".format(
+ response_id
+ )
+ ).first.click()
+
+ return (
not self.is_response_editor_visible(response_id) and
self.is_response_visible(response_id) and
self.get_response_body(response_id) == new_response_body
- ),
- "Comment edit succeeded"
- ).fulfill()
+ )
+
+ self.wait_for(submit_response_check_func, "Comment edit succeeded")
def is_show_comments_visible(self, response_id):
"""Returns true if the "show comments" link is visible for a response"""
diff --git a/common/test/acceptance/tests/discussion/test_discussion.py b/common/test/acceptance/tests/discussion/test_discussion.py
index 0d4b988288..def96e830e 100644
--- a/common/test/acceptance/tests/discussion/test_discussion.py
+++ b/common/test/acceptance/tests/discussion/test_discussion.py
@@ -449,6 +449,145 @@ class DiscussionResponseEditTest(BaseDiscussionTestCase):
page.set_response_editor_value(response_id, new_response)
page.submit_response_edit(response_id, new_response)
+ def test_edit_response_add_link(self):
+ """
+ Scenario: User submits valid input to the 'add link' form
+ Given I am editing a response on a discussion page
+ When I click the 'add link' icon in the editor toolbar
+ And enter a valid url to the URL input field
+ And enter a valid string in the Description input field
+ And click the 'OK' button
+ Then the edited response should contain the new link
+ """
+ self.setup_user()
+ self.setup_view()
+ page = self.create_single_thread_page("response_edit_test_thread")
+ page.visit()
+
+ response_id = "response_self_author"
+ url = "http://example.com"
+ description = "example"
+
+ page.start_response_edit(response_id)
+ page.set_response_editor_value(response_id, "")
+ page.add_content_via_editor_button(
+ "link", response_id, url, description)
+ page.submit_response_edit(response_id, description)
+
+ expected_response_html = (
+ '{}
'.format(url, description)
+ )
+ actual_response_html = page.q(
+ css=".response_{} .response-body".format(response_id)
+ ).html[0]
+ self.assertEqual(expected_response_html, actual_response_html)
+
+ def test_edit_response_add_image(self):
+ """
+ Scenario: User submits valid input to the 'add image' form
+ Given I am editing a response on a discussion page
+ When I click the 'add image' icon in the editor toolbar
+ And enter a valid url to the URL input field
+ And enter a valid string in the Description input field
+ And click the 'OK' button
+ Then the edited response should contain the new image
+ """
+ self.setup_user()
+ self.setup_view()
+ page = self.create_single_thread_page("response_edit_test_thread")
+ page.visit()
+
+ response_id = "response_self_author"
+ url = "http://www.example.com/something.png"
+ description = "image from example.com"
+
+ page.start_response_edit(response_id)
+ page.set_response_editor_value(response_id, "")
+ page.add_content_via_editor_button(
+ "image", response_id, url, description)
+ page.submit_response_edit(response_id, '')
+
+ expected_response_html = (
+ '
'.format(url, description)
+ )
+ actual_response_html = page.q(
+ css=".response_{} .response-body".format(response_id)
+ ).html[0]
+ self.assertEqual(expected_response_html, actual_response_html)
+
+ def test_edit_response_add_image_error_msg(self):
+ """
+ Scenario: User submits invalid input to the 'add image' form
+ Given I am editing a response on a discussion page
+ When I click the 'add image' icon in the editor toolbar
+ And enter an invalid url to the URL input field
+ And enter an empty string in the Description input field
+ And click the 'OK' button
+ Then I should be shown 2 error messages
+ """
+ self.setup_user()
+ self.setup_view()
+ page = self.create_single_thread_page("response_edit_test_thread")
+ page.visit()
+ page.start_response_edit("response_self_author")
+ page.add_content_via_editor_button(
+ "image", "response_self_author", '', '')
+ page.verify_link_editor_error_messages_shown()
+
+ def test_edit_response_add_decorative_image(self):
+ """
+ Scenario: User submits invalid input to the 'add image' form
+ Given I am editing a response on a discussion page
+ When I click the 'add image' icon in the editor toolbar
+ And enter a valid url to the URL input field
+ And enter an empty string in the Description input field
+ And I check the 'image is decorative' checkbox
+ And click the 'OK' button
+ Then the edited response should contain the new image
+ """
+ self.setup_user()
+ self.setup_view()
+ page = self.create_single_thread_page("response_edit_test_thread")
+ page.visit()
+
+ response_id = "response_self_author"
+ url = "http://www.example.com/something.png"
+ description = ""
+
+ page.start_response_edit(response_id)
+ page.set_response_editor_value(response_id, "Some content")
+ page.add_content_via_editor_button(
+ "image", response_id, url, description, is_decorative=True)
+ page.submit_response_edit(response_id, "Some content")
+
+ expected_response_html = (
+ 'Some content
'.format(
+ url, description)
+ )
+ actual_response_html = page.q(
+ css=".response_{} .response-body".format(response_id)
+ ).html[0]
+ self.assertEqual(expected_response_html, actual_response_html)
+
+ def test_edit_response_add_link_error_msg(self):
+ """
+ Scenario: User submits invalid input to the 'add link' form
+ Given I am editing a response on a discussion page
+ When I click the 'add link' icon in the editor toolbar
+ And enter an invalid url to the URL input field
+ And enter an empty string in the Description input field
+ And click the 'OK' button
+ Then I should be shown 2 error messages
+ """
+ self.setup_user()
+ self.setup_view()
+ page = self.create_single_thread_page("response_edit_test_thread")
+ page.visit()
+ page.start_response_edit("response_self_author")
+ page.add_content_via_editor_button(
+ "link", "response_self_author", '', '')
+ page.verify_link_editor_error_messages_shown()
+
def test_edit_response_as_student(self):
"""
Scenario: Students should be able to edit the response they created not responses of other users
diff --git a/lms/static/js/Markdown.Editor.js b/lms/static/js/Markdown.Editor.js
index abcd95f5a2..8be91adde1 100644
--- a/lms/static/js/Markdown.Editor.js
+++ b/lms/static/js/Markdown.Editor.js
@@ -21,25 +21,35 @@
// -------------------------------------------------------------------
// YOUR CHANGES GO HERE
//
- // I've tried to localize the things you are likely to change to
+ // I've tried to localize the things you are likely to change to
// this area.
// -------------------------------------------------------------------
- // The text that appears on the upper part of the dialog box when
- // entering links.
- var linkDialogText = "" + gettext("Insert Hyperlink") + "
http://example.com/ " +
- // Translators: Please keep the quotation marks (") around this text
- gettext("\"optional title\"") + "
";
- var imageDialogText = "" + gettext("Insert Image (upload file or type url)") + "
http://example.com/images/diagram.jpg " +
- // Translators: Please keep the quotation marks (") around this text
- gettext("\"optional title\"") + "
";
+ // The text that appears on the dialog box when entering links.
+ var linkDialogText = gettext("Insert Hyperlink"),
+ linkUrlHelpText = gettext("e.g. 'http://google.com/'"),
+ linkDestinationLabel = gettext("Link Description"),
+ linkDestinationHelpText = gettext("e.g. 'google'"),
+ linkDestinationError = gettext("Please provide a description of the link destination."),
+ linkDefaultText = "http://"; // The default text that appears in input
- // The default text that appears in the dialog input box when entering
- // links.
- var imageDefaultText = "http://";
- var linkDefaultText = "http://";
+ // The text that appears on the dialog box when entering Images.
+ var imageDialogText = gettext("Insert Image (upload file or type URL)"),
+ imageUrlHelpText = gettext("Type in a URL or use the \"Choose File\" button to upload a file from your machine. (e.g. 'http://example.com/img/clouds.jpg')"), // jshint ignore:line
+ imageDescriptionLabel = gettext("Image Description"),
+ imageDefaultText = "http://", // The default text that appears in input
+ imageDescError = gettext("Please describe this image or agree that it has no contextual value by checking the checkbox."), // jshint ignore:line
+ imageDescriptionHelpText = gettext("e.g. 'Sky with clouds'. The description is helpful for users who cannot see the image."), // jshint ignore:line
+ imageDescriptionHelpLink = {
+ href: 'http://www.w3.org/TR/html5/embedded-content-0.html#alt',
+ text: gettext("How to create useful text alternatives.")
+ },
+ imageIsDecorativeLabel = gettext("This image is for decorative purposes only and does not require a description."); // jshint ignore:line
- var defaultHelpHoverTitle = gettext("Markdown Editing Help");
+ // Text that is shared between both link and image dialog boxes.
+ var defaultHelpHoverTitle = gettext("Markdown Editing Help"),
+ urlLabel = gettext("URL"),
+ urlError = gettext("Please provide a valid URL.");
// -------------------------------------------------------------------
// END OF YOUR CHANGES
@@ -64,6 +74,7 @@
* its own image insertion dialog, this hook should return true, and the callback should be called with the chosen
* image url (or null if the user cancelled). If this hook returns false, the default dialog will be used.
*/
+ this.util = util;
this.getConverter = function () { return markdownConverter; }
@@ -164,7 +175,7 @@
beforeReplacer = function (s) { that.before += s; return ""; }
afterReplacer = function (s) { that.after = s + that.after; return ""; }
}
-
+
this.selection = this.selection.replace(/^(\s*)/, beforeReplacer).replace(/(\s*)$/, afterReplacer);
};
@@ -232,14 +243,14 @@
}
};
- // end of Chunks
+ // end of Chunks
// A collection of the important regions on the page.
// Cached so we don't have to keep traversing the DOM.
// Also holds ieCachedRange and ieCachedScrollTop, where necessary; working around
// this issue:
// Internet explorer has problems with CSS sprite buttons that use HTML
- // lists. When you click on the background image "button", IE will
+ // lists. When you click on the background image "button", IE will
// select the non-existent link text and discard the selection in the
// textarea. The solution to this is to cache the textarea selection
// on the button's mousedown event and set a flag. In the part of the
@@ -256,6 +267,10 @@
this.input = doc.getElementById("wmd-input" + postfix);
};
+ util.isValidUrl = function(url) {
+ return /^((?:http|https|ftp):\/{2}|\/)[^]+$/.test(url);
+ };
+
// Returns true if the DOM element is visible, false if it's hidden.
// Checks if display is anything other than none.
util.isVisible = function (elem) {
@@ -584,7 +599,7 @@
setMode("escape");
}
else if ((keyCode < 16 || keyCode > 20) && keyCode != 91) {
- // 16-20 are shift, etc.
+ // 16-20 are shift, etc.
// 91: left window key
// I think this might be a little messed up since there are
// a lot of nonprinting keys above 20.
@@ -728,7 +743,7 @@
if (panels.ieCachedRange)
stateObj.scrollTop = panels.ieCachedScrollTop; // this is set alongside with ieCachedRange
-
+
panels.ieCachedRange = null;
this.setInputAreaSelection();
@@ -975,9 +990,9 @@
var background = doc.createElement("div"),
style = background.style;
-
+
background.className = "wmd-prompt-background";
-
+
style.position = "absolute";
style.top = "0";
@@ -1014,17 +1029,28 @@
// callback: The function which is executed when the prompt is dismissed, either via OK or Cancel.
// It receives a single argument; either the entered text (if OK was chosen) or null (if Cancel
// was chosen).
- ui.prompt = function (text, defaultInputText, callback, imageUploadHandler) {
+ ui.prompt = function (title,
+ urlLabel,
+ urlHelp,
+ urlError,
+ urlDescLabel,
+ urlDescHelp,
+ urlDescHelpLink,
+ urlDescError,
+ defaultInputText,
+ callback,
+ imageIsDecorativeLabel,
+ imageUploadHandler) {
// These variables need to be declared at this level since they are used
// in multiple functions.
- var dialog; // The dialog box.
- var input; // The text box where you enter the hyperlink.
-
-
- if (defaultInputText === undefined) {
- defaultInputText = "";
- }
+ var dialog, // The dialog box.
+ urlInput, // The text box where you enter the hyperlink.
+ urlErrorMsg,
+ descInput, // The text box where you enter the description.
+ descErrorMsg,
+ okButton,
+ cancelButton;
// Used as a keydown event handler. Esc dismisses the prompt.
// Key code 27 is ESC.
@@ -1035,112 +1061,108 @@
}
};
+ var clearFormErrorMessages = function () {
+ urlInput.classList.remove('has-error');
+ urlErrorMsg.style.display = 'none';
+ descInput.classList.remove('has-error');
+ descErrorMsg.style.display = 'none';
+ };
+
// Dismisses the hyperlink input box.
// isCancel is true if we don't care about the input text.
// isCancel is false if we are going to keep the text.
var close = function (isCancel) {
util.removeEvent(doc.body, "keydown", checkEscape);
- var text = input.value;
+ var url = urlInput.value.trim();
+ var description = descInput.value.trim();
+
+ clearFormErrorMessages();
if (isCancel) {
- text = null;
+ url = null;
}
else {
// Fixes common pasting errors.
- text = text.replace(/^http:\/\/(https?|ftp):\/\//, '$1://');
+ url = url.replace(/^http:\/\/(https?|ftp):\/\//, '$1://');
// doesn't change url if started with '/' (local)
- if (!/^(?:https?|ftp):\/\//.test(text) && text.charAt(0) != '/') {
- text = 'http://' + text;
+ if (!/^(?:https?|ftp):\/\//.test(url) && url.charAt(0) !== '/') {
+ url = 'http://' + url;
}
}
- dialog.parentNode.removeChild(dialog);
+ var isValidUrl = util.isValidUrl(url),
+ isValidDesc = (
+ descInput.checkValidity() &&
+ (descInput.required ? description.length : true)
+ );
+
+ if ((isValidUrl && isValidDesc) || isCancel) {
+ dialog.parentNode.removeChild(dialog);
+ callback(url, description);
+ } else {
+ var errorCount = 0;
+ if (!isValidUrl) {
+ urlInput.classList.add('has-error');
+ urlErrorMsg.style.display = 'inline-block';
+ errorCount += 1;
+ } if (!isValidDesc) {
+ descInput.classList.add('has-error');
+ descErrorMsg.style.display = 'inline-block';
+ errorCount += 1;
+ }
+
+ document.getElementById('wmd-editor-dialog-form-errors').textContent = [
+ interpolate(
+ ngettext(
+ // Translators: 'errorCount' is the number of errors found in the form.
+ '%(errorCount)s error found in form.', '%(errorCount)s errors found in form.',
+ errorCount
+ ), {'errorCount': errorCount}, true
+ ),
+ !isValidUrl ? urlErrorMsg.textContent : '',
+ !isValidDesc ? descErrorMsg.textContent : ''
+ ].join(' ');
+
+ document.getElementById('wmd-editor-dialog-form-errors').focus();
+ }
- callback(text);
return false;
};
-
-
// Create the text input box form/window.
var createDialog = function () {
-
// The main dialog box.
dialog = doc.createElement("div");
+ dialog.innerHTML = _.template(
+ document.getElementById("customwmd-prompt-template").innerHTML, {
+ title: title,
+ uploadFieldClass: (imageUploadHandler ? 'file-upload' : ''),
+ urlLabel: urlLabel,
+ urlError: urlError,
+ urlHelp: urlHelp,
+ urlDescLabel: urlDescLabel,
+ descError: urlDescError,
+ urlDescHelp: urlDescHelp,
+ urlDescHelpLink: urlDescHelpLink,
+ okText: gettext("OK"),
+ cancelText: gettext("Cancel"),
+ chooseFileText: gettext("Choose File"),
+ imageIsDecorativeLabel: imageIsDecorativeLabel,
+ imageUploadHandler: imageUploadHandler
+ });
dialog.setAttribute("role", "dialog");
+ dialog.setAttribute("tabindex", "-1");
+ dialog.setAttribute("aria-labelledby", "editorDialogTitle");
dialog.className = "wmd-prompt-dialog";
dialog.style.padding = "10px;";
dialog.style.position = "fixed";
- dialog.style.width = "400px";
+ dialog.style.width = "500px";
dialog.style.zIndex = "1001";
- // The dialog text.
- var question = doc.createElement("div");
- question.innerHTML = text;
- question.style.padding = "5px";
- dialog.appendChild(question);
-
- // The web form container for the text box and buttons.
- var form = doc.createElement("form"),
- style = form.style;
- form.onsubmit = function () { return close(false); };
- style.padding = "0";
- style.margin = "0";
- style.cssFloat = "left";
- style.width = "100%";
- style.textAlign = "center";
- style.position = "relative";
- dialog.appendChild(form);
-
- // The input text box
- input = doc.createElement("input");
- input.type = "text";
- input.value = defaultInputText;
- style = input.style;
- style.display = "block";
- style.width = "80%";
- style.marginLeft = style.marginRight = "auto";
- form.appendChild(input);
-
- // The choose file button if prompt type is 'image'
-
- if (imageUploadHandler) {
- var chooseFile = doc.createElement("input");
- chooseFile.type = "file";
- chooseFile.name = "file-upload";
- chooseFile.id = "file-upload";
- chooseFile.onchange = function() {
- imageUploadHandler(this, input);
- };
- form.appendChild(doc.createElement("br"));
- form.appendChild(chooseFile);
- }
-
-
- // The ok button
- var okButton = doc.createElement("input");
- okButton.type = "button";
- okButton.onclick = function () { return close(false); };
- okButton.value = "OK";
- style = okButton.style;
- style.margin = "10px";
- style.display = "inline";
- style.width = "7em";
-
-
- // The cancel button
- var cancelButton = doc.createElement("input");
- cancelButton.type = "button";
- cancelButton.onclick = function () { return close(true); };
- cancelButton.value = "Cancel";
- style = cancelButton.style;
- style.margin = "10px";
- style.display = "inline";
- style.width = "7em";
-
- form.appendChild(okButton);
- form.appendChild(cancelButton);
+ doc.body.appendChild(dialog);
+ // This has to be done AFTER adding the dialog to the form if you
+ // want it to be centered.
util.addEvent(doc.body, "keydown", checkEscape);
dialog.style.top = "50%";
dialog.style.left = "50%";
@@ -1150,15 +1172,57 @@
dialog.style.top = doc.documentElement.scrollTop + 200 + "px";
dialog.style.left = "50%";
}
- doc.body.appendChild(dialog);
-
- // This has to be done AFTER adding the dialog to the form if you
- // want it to be centered.
dialog.style.marginTop = -(position.getHeight(dialog) / 2) + "px";
dialog.style.marginLeft = -(position.getWidth(dialog) / 2) + "px";
+ urlInput = document.getElementById("new-url-input");
+ urlErrorMsg = document.getElementById("new-url-input-field-message");
+ descInput = document.getElementById("new-url-desc-input");
+ descErrorMsg = document.getElementById("new-url-desc-input-field-message");
+ urlInput.value = defaultInputText;
+
+ okButton = document.getElementById("new-link-image-ok");
+ cancelButton = document.getElementById("new-link-image-cancel");
+
+ okButton.onclick = function () { return close(false); };
+ cancelButton.onclick = function () { return close(true); };
+
+ if(imageUploadHandler) {
+ var startUploadHandler = function () {
+ document.getElementById("file-upload").onchange = function() {
+ imageUploadHandler(this, urlInput);
+ urlInput.focus();
+
+ // Ensures that a user can update their file choice.
+ startUploadHandler();
+ };
+ };
+ startUploadHandler();
+ document.getElementById("file-upload-proxy").onclick = function () {
+ document.getElementById("file-upload").click();
+ return false;
+ };
+ document.getElementById("img-is-decorative").onchange = function () {
+ descInput.required = !descInput.required;
+ };
+ }
+
+ // trap focus in the dialog box
+ $(dialog).on("keydown", function (event) {
+ // On tab backward from the first tabbable item in the prompt
+ if (event.which === 9 && event.shiftKey && event.target === urlInput) {
+ event.preventDefault();
+ cancelButton.focus();
+ }
+ // On tab forward from the last tabbable item in the prompt
+ else if (event.which === 9 && !event.shiftKey && event.target === cancelButton) {
+ event.preventDefault();
+ urlInput.focus();
+ }
+ });
};
+
// Why is this in a zero-length timeout?
// Is it working around a browser bug?
setTimeout(function () {
@@ -1166,19 +1230,19 @@
createDialog();
var defTextLen = defaultInputText.length;
- if (input.selectionStart !== undefined) {
- input.selectionStart = 0;
- input.selectionEnd = defTextLen;
+ if (urlInput.selectionStart !== undefined) {
+ urlInput.selectionStart = 0;
+ urlInput.selectionEnd = defTextLen;
}
- else if (input.createTextRange) {
- var range = input.createTextRange();
+ else if (urlInput.createTextRange) {
+ var range = urlInput.createTextRange();
range.collapse(false);
range.moveStart("character", -defTextLen);
range.moveEnd("character", defTextLen);
range.select();
}
- input.focus();
+ dialog.focus();
}, 0);
};
@@ -1310,7 +1374,7 @@
//
// var link = CreateLinkDialog();
// makeMarkdownLink(link);
- //
+ //
// Instead of this straightforward method of handling a
// dialog I have to pass any code which would execute
// after the dialog is dismissed (e.g. link creation)
@@ -1688,7 +1752,6 @@
}
commandProto.doLinkOrImage = function (chunk, postProcessing, isImage, imageUploadHandler) {
-
chunk.trimWhitespace();
chunk.findTags(/\s*!?\[/, /\][ ]?(?:\n[ ]*)?(\[.*?\])?/);
var background;
@@ -1701,7 +1764,7 @@
}
else {
-
+
// We're moving start and end tag back into the selection, since (as we're in the else block) we're not
// *removing* a link, but *adding* one, so whatever findTags() found is now back to being part of the
// link text. linkEnteredCallback takes care of escaping any brackets.
@@ -1715,8 +1778,7 @@
var that = this;
// The function to be executed when you enter a link and press OK or Cancel.
// Marks up the link and adds the ref.
- var linkEnteredCallback = function (link) {
-
+ var linkEnteredCallback = function (link, description) {
background.parentNode.removeChild(background);
if (link !== null) {
@@ -1739,7 +1801,7 @@
// would mean a zero-width match at the start. Since zero-width matches advance the string position,
// the first bracket could then not act as the "not a backslash" for the second.
chunk.selection = (" " + chunk.selection).replace(/([^\\](?:\\\\)*)(?=[[\]])/g, "$1\\").substr(1);
-
+
var linkDef = " [999]: " + properlyEncoded(link);
var num = that.addLinkDef(chunk, linkDef);
@@ -1748,10 +1810,10 @@
if (!chunk.selection) {
if (isImage) {
- chunk.selection = gettext("enter image description here");
+ chunk.selection = description ? description : "";
}
else {
- chunk.selection = gettext("enter link description here");
+ chunk.selection = description ? description : gettext("enter link description here");
}
}
}
@@ -1761,11 +1823,36 @@
background = ui.createBackground();
if (isImage) {
- if (!this.hooks.insertImageDialog(linkEnteredCallback))
- ui.prompt(imageDialogText, imageDefaultText, linkEnteredCallback, imageUploadHandler);
+ if (!this.hooks.insertImageDialog(linkEnteredCallback)) {
+ ui.prompt(
+ imageDialogText,
+ urlLabel,
+ imageUrlHelpText,
+ urlError,
+ imageDescriptionLabel,
+ imageDescriptionHelpText,
+ imageDescriptionHelpLink,
+ imageDescError,
+ imageDefaultText,
+ linkEnteredCallback,
+ imageIsDecorativeLabel,
+ imageUploadHandler
+ );
+ }
}
else {
- ui.prompt(linkDialogText, linkDefaultText, linkEnteredCallback);
+ ui.prompt(
+ linkDialogText,
+ urlLabel,
+ linkUrlHelpText,
+ urlError,
+ linkDestinationLabel,
+ linkDestinationHelpText,
+ '',
+ linkDestinationError,
+ linkDefaultText,
+ linkEnteredCallback
+ );
}
return true;
}
@@ -1781,7 +1868,7 @@
chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]*\n$/, "\n\n");
chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}>[ \t]*\n$/, "\n\n");
chunk.before = chunk.before.replace(/(\n|^)[ \t]+\n$/, "\n\n");
-
+
// There's no selection, end the cursor wasn't at the end of the line:
// The user wants to split the current list item / code line / blockquote line
// (for the latter it doesn't really matter) in two. Temporarily select the
@@ -1809,7 +1896,7 @@
commandMgr.doCode(chunk);
}
}
-
+
if (fakeSelection) {
chunk.after = chunk.selection + chunk.after;
chunk.selection = "";
diff --git a/lms/static/js/spec/main.js b/lms/static/js/spec/main.js
index bf49ffa4ad..427b066f55 100644
--- a/lms/static/js/spec/main.js
+++ b/lms/static/js/spec/main.js
@@ -221,7 +221,7 @@
exports: 'Markdown.Converter'
},
'Markdown.Editor': {
- deps: ['Markdown.Converter'],
+ deps: ['Markdown.Converter', 'gettext', 'underscore'],
exports: 'Markdown.Editor'
},
'Markdown.Sanitizer': {
@@ -744,7 +744,8 @@
'lms/include/js/spec/financial-assistance/financial_assistance_form_view_spec.js',
'lms/include/js/spec/bookmarks/bookmarks_list_view_spec.js',
'lms/include/js/spec/bookmarks/bookmark_button_view_spec.js',
- 'lms/include/js/spec/views/message_banner_spec.js'
+ 'lms/include/js/spec/views/message_banner_spec.js',
+ 'lms/include/js/spec/markdown_editor_spec.js'
]);
}).call(this, requirejs, define);
diff --git a/lms/static/js/spec/markdown_editor_spec.js b/lms/static/js/spec/markdown_editor_spec.js
new file mode 100644
index 0000000000..674039575b
--- /dev/null
+++ b/lms/static/js/spec/markdown_editor_spec.js
@@ -0,0 +1,53 @@
+define(['Markdown.Editor'], function(MarkdownEditor) {
+ 'use strict';
+ describe('Markdown.Editor', function() {
+ var editor = new MarkdownEditor();
+
+ describe('util.isValidUrl', function () {
+ it('should return true for http://example.com', function () {
+ expect(
+ editor.util.isValidUrl('http://example.com')
+ ).toBeTruthy();
+ });
+ it('should return true for https://example.com', function () {
+ expect(
+ editor.util.isValidUrl('https://example.com')
+ ).toBeTruthy();
+ });
+ it('should return true for ftp://example.com', function () {
+ expect(
+ editor.util.isValidUrl('ftp://example.com')
+ ).toBeTruthy();
+ });
+ it('should return false for http://', function () {
+ expect(editor.util.isValidUrl('http://')).toBeFalsy();
+ });
+ it('should return false for https://', function () {
+ expect(editor.util.isValidUrl('https://')).toBeFalsy();
+ });
+ it('should return false for ftp://', function () {
+ expect(editor.util.isValidUrl('ftp://')).toBeFalsy();
+ });
+ it('should return false for fake://example.com', function () {
+ expect(
+ editor.util.isValidUrl('fakeprotocol://example.com')
+ ).toBeFalsy();
+ });
+ it('should return false for fake://', function () {
+ expect(
+ editor.util.isValidUrl('fakeprotocol://')
+ ).toBeFalsy();
+ });
+ it('should return false for www.noprotocol.com', function () {
+ expect(
+ editor.util.isValidUrl('www.noprotocol.com')
+ ).toBeFalsy();
+ });
+ it('should return false for an empty string', function () {
+ expect(
+ editor.util.isValidUrl('')
+ ).toBeFalsy();
+ });
+ });
+ });
+});
diff --git a/lms/static/sass/_build-course.scss b/lms/static/sass/_build-course.scss
index bc92cf0e9c..662b00dd2f 100644
--- a/lms/static/sass/_build-course.scss
+++ b/lms/static/sass/_build-course.scss
@@ -59,6 +59,7 @@
// discussion
@import "course/discussion/form-wmd-toolbar";
+@import "course/discussion/form";
// search
@import 'search/_search';
diff --git a/lms/static/sass/course/discussion/_form-wmd-toolbar.scss b/lms/static/sass/course/discussion/_form-wmd-toolbar.scss
index 45db5c108a..5b777be6a6 100644
--- a/lms/static/sass/course/discussion/_form-wmd-toolbar.scss
+++ b/lms/static/sass/course/discussion/_form-wmd-toolbar.scss
@@ -105,16 +105,38 @@
font-family: arial, helvetica, sans-serif;
}
+.wmd-prompt-dialog {
+ .form {
+ width: 400px;
+ margin: 0 auto;
+ }
+ .form-actions {
+ text-align: center;
+ }
-.wmd-prompt-dialog > form > input[type="text"] {
- border: 1px solid #999999;
- color: black;
+ .input-text {
+ width: 400px;
+ padding: 0px 12px;
+ }
+
+ .field-hint,
+ .field-message.has-error {
+ width: 380px;
+ }
+
+ .file-upload {
+ .input-text {
+ width: 230px;
+ }
+ .field-message.has-error {
+ margin-top: -1px;
+ width: 210px;
+ }
+ .form-btn {
+ margin: 0 0 0 10px;
+ height: 35px;
+ width: 130px;
+ padding: 6px;
+ }
+ }
}
-
-.wmd-prompt-dialog > form > input[type="button"]{
- border: 1px solid #888888;
- font-family: trebuchet MS, helvetica, sans-serif;
- font-size: 1em;
- font-weight: bold;
-}
-
diff --git a/lms/static/sass/course/discussion/_form.scss b/lms/static/sass/course/discussion/_form.scss
new file mode 100644
index 0000000000..7f50f6e4b6
--- /dev/null
+++ b/lms/static/sass/course/discussion/_form.scss
@@ -0,0 +1,203 @@
+// ------------------------------
+// edX Pattern Library: Components - Forms
+
+// About: Contains base styling for forms
+
+// #SETTINGS
+// #GLOBAL
+// #INPUT TEXT
+// #INPUT RADIO/CHECKBOX
+// ------------------------------
+
+
+// ------------------------------
+// IMPORTANT: This is meant for the modals in the discussion forum
+// when doing updates on them we added the classes to make it compatible
+// with the pattern library. So, it is modified and scoped to that modal.
+
+// TODO: Remove this file once the pattern library it implemented.
+// ------------------------------
+.wmd-prompt-dialog {
+ // ----------------------------
+ // #SETTINGS
+ // ----------------------------
+ $spacing-vertical-x-small: ($baseline/2);
+ $spacing-vertical-base: ($baseline*2);
+ $spacing-vertical-mid-small: ($baseline*1.5);
+ $spacing-vertical-small: $baseline;
+
+ $font-size-large: 18px;
+ $font-size-base: 16px;
+ $font-size-small: 14px;
+
+ $component-border-radius: 3px !default;
+
+ $error-base: rgb(178, 6, 16);
+ $error-dark: rgb(125, 9, 16);
+ $grayscale-x-dark: rgb(77, 75, 75);
+ $grayscale-x-light: rgb(231, 230, 230);
+ $grayscale-white: rgb(252, 252, 252);
+ $grayscale-cool-x-dark: rgb(52, 56, 58);
+ $grayscale-cool-x-light: rgb(229, 233, 235);
+ $primary-accent: rgb(14, 166, 236);
+ $transparent: rgba(167, 164, 164, 0.498039);
+
+ $text-base-color: $grayscale-x-dark !default;
+ $label-color: $text-base-color !default;
+ $label-color-active: $grayscale-x-dark !default;
+ $input-placeholder-text: $grayscale-cool-x-light !default;
+ $input-default-background: $grayscale-white !default;
+ $input-default-border-color: $grayscale-x-light !default;
+ $input-default-focus-border-color: $primary-accent !default;
+ $input-default-color: $grayscale-cool-x-dark !default;
+ $input-default-focus-color: $grayscale-cool-x-dark !default;
+ $input-alt-background: $transparent !default;
+ $input-alt-focus-border-color: $grayscale-x-dark !default;
+
+
+ // ----------------------------
+ // #GLOBAL
+ // ----------------------------
+ // sections of a form
+ .form-group {
+ margin-bottom: $spacing-vertical-mid-small;
+
+ // section title or legend
+ .form-group-hd {
+ margin-bottom: $spacing-vertical-small;
+ font-size: $font-size-large;
+ }
+
+ .field {
+ margin-bottom: $spacing-vertical-base;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+ }
+
+ // radio button and checkbox fieldsets
+ .field-group {
+ margin-bottom: $spacing-vertical-small;
+
+ // group title or legend
+ .field-group-hd {
+ margin-bottom: $spacing-vertical-small;
+ font-size: $font-size-large;
+ }
+
+ .field {
+ margin-bottom: $spacing-vertical-x-small;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+ }
+
+ .field-label {
+ display: block;
+ width: auto;
+ margin-bottom: $spacing-vertical-x-small;
+ font-size: $font-size-base;
+ line-height: 100%;
+ color: $label-color;
+
+ // presents the label inline with the form control
+ &.label-inline {
+ display: inline-block;
+ margin-bottom: 0;
+ }
+
+ // STATE: is selected
+ .field-input:checked + .field-input-label,
+ .field-radio:checked + .field-input-label,
+ &.is-active,
+ &.is-selected {
+ color: $label-color-active;
+ }
+ }
+
+ .field-message {
+ font-size: $font-size-small;
+ border-bottom-left-radius: $component-border-radius;
+ border-bottom-right-radius: $component-border-radius;
+
+ &.has-error {
+ padding: $spacing-vertical-x-small;
+ background: $error-base;
+ color: $grayscale-white;
+ }
+ }
+
+ .field-input,
+ .field-select,
+ .field-textarea {
+ display: inline-block;
+ padding: rem($baseline/2);
+ border: 1px solid $input-default-border-color;
+ background: $input-default-background;
+ font-size: $font-size-base;
+ color: $input-default-color;
+
+ // STATE: is active or has focus
+ &:focus,
+ &.is-active {
+ border-color: $input-default-focus-border-color;
+ color: $input-default-focus-color;
+ }
+
+ // STATE: has an error
+ &.has-error {
+ border-color: $error-base;
+
+ & + .field-hint {
+ color: $error-dark;
+ }
+
+ .icon {
+ fill: $error-base;
+ }
+ }
+ }
+
+
+ // ----------------------------
+ // #INPUT TEXT
+ // ----------------------------
+ .input-text {
+
+ &.input-alt {
+ padding: $spacing-vertical-small 0;
+ border-width: 0 0 2px 0;
+ background: $input-alt-background;
+
+ // STATE: is active or has focus
+ &:focus,
+ &.is-active {
+ border-color: $input-alt-focus-border-color;
+ background: $input-alt-background;
+ }
+
+ &.has-error {
+ border-width: 1px 1px 2px 1px;
+ border-color: $error-base;
+ }
+ }
+ }
+
+ // ----------------------------
+ // Buttons
+ // ----------------------------
+ .form-btn {
+ display: inline-block;
+ margin: 10px;
+ border-style: solid;
+ border-radius: $component-border-radius;
+ border-width: 1px;
+ padding: $spacing-vertical-x-small $baseline;
+ font-size: $font-size-base;
+ font-weight: 600;
+ }
+}
diff --git a/lms/templates/discussion/_underscore_templates.html b/lms/templates/discussion/_underscore_templates.html
index 7056157416..20100c78e4 100644
--- a/lms/templates/discussion/_underscore_templates.html
+++ b/lms/templates/discussion/_underscore_templates.html
@@ -11,7 +11,7 @@ template_names = [
'thread', 'thread-show', 'thread-edit', 'thread-response', 'thread-response-show', 'thread-response-edit',
'response-comment-show', 'response-comment-edit', 'thread-list-item', 'discussion-home', 'search-alert',
'new-post', 'thread-type', 'new-post-menu-entry', 'new-post-menu-category', 'topic', 'post-user-display',
- 'inline-discussion', 'pagination', 'user-profile', 'profile-thread'
+ 'inline-discussion', 'pagination', 'user-profile', 'profile-thread', 'customwmd-prompt'
]
%>