Commit 092eae9f by Christine Lytwynec

Merge pull request #11130 from edx/clytwynec/ac-75

AC-75 and AC-73
parents 0c55f17d 8c8953fe
<h4 id="editor-dialog-title"><%- title %></h4>
<div role="status" id="wmd-editor-dialog-form-errors" class="sr" tabindex="-1"></div>
<div>
<form class="form">
<fieldset class="field-group">
<legend class="form-group-hd sr"><%- title %></legend>
<div class="field <%- uploadFieldClass %>">
<label id="new-url-input-label" for="new-url-input" class="field-label">
<%- urlLabel %></label>
<input type="text" id="new-url-input" class="field-input input-text" aria-describedby="new-url-input-help">
<% if (imageUploadHandler) { %>
<button id="file-upload-proxy" class="btn btn-primary btn-base form-btn">
<%- chooseFileText %>
</button>
<input type="file" name="file-upload" id="file-upload" style="display:none;"/>
<% } %>
<div id="new-url-input-field-message" class="field-message has-error" style="display:none">
<span class="field-message-content"><%- urlError %></span>
</div>
<div id="new-url-input-help" class="field-hint">
<%- urlHelp %>
</div>
</div>
<div class="field">
<label for="new-url-desc-input" class="field-label"><%- urlDescLabel %></label>
<input type="text" id="new-url-desc-input" class="field-input input-text" required aria-describedby="new-url-desc-input-help">
<div id="new-url-desc-input-field-message" class="field-message has-error" style="display:none">
<span class="field-message-content"><%- descError %></span>
</div>
<div id="new-url-desc-input-help" class="field-hint">
<%- urlDescHelp %>
<% if (urlDescHelpLink) { %>
<a href="<%- urlDescHelpLink['href'] %>">
<%- urlDescHelpLink['text'] %>
</a>
<% } %>
</div>
</div>
<div class="field"><% if (imageUploadHandler) { %>
<label for="img-is-decorative" class="field-label label-inline">
<input type="checkbox" id="img-is-decorative" class="field-input input-checkbox">
<span class="field-input-label"><%- imageIsDecorativeLabel %></span>
</label> <% }
%></div>
</fieldset>
<div class="form-actions">
<input type="button" id="new-link-image-ok" class="btn btn-primary btn-base form-btn" value="<%- okText %>" />
<input type="button" id="new-link-image-cancel" class="btn btn-primary btn-base form-btn" value="<%- cancelText %>" >
</div>
</form>
</div>
...@@ -195,17 +195,58 @@ class DiscussionThreadPage(PageObject, DiscussionPageMixin): ...@@ -195,17 +195,58 @@ class DiscussionThreadPage(PageObject, DiscussionPageMixin):
"""Replace the contents of the response editor""" """Replace the contents of the response editor"""
self._find_within(".response_{} .discussion-response .wmd-input".format(response_id)).fill(new_body) 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): def submit_response_edit(self, response_id, new_response_body):
"""Click the submit button on the response editor""" """Click the submit button on the response editor"""
self._find_within(".response_{} .discussion-response .post-update".format(response_id)).first.click()
EmptyPromise( def submit_response_check_func():
lambda: ( """
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 not self.is_response_editor_visible(response_id) and
self.is_response_visible(response_id) and self.is_response_visible(response_id) and
self.get_response_body(response_id) == new_response_body 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): def is_show_comments_visible(self, response_id):
"""Returns true if the "show comments" link is visible for a response""" """Returns true if the "show comments" link is visible for a response"""
......
...@@ -449,6 +449,145 @@ class DiscussionResponseEditTest(BaseDiscussionTestCase): ...@@ -449,6 +449,145 @@ class DiscussionResponseEditTest(BaseDiscussionTestCase):
page.set_response_editor_value(response_id, new_response) page.set_response_editor_value(response_id, new_response)
page.submit_response_edit(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 = (
'<p><a href="{}">{}</a></p>'.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 = (
'<p><img src="{}" alt="{}" title=""></p>'.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 = (
'<p>Some content<img src="{}" alt="{}" title=""></p>'.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): def test_edit_response_as_student(self):
""" """
Scenario: Students should be able to edit the response they created not responses of other users Scenario: Students should be able to edit the response they created not responses of other users
......
...@@ -25,21 +25,31 @@ ...@@ -25,21 +25,31 @@
// this area. // this area.
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// The text that appears on the upper part of the dialog box when // The text that appears on the dialog box when entering links.
// entering links. var linkDialogText = gettext("Insert Hyperlink"),
var linkDialogText = "<p><b>" + gettext("Insert Hyperlink") + "</b></p><p>http://example.com/ " + linkUrlHelpText = gettext("e.g. 'http://google.com/'"),
// Translators: Please keep the quotation marks (") around this text linkDestinationLabel = gettext("Link Description"),
gettext("\"optional title\"") + "</p>"; linkDestinationHelpText = gettext("e.g. 'google'"),
var imageDialogText = "<p><b>" + gettext("Insert Image (upload file or type url)") + "</b></p><p>http://example.com/images/diagram.jpg " + linkDestinationError = gettext("Please provide a description of the link destination."),
// Translators: Please keep the quotation marks (") around this text linkDefaultText = "http://"; // The default text that appears in input
gettext("\"optional title\"") + "<br><br></p>";
// The text that appears on the dialog box when entering Images.
// The default text that appears in the dialog input box when entering var imageDialogText = gettext("Insert Image (upload file or type URL)"),
// links. 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
var imageDefaultText = "http://"; imageDescriptionLabel = gettext("Image Description"),
var linkDefaultText = "http://"; 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
var defaultHelpHoverTitle = gettext("Markdown Editing Help"); 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
// 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 // END OF YOUR CHANGES
...@@ -64,6 +74,7 @@ ...@@ -64,6 +74,7 @@
* its own image insertion dialog, this hook should return true, and the callback should be called with the chosen * 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. * 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; } this.getConverter = function () { return markdownConverter; }
...@@ -256,6 +267,10 @@ ...@@ -256,6 +267,10 @@
this.input = doc.getElementById("wmd-input" + postfix); 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. // Returns true if the DOM element is visible, false if it's hidden.
// Checks if display is anything other than none. // Checks if display is anything other than none.
util.isVisible = function (elem) { util.isVisible = function (elem) {
...@@ -1014,17 +1029,28 @@ ...@@ -1014,17 +1029,28 @@
// callback: The function which is executed when the prompt is dismissed, either via OK or Cancel. // 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 // It receives a single argument; either the entered text (if OK was chosen) or null (if Cancel
// was chosen). // 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 // These variables need to be declared at this level since they are used
// in multiple functions. // in multiple functions.
var dialog; // The dialog box. var dialog, // The dialog box.
var input; // The text box where you enter the hyperlink. urlInput, // The text box where you enter the hyperlink.
urlErrorMsg,
descInput, // The text box where you enter the description.
if (defaultInputText === undefined) { descErrorMsg,
defaultInputText = ""; okButton,
} cancelButton;
// Used as a keydown event handler. Esc dismisses the prompt. // Used as a keydown event handler. Esc dismisses the prompt.
// Key code 27 is ESC. // Key code 27 is ESC.
...@@ -1035,112 +1061,108 @@ ...@@ -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. // Dismisses the hyperlink input box.
// isCancel is true if we don't care about the input text. // isCancel is true if we don't care about the input text.
// isCancel is false if we are going to keep the text. // isCancel is false if we are going to keep the text.
var close = function (isCancel) { var close = function (isCancel) {
util.removeEvent(doc.body, "keydown", checkEscape); util.removeEvent(doc.body, "keydown", checkEscape);
var text = input.value; var url = urlInput.value.trim();
var description = descInput.value.trim();
clearFormErrorMessages();
if (isCancel) { if (isCancel) {
text = null; url = null;
} }
else { else {
// Fixes common pasting errors. // 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) // doesn't change url if started with '/' (local)
if (!/^(?:https?|ftp):\/\//.test(text) && text.charAt(0) != '/') { if (!/^(?:https?|ftp):\/\//.test(url) && url.charAt(0) !== '/') {
text = 'http://' + text; url = 'http://' + url;
} }
} }
var isValidUrl = util.isValidUrl(url),
isValidDesc = (
descInput.checkValidity() &&
(descInput.required ? description.length : true)
);
if ((isValidUrl && isValidDesc) || isCancel) {
dialog.parentNode.removeChild(dialog); 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; return false;
}; };
// Create the text input box form/window. // Create the text input box form/window.
var createDialog = function () { var createDialog = function () {
// The main dialog box. // The main dialog box.
dialog = doc.createElement("div"); 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("role", "dialog");
dialog.setAttribute("tabindex", "-1");
dialog.setAttribute("aria-labelledby", "editorDialogTitle");
dialog.className = "wmd-prompt-dialog"; dialog.className = "wmd-prompt-dialog";
dialog.style.padding = "10px;"; dialog.style.padding = "10px;";
dialog.style.position = "fixed"; dialog.style.position = "fixed";
dialog.style.width = "400px"; dialog.style.width = "500px";
dialog.style.zIndex = "1001"; dialog.style.zIndex = "1001";
// The dialog text. doc.body.appendChild(dialog);
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);
// 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); util.addEvent(doc.body, "keydown", checkEscape);
dialog.style.top = "50%"; dialog.style.top = "50%";
dialog.style.left = "50%"; dialog.style.left = "50%";
...@@ -1150,15 +1172,57 @@ ...@@ -1150,15 +1172,57 @@
dialog.style.top = doc.documentElement.scrollTop + 200 + "px"; dialog.style.top = doc.documentElement.scrollTop + 200 + "px";
dialog.style.left = "50%"; 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.marginTop = -(position.getHeight(dialog) / 2) + "px";
dialog.style.marginLeft = -(position.getWidth(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? // Why is this in a zero-length timeout?
// Is it working around a browser bug? // Is it working around a browser bug?
setTimeout(function () { setTimeout(function () {
...@@ -1166,19 +1230,19 @@ ...@@ -1166,19 +1230,19 @@
createDialog(); createDialog();
var defTextLen = defaultInputText.length; var defTextLen = defaultInputText.length;
if (input.selectionStart !== undefined) { if (urlInput.selectionStart !== undefined) {
input.selectionStart = 0; urlInput.selectionStart = 0;
input.selectionEnd = defTextLen; urlInput.selectionEnd = defTextLen;
} }
else if (input.createTextRange) { else if (urlInput.createTextRange) {
var range = input.createTextRange(); var range = urlInput.createTextRange();
range.collapse(false); range.collapse(false);
range.moveStart("character", -defTextLen); range.moveStart("character", -defTextLen);
range.moveEnd("character", defTextLen); range.moveEnd("character", defTextLen);
range.select(); range.select();
} }
input.focus(); dialog.focus();
}, 0); }, 0);
}; };
...@@ -1688,7 +1752,6 @@ ...@@ -1688,7 +1752,6 @@
} }
commandProto.doLinkOrImage = function (chunk, postProcessing, isImage, imageUploadHandler) { commandProto.doLinkOrImage = function (chunk, postProcessing, isImage, imageUploadHandler) {
chunk.trimWhitespace(); chunk.trimWhitespace();
chunk.findTags(/\s*!?\[/, /\][ ]?(?:\n[ ]*)?(\[.*?\])?/); chunk.findTags(/\s*!?\[/, /\][ ]?(?:\n[ ]*)?(\[.*?\])?/);
var background; var background;
...@@ -1715,8 +1778,7 @@ ...@@ -1715,8 +1778,7 @@
var that = this; var that = this;
// The function to be executed when you enter a link and press OK or Cancel. // The function to be executed when you enter a link and press OK or Cancel.
// Marks up the link and adds the ref. // Marks up the link and adds the ref.
var linkEnteredCallback = function (link) { var linkEnteredCallback = function (link, description) {
background.parentNode.removeChild(background); background.parentNode.removeChild(background);
if (link !== null) { if (link !== null) {
...@@ -1748,10 +1810,10 @@ ...@@ -1748,10 +1810,10 @@
if (!chunk.selection) { if (!chunk.selection) {
if (isImage) { if (isImage) {
chunk.selection = gettext("enter image description here"); chunk.selection = description ? description : "";
} }
else { else {
chunk.selection = gettext("enter link description here"); chunk.selection = description ? description : gettext("enter link description here");
} }
} }
} }
...@@ -1761,11 +1823,36 @@ ...@@ -1761,11 +1823,36 @@
background = ui.createBackground(); background = ui.createBackground();
if (isImage) { if (isImage) {
if (!this.hooks.insertImageDialog(linkEnteredCallback)) if (!this.hooks.insertImageDialog(linkEnteredCallback)) {
ui.prompt(imageDialogText, imageDefaultText, linkEnteredCallback, imageUploadHandler); ui.prompt(
imageDialogText,
urlLabel,
imageUrlHelpText,
urlError,
imageDescriptionLabel,
imageDescriptionHelpText,
imageDescriptionHelpLink,
imageDescError,
imageDefaultText,
linkEnteredCallback,
imageIsDecorativeLabel,
imageUploadHandler
);
}
} }
else { else {
ui.prompt(linkDialogText, linkDefaultText, linkEnteredCallback); ui.prompt(
linkDialogText,
urlLabel,
linkUrlHelpText,
urlError,
linkDestinationLabel,
linkDestinationHelpText,
'',
linkDestinationError,
linkDefaultText,
linkEnteredCallback
);
} }
return true; return true;
} }
......
...@@ -221,7 +221,7 @@ ...@@ -221,7 +221,7 @@
exports: 'Markdown.Converter' exports: 'Markdown.Converter'
}, },
'Markdown.Editor': { 'Markdown.Editor': {
deps: ['Markdown.Converter'], deps: ['Markdown.Converter', 'gettext', 'underscore'],
exports: 'Markdown.Editor' exports: 'Markdown.Editor'
}, },
'Markdown.Sanitizer': { 'Markdown.Sanitizer': {
...@@ -744,7 +744,8 @@ ...@@ -744,7 +744,8 @@
'lms/include/js/spec/financial-assistance/financial_assistance_form_view_spec.js', '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/bookmarks_list_view_spec.js',
'lms/include/js/spec/bookmarks/bookmark_button_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); }).call(this, requirejs, define);
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();
});
});
});
});
...@@ -59,6 +59,7 @@ ...@@ -59,6 +59,7 @@
// discussion // discussion
@import "course/discussion/form-wmd-toolbar"; @import "course/discussion/form-wmd-toolbar";
@import "course/discussion/form";
// search // search
@import 'search/_search'; @import 'search/_search';
......
...@@ -105,16 +105,38 @@ ...@@ -105,16 +105,38 @@
font-family: arial, helvetica, sans-serif; font-family: arial, helvetica, sans-serif;
} }
.wmd-prompt-dialog {
.wmd-prompt-dialog > form > input[type="text"] { .form {
border: 1px solid #999999; width: 400px;
color: black; margin: 0 auto;
} }
.form-actions {
.wmd-prompt-dialog > form > input[type="button"]{ text-align: center;
border: 1px solid #888888; }
font-family: trebuchet MS, helvetica, sans-serif;
font-size: 1em; .input-text {
font-weight: bold; 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;
}
}
} }
// ------------------------------
// 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;
}
}
...@@ -11,7 +11,7 @@ template_names = [ ...@@ -11,7 +11,7 @@ template_names = [
'thread', 'thread-show', 'thread-edit', 'thread-response', 'thread-response-show', 'thread-response-edit', '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', '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', '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'
] ]
%> %>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment