Commit 8c8953fe by Christine Lytwynec

Update discussion markdown editor add link and image modals

parent 8c26178d
<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):
"""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"""
......
......@@ -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 = (
'<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):
"""
Scenario: Students should be able to edit the response they created not responses of other users
......
......@@ -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 = "<p><b>" + gettext("Insert Hyperlink") + "</b></p><p>http://example.com/ " +
// Translators: Please keep the quotation marks (") around this text
gettext("\"optional title\"") + "</p>";
var imageDialogText = "<p><b>" + gettext("Insert Image (upload file or type url)") + "</b></p><p>http://example.com/images/diagram.jpg " +
// Translators: Please keep the quotation marks (") around this text
gettext("\"optional title\"") + "<br><br></p>";
// The default text that appears in the dialog input box when entering
// links.
var imageDefaultText = "http://";
var linkDefaultText = "http://";
var defaultHelpHoverTitle = gettext("Markdown Editing Help");
// 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 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
// 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;
}
callback(text);
return false;
};
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();
}
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 = "";
......
......@@ -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);
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 @@
// discussion
@import "course/discussion/form-wmd-toolbar";
@import "course/discussion/form";
// search
@import 'search/_search';
......
......@@ -105,16 +105,38 @@
font-family: arial, helvetica, sans-serif;
}
.wmd-prompt-dialog > form > input[type="text"] {
border: 1px solid #999999;
color: black;
}
.wmd-prompt-dialog > form > input[type="button"]{
border: 1px solid #888888;
font-family: trebuchet MS, helvetica, sans-serif;
font-size: 1em;
font-weight: bold;
.wmd-prompt-dialog {
.form {
width: 400px;
margin: 0 auto;
}
.form-actions {
text-align: center;
}
.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;
}
}
}
// ------------------------------
// 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 = [
'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'
]
%>
......
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