@@ -0,0 +1,51 @@
|
||||
<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 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 = "";
|
||||
|
||||
@@ -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);
|
||||
|
||||
53
lms/static/js/spec/markdown_editor_spec.js
Normal file
53
lms/static/js/spec/markdown_editor_spec.js
Normal file
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
203
lms/static/sass/course/discussion/_form.scss
Normal file
203
lms/static/sass/course/discussion/_form.scss
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
]
|
||||
%>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user