Files
edx-platform/common/static/coffee/src/discussion/utils.coffee
Greg Price 95932610a7 Add focus trap on forum navigation thread loading
For accessibility purposes, it is bad to allow a user to initiate
loading of additional threads in the navigation sidebar and then shift
focus away from the sidebar, only to have focus snap back when the
additional threads are loaded. Now, we trap focus on the loading element
as recommended by our accessibility consultant.

JIRA: FOR-238
2013-11-19 10:06:30 -05:00

303 lines
12 KiB
CoffeeScript

$ ->
if !window.$$contents
window.$$contents = {}
$.fn.extend
loading: ->
@$_loading = $("<div class='loading-animation'><span class='sr'>Loading content</span></div>")
$(this).after(@$_loading)
loaded: ->
@$_loading.remove()
class @DiscussionUtil
@wmdEditors: {}
@getTemplate: (id) ->
$("script##{id}").html()
@loadRoles: (roles)->
@roleIds = roles
@loadFlagModerator: (what)->
@isFlagModerator = ((what=="True") or (what == 1))
@loadRolesFromContainer: ->
@loadRoles($("#discussion-container").data("roles"))
@loadFlagModerator($("#discussion-container").data("flag-moderator"))
@isStaff: (user_id) ->
staff = _.union(@roleIds['Moderator'], @roleIds['Administrator'])
_.include(staff, parseInt(user_id))
@isTA: (user_id) ->
ta = _.union(@roleIds['Community TA'])
_.include(ta, parseInt(user_id))
@bulkUpdateContentInfo: (infos) ->
for id, info of infos
Content.getContent(id).updateInfo(info)
@generateDiscussionLink: (cls, txt, handler) ->
$("<a>").addClass("discussion-link")
.attr("href", "javascript:void(0)")
.addClass(cls).html(txt)
.click -> handler(this)
@urlFor: (name, param, param1, param2) ->
{
follow_discussion : "/courses/#{$$course_id}/discussion/#{param}/follow"
unfollow_discussion : "/courses/#{$$course_id}/discussion/#{param}/unfollow"
create_thread : "/courses/#{$$course_id}/discussion/#{param}/threads/create"
search_similar_threads : "/courses/#{$$course_id}/discussion/#{param}/threads/search_similar"
update_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/update"
create_comment : "/courses/#{$$course_id}/discussion/threads/#{param}/reply"
delete_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/delete"
flagAbuse_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/flagAbuse"
unFlagAbuse_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/unFlagAbuse"
flagAbuse_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/flagAbuse"
unFlagAbuse_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/unFlagAbuse"
upvote_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/upvote"
downvote_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/downvote"
pin_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/pin"
un_pin_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/unpin"
undo_vote_for_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/unvote"
follow_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/follow"
unfollow_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/unfollow"
update_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/update"
endorse_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/endorse"
create_sub_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/reply"
delete_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/delete"
upvote_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/upvote"
downvote_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/downvote"
undo_vote_for_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/unvote"
upload : "/courses/#{$$course_id}/discussion/upload"
search : "/courses/#{$$course_id}/discussion/forum/search"
tags_autocomplete : "/courses/#{$$course_id}/discussion/threads/tags/autocomplete"
retrieve_discussion : "/courses/#{$$course_id}/discussion/forum/#{param}/inline"
retrieve_single_thread : "/courses/#{$$course_id}/discussion/forum/#{param}/threads/#{param1}"
update_moderator_status : "/courses/#{$$course_id}/discussion/users/#{param}/update_moderator_status"
openclose_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/close"
permanent_link_thread : "/courses/#{$$course_id}/discussion/forum/#{param}/threads/#{param1}"
permanent_link_comment : "/courses/#{$$course_id}/discussion/forum/#{param}/threads/#{param1}##{param2}"
user_profile : "/courses/#{$$course_id}/discussion/forum/users/#{param}"
followed_threads : "/courses/#{$$course_id}/discussion/forum/users/#{param}/followed"
threads : "/courses/#{$$course_id}/discussion/forum"
"enable_notifications" : "/notification_prefs/enable/"
"disable_notifications" : "/notification_prefs/disable/"
"notifications_status" : "/notification_prefs/status"
}[name]
@makeFocusTrap: (elem) ->
elem.keydown(
(event) ->
if event.which == 9 # Tab
event.preventDefault()
)
@discussionAlert: (header, body) ->
if $("#discussion-alert").length == 0
alertDiv = $("<div class='modal' role='alertdialog' id='discussion-alert' aria-describedby='discussion-alert-message'/>").css("display", "none")
alertDiv.html(
"<div class='inner-wrapper discussion-alert-wrapper'>" +
" <button class='close-modal dismiss' aria-hidden='true'>&#10005;</button>" +
" <header><h2/><hr/></header>" +
" <p id='discussion-alert-message'/>" +
" <hr/>" +
" <button class='dismiss'>OK</button>" +
"</div>"
)
@makeFocusTrap(alertDiv.find("button"))
alertTrigger = $("<a href='#discussion-alert' id='discussion-alert-trigger'/>").css("display", "none")
alertTrigger.leanModal({closeButton: "#discussion-alert .dismiss", overlay: 1, top: 200})
$("body").append(alertDiv).append(alertTrigger)
$("#discussion-alert header h2").html(header)
$("#discussion-alert p").html(body)
$("#discussion-alert-trigger").click()
$("#discussion-alert button").focus()
@safeAjax: (params) ->
$elem = params.$elem
if $elem and $elem.attr("disabled")
return
params["url"] = URI(params["url"]).addSearch ajax: 1
params["beforeSend"] = ->
if $elem
$elem.attr("disabled", "disabled")
if params["$loading"]
if params["loadingCallback"]?
params["loadingCallback"].apply(params["$loading"])
else
params["$loading"].loading()
if !params["error"]
params["error"] = =>
@discussionAlert(
"Sorry",
"We had some trouble processing your request. Please ensure you" +
" have copied any unsaved work and then reload the page."
)
request = $.ajax(params).always ->
if $elem
$elem.removeAttr("disabled")
if params["$loading"]
if params["loadedCallback"]?
params["loadedCallback"].apply(params["$loading"])
else
params["$loading"].loaded()
return request
@bindLocalEvents: ($local, eventsHandler) ->
for eventSelector, handler of eventsHandler
[event, selector] = eventSelector.split(' ')
$local(selector).unbind(event)[event] handler
@processTag: (text) ->
text.toLowerCase()
@tagsInputOptions: ->
autocomplete_url: @urlFor('tags_autocomplete')
autocomplete:
remoteDataType: 'json'
interactive: true
height: '30px'
width: '100%'
defaultText: "Tag your post: press enter after each tag"
removeWithBackspace: true
preprocessTag: @processTag
@formErrorHandler: (errorsField) ->
(xhr, textStatus, error) ->
response = JSON.parse(xhr.responseText)
if response.errors? and response.errors.length > 0
errorsField.empty()
for error in response.errors
errorsField.append($("<li>").addClass("new-post-form-error").html(error)).show()
@clearFormErrors: (errorsField) ->
errorsField.empty()
@postMathJaxProcessor: (text) ->
RE_INLINEMATH = /^\$([^\$]*)\$/g
RE_DISPLAYMATH = /^\$\$([^\$]*)\$\$/g
@processEachMathAndCode text, (s, type) ->
if type == 'display'
s.replace RE_DISPLAYMATH, ($0, $1) ->
"\\[" + $1 + "\\]"
else if type == 'inline'
s.replace RE_INLINEMATH, ($0, $1) ->
"\\(" + $1 + "\\)"
else
s
@makeWmdEditor: ($content, $local, cls_identifier) ->
elem = $local(".#{cls_identifier}")
placeholder = elem.data('placeholder')
id = elem.attr("data-id") # use attr instead of data because we want to avoid type coercion
appended_id = "-#{cls_identifier}-#{id}"
imageUploadUrl = @urlFor('upload')
_processor = (_this) ->
(text) -> _this.postMathJaxProcessor(text)
editor = Markdown.makeWmdEditor elem, appended_id, imageUploadUrl, _processor(@)
@wmdEditors["#{cls_identifier}-#{id}"] = editor
if placeholder?
elem.find("#wmd-input#{appended_id}").attr('placeholder', placeholder)
editor
@getWmdEditor: ($content, $local, cls_identifier) ->
elem = $local(".#{cls_identifier}")
id = elem.attr("data-id") # use attr instead of data because we want to avoid type coercion
@wmdEditors["#{cls_identifier}-#{id}"]
@getWmdInput: ($content, $local, cls_identifier) ->
elem = $local(".#{cls_identifier}")
id = elem.attr("data-id") # use attr instead of data because we want to avoid type coercion
$local("#wmd-input-#{cls_identifier}-#{id}")
@getWmdContent: ($content, $local, cls_identifier) ->
@getWmdInput($content, $local, cls_identifier).val()
@setWmdContent: ($content, $local, cls_identifier, text) ->
@getWmdInput($content, $local, cls_identifier).val(text)
@getWmdEditor($content, $local, cls_identifier).refreshPreview()
@processEachMathAndCode: (text, processor) ->
codeArchive = []
RE_DISPLAYMATH = /^([^\$]*?)\$\$([^\$]*?)\$\$(.*)$/m
RE_INLINEMATH = /^([^\$]*?)\$([^\$]+?)\$(.*)$/m
ESCAPED_DOLLAR = '@@ESCAPED_D@@'
ESCAPED_BACKSLASH = '@@ESCAPED_B@@'
processedText = ""
$div = $("<div>").html(text)
$div.find("code").each (index, code) ->
codeArchive.push $(code).html()
$(code).html(codeArchive.length - 1)
text = $div.html()
text = text.replace /\\\$/g, ESCAPED_DOLLAR
while true
if RE_INLINEMATH.test(text)
text = text.replace RE_INLINEMATH, ($0, $1, $2, $3) ->
processedText += $1 + processor("$" + $2 + "$", 'inline')
$3
else if RE_DISPLAYMATH.test(text)
text = text.replace RE_DISPLAYMATH, ($0, $1, $2, $3) ->
#processedText += $1 + processor("$$" + $2 + "$$", 'display')
#bug fix, ordering is off
processedText = processor("$$" + $2 + "$$", 'display') + processedText
processedText = $1 + processedText
$3
else
processedText += text
break
text = processedText
text = text.replace(new RegExp(ESCAPED_DOLLAR, 'g'), '\\$')
text = text.replace /\\\\\\\\/g, ESCAPED_BACKSLASH
text = text.replace /\\begin\{([a-z]*\*?)\}([\s\S]*?)\\end\{\1\}/img, ($0, $1, $2) ->
processor("\\begin{#{$1}}" + $2 + "\\end{#{$1}}")
text = text.replace(new RegExp(ESCAPED_BACKSLASH, 'g'), '\\\\\\\\')
$div = $("<div>").html(text)
cnt = 0
$div.find("code").each (index, code) ->
$(code).html(processor(codeArchive[cnt], 'code'))
cnt += 1
text = $div.html()
text
@unescapeHighlightTag: (text) ->
text.replace(/\&lt\;highlight\&gt\;/g, "<span class='search-highlight'>")
.replace(/\&lt\;\/highlight\&gt\;/g, "</span>")
@stripHighlight: (text) ->
text.replace(/\&(amp\;)?lt\;highlight\&(amp\;)?gt\;/g, "")
.replace(/\&(amp\;)?lt\;\/highlight\&(amp\;)?gt\;/g, "")
@stripLatexHighlight: (text) ->
@processEachMathAndCode text, @stripHighlight
@markdownWithHighlight: (text) ->
text = text.replace(/^\&gt\;/gm, ">")
converter = Markdown.getMathCompatibleConverter()
text = @unescapeHighlightTag @stripLatexHighlight converter.makeHtml text
return text.replace(/^>/gm,"&gt;")
@abbreviateString: (text, minLength) ->
# Abbreviates a string to at least minLength characters, stopping at word boundaries
if text.length<minLength
return text
else
while minLength < text.length && text[minLength] != ' '
minLength++
return text.substr(0, minLength) + '...'