Update UI for forum actions
The actions are now consolidated in one location for each piece of content. Primary actions (vote, follow, endorse, mark as answer) are buttons, and secondary actions (pin, edit, delete, report, close) are in a menu. This also includes improved front-end error handling for the actions and significant test cleanup. Co-authored-by: jsa <jsa@edx.org> Co-authored-by: marco <marcotuts@gmail.com> Co-authored-by: Frances Botsford <frances@edx.org> Co-authored-by: Brian Talbot <btalbot@edx.org>
This commit is contained in:
@@ -108,13 +108,21 @@ if Backbone?
|
||||
@get("abuse_flaggers").pop(window.user.get('id'))
|
||||
@trigger "change", @
|
||||
|
||||
isFlagged: ->
|
||||
user = DiscussionUtil.getUser()
|
||||
flaggers = @get("abuse_flaggers")
|
||||
user and (user.id in flaggers or (DiscussionUtil.isPrivilegedUser(user.id) and flaggers.length > 0))
|
||||
|
||||
incrementVote: (increment) ->
|
||||
newVotes = _.clone(@get("votes"))
|
||||
newVotes.up_count = newVotes.up_count + increment
|
||||
@set("votes", newVotes)
|
||||
|
||||
vote: ->
|
||||
@get("votes")["up_count"] = parseInt(@get("votes")["up_count"]) + 1
|
||||
@trigger "change", @
|
||||
@incrementVote(1)
|
||||
|
||||
unvote: ->
|
||||
@get("votes")["up_count"] = parseInt(@get("votes")["up_count"]) - 1
|
||||
@trigger "change", @
|
||||
@incrementVote(-1)
|
||||
|
||||
class @Thread extends @Content
|
||||
urlMappers:
|
||||
|
||||
@@ -21,15 +21,14 @@ class @DiscussionUtil
|
||||
@setUser: (user) ->
|
||||
@user = user
|
||||
|
||||
@getUser: () ->
|
||||
@user
|
||||
|
||||
@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) ->
|
||||
user_id ?= @user?.id
|
||||
@@ -162,6 +161,13 @@ class @DiscussionUtil
|
||||
params["$loading"].loaded()
|
||||
return request
|
||||
|
||||
@updateWithUndo: (model, updates, safeAjaxParams, errorMsg) ->
|
||||
if errorMsg
|
||||
safeAjaxParams.error = => @discussionAlert(gettext("Sorry"), errorMsg)
|
||||
undo = _.pick(model.attributes, _.keys(updates))
|
||||
model.set(updates)
|
||||
@safeAjax(safeAjaxParams).fail(() -> model.set(undo))
|
||||
|
||||
@bindLocalEvents: ($local, eventsHandler) ->
|
||||
for eventSelector, handler of eventsHandler
|
||||
[event, selector] = eventSelector.split(' ')
|
||||
|
||||
@@ -8,30 +8,6 @@ if Backbone?
|
||||
(event) -> DiscussionUtil.activateOnSpace(event, @toggleFlagAbuse)
|
||||
|
||||
attrRenderer:
|
||||
closed: (closed) ->
|
||||
return if not @$(".action-openclose").length
|
||||
return if not @$(".post-status-closed").length
|
||||
if closed
|
||||
@$(".post-status-closed").show()
|
||||
@$(".action-openclose").html(@$(".action-openclose").html().replace(gettext("Close"), gettext("Open")))
|
||||
@$(".discussion-reply-new").hide()
|
||||
else
|
||||
@$(".post-status-closed").hide()
|
||||
@$(".action-openclose").html(@$(".action-openclose").html().replace(gettext("Open"), gettext("Close")))
|
||||
@$(".discussion-reply-new").show()
|
||||
|
||||
voted: (voted) ->
|
||||
|
||||
votes_point: (votes_point) ->
|
||||
|
||||
comments_count: (comments_count) ->
|
||||
|
||||
subscribed: (subscribed) ->
|
||||
if subscribed
|
||||
@$(".dogear").addClass("is-followed").attr("aria-checked", "true")
|
||||
else
|
||||
@$(".dogear").removeClass("is-followed").attr("aria-checked", "false")
|
||||
|
||||
ability: (ability) ->
|
||||
for action, selector of @abilityRenderer
|
||||
if not ability[action]
|
||||
@@ -41,14 +17,22 @@ if Backbone?
|
||||
|
||||
abilityRenderer:
|
||||
editable:
|
||||
enable: -> @$(".action-edit").closest("li").show()
|
||||
disable: -> @$(".action-edit").closest("li").hide()
|
||||
enable: -> @$(".action-edit").closest(".actions-item").removeClass("is-hidden")
|
||||
disable: -> @$(".action-edit").closest(".actions-item").addClass("is-hidden")
|
||||
can_delete:
|
||||
enable: -> @$(".action-delete").closest("li").show()
|
||||
disable: -> @$(".action-delete").closest("li").hide()
|
||||
enable: -> @$(".action-delete").closest(".actions-item").removeClass("is-hidden")
|
||||
disable: -> @$(".action-delete").closest(".actions-item").addClass("is-hidden")
|
||||
can_openclose:
|
||||
enable: -> @$(".action-openclose").closest("li").show()
|
||||
disable: -> @$(".action-openclose").closest("li").hide()
|
||||
enable: ->
|
||||
_.each(
|
||||
[".action-close", ".action-pin"],
|
||||
(selector) => @$(selector).closest(".actions-item").removeClass("is-hidden")
|
||||
)
|
||||
disable: ->
|
||||
_.each(
|
||||
[".action-close", ".action-pin"],
|
||||
(selector) => @$(selector).closest(".actions-item").addClass("is-hidden")
|
||||
)
|
||||
|
||||
renderPartialAttrs: ->
|
||||
for attr, value of @model.changedAttributes()
|
||||
@@ -76,114 +60,237 @@ if Backbone?
|
||||
|
||||
initialize: ->
|
||||
@model.bind('change', @renderPartialAttrs, @)
|
||||
|
||||
|
||||
toggleFollowing: (event) =>
|
||||
event.preventDefault()
|
||||
$elem = $(event.target)
|
||||
url = null
|
||||
if not @model.get('subscribed')
|
||||
@model.follow()
|
||||
url = @model.urlFor("follow")
|
||||
else
|
||||
@model.unfollow()
|
||||
url = @model.urlFor("unfollow")
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: $elem
|
||||
url: url
|
||||
type: "POST"
|
||||
|
||||
toggleFlagAbuse: (event) =>
|
||||
event.preventDefault()
|
||||
if window.user.id in @model.get("abuse_flaggers") or (DiscussionUtil.isFlagModerator and @model.get("abuse_flaggers").length > 0)
|
||||
@unFlagAbuse()
|
||||
else
|
||||
@flagAbuse()
|
||||
|
||||
flagAbuse: =>
|
||||
url = @model.urlFor("flagAbuse")
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: @$(".discussion-flag-abuse")
|
||||
url: url
|
||||
type: "POST"
|
||||
success: (response, textStatus) =>
|
||||
if textStatus == 'success'
|
||||
###
|
||||
note, we have to clone the array in order to trigger a change event
|
||||
###
|
||||
temp_array = _.clone(@model.get('abuse_flaggers'));
|
||||
temp_array.push(window.user.id)
|
||||
@model.set('abuse_flaggers', temp_array)
|
||||
|
||||
unFlagAbuse: =>
|
||||
url = @model.urlFor("unFlagAbuse")
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: @$(".discussion-flag-abuse")
|
||||
url: url
|
||||
type: "POST"
|
||||
success: (response, textStatus) =>
|
||||
if textStatus == 'success'
|
||||
temp_array = _.clone(@model.get('abuse_flaggers'));
|
||||
temp_array.pop(window.user.id)
|
||||
# if you're an admin, clear this
|
||||
if DiscussionUtil.isFlagModerator
|
||||
temp_array = []
|
||||
|
||||
@model.set('abuse_flaggers', temp_array)
|
||||
|
||||
renderVote: =>
|
||||
button = @$el.find(".vote-btn")
|
||||
voted = window.user.voted(@model)
|
||||
voteNum = @model.get("votes")["up_count"]
|
||||
button.toggleClass("is-cast", voted)
|
||||
button.attr("aria-pressed", voted)
|
||||
button.attr("data-tooltip", if voted then gettext("remove vote") else gettext("vote"))
|
||||
buttonTextFmt =
|
||||
if voted
|
||||
ngettext(
|
||||
"vote (click to remove your vote)",
|
||||
"votes (click to remove your vote)",
|
||||
voteNum
|
||||
)
|
||||
else
|
||||
ngettext(
|
||||
"vote (click to vote)",
|
||||
"votes (click to vote)",
|
||||
voteNum
|
||||
)
|
||||
buttonTextFmt = "%(voteNum)s%(startSrSpan)s " + buttonTextFmt + "%(endSrSpan)s"
|
||||
buttonText = interpolate(
|
||||
buttonTextFmt,
|
||||
{voteNum: voteNum, startSrSpan: "<span class='sr'>", endSrSpan: "</span>"},
|
||||
true
|
||||
@listenTo(@model, "change:endorsed", =>
|
||||
if @model instanceof Comment
|
||||
@trigger("comment:endorse")
|
||||
)
|
||||
button.html("<span class='plus-icon'/>" + buttonText)
|
||||
|
||||
class @DiscussionContentShowView extends DiscussionContentView
|
||||
events:
|
||||
_.reduce(
|
||||
[
|
||||
[".action-follow", "toggleFollow"],
|
||||
[".action-answer", "toggleEndorse"],
|
||||
[".action-endorse", "toggleEndorse"],
|
||||
[".action-vote", "toggleVote"],
|
||||
[".action-more", "toggleSecondaryActions"],
|
||||
[".action-pin", "togglePin"],
|
||||
[".action-edit", "edit"],
|
||||
[".action-delete", "_delete"],
|
||||
[".action-report", "toggleReport"],
|
||||
[".action-close", "toggleClose"],
|
||||
],
|
||||
(obj, event) =>
|
||||
selector = event[0]
|
||||
funcName = event[1]
|
||||
obj["click #{selector}"] = (event) -> @[funcName](event)
|
||||
obj["keydown #{selector}"] = (event) -> DiscussionUtil.activateOnSpace(event, @[funcName])
|
||||
obj
|
||||
,
|
||||
{}
|
||||
)
|
||||
|
||||
updateButtonState: (selector, checked) =>
|
||||
$button = @$(selector)
|
||||
$button.toggleClass("is-checked", checked)
|
||||
$button.attr("aria-checked", checked)
|
||||
|
||||
attrRenderer: $.extend({}, DiscussionContentView.prototype.attrRenderer, {
|
||||
subscribed: (subscribed) ->
|
||||
@updateButtonState(".action-follow", subscribed)
|
||||
|
||||
endorsed: (endorsed) ->
|
||||
selector = if @model.get("thread").get("thread_type") == "question" then ".action-answer" else ".action-endorse"
|
||||
@updateButtonState(selector, endorsed)
|
||||
$button = @$(selector)
|
||||
$button.closest(".actions-item").toggleClass("is-hidden", not @model.canBeEndorsed())
|
||||
$button.toggleClass("is-checked", endorsed)
|
||||
|
||||
votes: (votes) ->
|
||||
selector = ".action-vote"
|
||||
@updateButtonState(selector, window.user.voted(@model))
|
||||
button = @$el.find(selector)
|
||||
numVotes = votes.up_count
|
||||
button.find(".js-sr-vote-count").html(
|
||||
interpolate(
|
||||
ngettext("currently %(numVotes)s vote", "currently %(numVotes)s votes", numVotes),
|
||||
{numVotes: numVotes},
|
||||
true
|
||||
)
|
||||
)
|
||||
button.find(".js-visual-vote-count").html(
|
||||
interpolate(
|
||||
ngettext("%(numVotes)s Vote", "%(numVotes)s Votes", numVotes),
|
||||
{numVotes: numVotes},
|
||||
true
|
||||
)
|
||||
)
|
||||
|
||||
pinned: (pinned) ->
|
||||
@updateButtonState(".action-pin", pinned)
|
||||
@$(".post-label-pinned").toggleClass("is-hidden", not pinned)
|
||||
|
||||
abuse_flaggers: (abuse_flaggers) ->
|
||||
flagged = @model.isFlagged()
|
||||
@updateButtonState(".action-report", flagged)
|
||||
@$(".post-label-reported").toggleClass("is-hidden", not flagged)
|
||||
|
||||
closed: (closed) ->
|
||||
@updateButtonState(".action-close", closed)
|
||||
@$(".post-label-closed").toggleClass("is-hidden", not closed)
|
||||
})
|
||||
|
||||
toggleSecondaryActions: (event) =>
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
@secondaryActionsExpanded = !@secondaryActionsExpanded
|
||||
@$(".action-more").toggleClass("is-expanded", @secondaryActionsExpanded)
|
||||
@$(".actions-dropdown").
|
||||
toggleClass("is-expanded", @secondaryActionsExpanded).
|
||||
attr("aria-expanded", @secondaryActionsExpanded)
|
||||
if @secondaryActionsExpanded
|
||||
if event.type == "keydown"
|
||||
@$(".action-list-item:first").focus()
|
||||
$("body").on("click", @toggleSecondaryActions)
|
||||
$("body").on("keydown", @handleSecondaryActionEscape)
|
||||
@$(".action-list-item").on("blur", @handleSecondaryActionBlur)
|
||||
else
|
||||
$("body").off("click", @toggleSecondaryActions)
|
||||
$("body").off("keydown", @handleSecondaryActionEscape)
|
||||
@$(".action-list-item").off("blur", @handleSecondaryActionBlur)
|
||||
|
||||
handleSecondaryActionEscape: (event) =>
|
||||
if event.keyCode == 27 # Esc
|
||||
@toggleSecondaryActions(event)
|
||||
@$(".action-more").focus()
|
||||
|
||||
handleSecondaryActionBlur: (event) =>
|
||||
setTimeout(
|
||||
=>
|
||||
if @secondaryActionsExpanded && @$(".actions-dropdown :focus").length == 0
|
||||
@toggleSecondaryActions(event)
|
||||
,
|
||||
10
|
||||
)
|
||||
|
||||
toggleFollow: (event) =>
|
||||
event.preventDefault()
|
||||
is_subscribing = not @model.get("subscribed")
|
||||
url = @model.urlFor(if is_subscribing then "follow" else "unfollow")
|
||||
if is_subscribing
|
||||
msg = gettext("We had some trouble subscribing you to this thread. Please try again.")
|
||||
else
|
||||
msg = gettext("We had some trouble unsubscribing you from this thread. Please try again.")
|
||||
DiscussionUtil.updateWithUndo(
|
||||
@model,
|
||||
{"subscribed": is_subscribing},
|
||||
{url: url, type: "POST", $elem: $(event.currentTarget)},
|
||||
msg
|
||||
)
|
||||
|
||||
toggleEndorse: (event) =>
|
||||
event.preventDefault()
|
||||
is_endorsing = not @model.get("endorsed")
|
||||
url = @model.urlFor("endorse")
|
||||
updates =
|
||||
endorsed: is_endorsing
|
||||
endorsement: if is_endorsing then {username: DiscussionUtil.getUser().get("username"), time: new Date().toISOString()} else null
|
||||
if @model.get('thread').get('thread_type') == 'question'
|
||||
if is_endorsing
|
||||
msg = gettext("We had some trouble marking this response as an answer. Please try again.")
|
||||
else
|
||||
msg = gettext("We had some trouble removing this response as an answer. Please try again.")
|
||||
else
|
||||
if is_endorsing
|
||||
msg = gettext("We had some trouble marking this response endorsed. Please try again.")
|
||||
else
|
||||
msg = gettext("We had some trouble removing this endorsement. Please try again.")
|
||||
beforeFunc = () => @trigger("comment:endorse")
|
||||
DiscussionUtil.updateWithUndo(
|
||||
@model,
|
||||
updates,
|
||||
{url: url, type: "POST", data: {endorsed: is_endorsing}, beforeSend: beforeFunc, $elem: $(event.currentTarget)},
|
||||
msg
|
||||
).always(@trigger("comment:endorse")) # ensures UI components get updated to the correct state when ajax completes
|
||||
|
||||
toggleVote: (event) =>
|
||||
event.preventDefault()
|
||||
if window.user.voted(@model)
|
||||
@unvote()
|
||||
user = DiscussionUtil.getUser()
|
||||
is_voting = not user.voted(@model)
|
||||
url = @model.urlFor(if is_voting then "upvote" else "unvote")
|
||||
updates =
|
||||
upvoted_ids: (if is_voting then _.union else _.difference)(user.get('upvoted_ids'), [@model.id])
|
||||
DiscussionUtil.updateWithUndo(
|
||||
user,
|
||||
updates,
|
||||
{url: url, type: "POST", $elem: $(event.currentTarget)},
|
||||
gettext("We had some trouble saving your vote. Please try again.")
|
||||
).done(() => if is_voting then @model.vote() else @model.unvote())
|
||||
|
||||
togglePin: (event) =>
|
||||
event.preventDefault()
|
||||
is_pinning = not @model.get("pinned")
|
||||
url = @model.urlFor(if is_pinning then "pinThread" else "unPinThread")
|
||||
if is_pinning
|
||||
msg = gettext("We had some trouble pinning this thread. Please try again.")
|
||||
else
|
||||
@vote()
|
||||
msg = gettext("We had some trouble unpinning this thread. Please try again.")
|
||||
DiscussionUtil.updateWithUndo(
|
||||
@model,
|
||||
{pinned: is_pinning},
|
||||
{url: url, type: "POST", $elem: $(event.currentTarget)},
|
||||
msg
|
||||
)
|
||||
|
||||
vote: =>
|
||||
window.user.vote(@model)
|
||||
url = @model.urlFor("upvote")
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: @$el.find(".vote-btn")
|
||||
url: url
|
||||
type: "POST"
|
||||
success: (response, textStatus) =>
|
||||
if textStatus == 'success'
|
||||
@model.set(response)
|
||||
toggleReport: (event) =>
|
||||
event.preventDefault()
|
||||
if @model.isFlagged()
|
||||
is_flagging = false
|
||||
msg = gettext("We had some trouble removing your flag on this post. Please try again.")
|
||||
else
|
||||
is_flagging = true
|
||||
msg = gettext("We had some trouble reporting this post. Please try again.")
|
||||
url = @model.urlFor(if is_flagging then "flagAbuse" else "unFlagAbuse")
|
||||
updates =
|
||||
abuse_flaggers: (if is_flagging then _.union else _.difference)(@model.get("abuse_flaggers"), [DiscussionUtil.getUser().id])
|
||||
DiscussionUtil.updateWithUndo(
|
||||
@model,
|
||||
updates,
|
||||
{url: url, type: "POST", $elem: $(event.currentTarget)},
|
||||
msg
|
||||
)
|
||||
|
||||
unvote: =>
|
||||
window.user.unvote(@model)
|
||||
url = @model.urlFor("unvote")
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: @$el.find(".vote-btn")
|
||||
url: url
|
||||
type: "POST"
|
||||
success: (response, textStatus) =>
|
||||
if textStatus == 'success'
|
||||
@model.set(response)
|
||||
toggleClose: (event) =>
|
||||
event.preventDefault()
|
||||
is_closing = not @model.get('closed')
|
||||
if is_closing
|
||||
msg = gettext("We had some trouble closing this thread. Please try again.")
|
||||
else
|
||||
msg = gettext("We had some trouble reopening this thread. Please try again.")
|
||||
updates = {closed: is_closing}
|
||||
DiscussionUtil.updateWithUndo(
|
||||
@model,
|
||||
updates,
|
||||
{url: @model.urlFor("close"), type: "POST", data: updates, $elem: $(event.currentTarget)},
|
||||
msg
|
||||
)
|
||||
|
||||
getAuthorDisplay: ->
|
||||
_.template($("#post-user-display-template").html())(
|
||||
username: @model.get('username') || null
|
||||
user_url: @model.get('user_url')
|
||||
is_community_ta: @model.get('community_ta_authored')
|
||||
is_staff: @model.get('staff_authored')
|
||||
)
|
||||
|
||||
getEndorserDisplay: ->
|
||||
endorsement = @model.get('endorsement')
|
||||
if endorsement and endorsement.username
|
||||
_.template($("#post-user-display-template").html())(
|
||||
username: endorsement.username
|
||||
user_url: DiscussionUtil.urlFor('user_profile', endorsement.user_id)
|
||||
is_community_ta: DiscussionUtil.isTA(endorsement.user_id)
|
||||
is_staff: DiscussionUtil.isStaff(endorsement.user_id)
|
||||
)
|
||||
else
|
||||
null
|
||||
|
||||
@@ -1,47 +1,27 @@
|
||||
if Backbone?
|
||||
class @DiscussionThreadShowView extends DiscussionContentView
|
||||
|
||||
events:
|
||||
"click .vote-btn":
|
||||
(event) -> @toggleVote(event)
|
||||
"keydown .vote-btn":
|
||||
(event) -> DiscussionUtil.activateOnSpace(event, @toggleVote)
|
||||
"click .discussion-flag-abuse": "toggleFlagAbuse"
|
||||
"keydown .discussion-flag-abuse":
|
||||
(event) -> DiscussionUtil.activateOnSpace(event, @toggleFlagAbuse)
|
||||
"click .admin-pin":
|
||||
(event) -> @togglePin(event)
|
||||
"keydown .admin-pin":
|
||||
(event) -> DiscussionUtil.activateOnSpace(event, @togglePin)
|
||||
"click .action-follow": "toggleFollowing"
|
||||
"keydown .action-follow":
|
||||
(event) -> DiscussionUtil.activateOnSpace(event, @toggleFollowing)
|
||||
"click .action-edit": "edit"
|
||||
"click .action-delete": "_delete"
|
||||
"click .action-openclose": "toggleClosed"
|
||||
|
||||
$: (selector) ->
|
||||
@$el.find(selector)
|
||||
|
||||
class @DiscussionThreadShowView extends DiscussionContentShowView
|
||||
initialize: (options) ->
|
||||
super()
|
||||
@mode = options.mode or "inline" # allowed values are "tab" or "inline"
|
||||
if @mode not in ["tab", "inline"]
|
||||
throw new Error("invalid mode: " + @mode)
|
||||
@model.on "change", @updateModelDetails
|
||||
|
||||
renderTemplate: ->
|
||||
@template = _.template($("#thread-show-template").html())
|
||||
context = @model.toJSON()
|
||||
context.mode = @mode
|
||||
context = $.extend(
|
||||
{
|
||||
mode: @mode,
|
||||
flagged: @model.isFlagged(),
|
||||
author_display: @getAuthorDisplay(),
|
||||
cid: @model.cid
|
||||
},
|
||||
@model.attributes,
|
||||
)
|
||||
@template(context)
|
||||
|
||||
render: ->
|
||||
@$el.html(@renderTemplate())
|
||||
@delegateEvents()
|
||||
@renderVote()
|
||||
@renderFlagged()
|
||||
@renderPinned()
|
||||
@renderAttrs()
|
||||
@$("span.timeago").timeago()
|
||||
@convertMath()
|
||||
@@ -49,60 +29,6 @@ if Backbone?
|
||||
@highlight @$("h1,h3")
|
||||
@
|
||||
|
||||
renderFlagged: =>
|
||||
if window.user.id in @model.get("abuse_flaggers") or (DiscussionUtil.isFlagModerator and @model.get("abuse_flaggers").length > 0)
|
||||
@$("[data-role=thread-flag]").addClass("flagged")
|
||||
@$("[data-role=thread-flag]").removeClass("notflagged")
|
||||
@$(".discussion-flag-abuse").attr("aria-pressed", "true")
|
||||
@$(".discussion-flag-abuse").attr("data-tooltip", gettext("Click to remove report"))
|
||||
###
|
||||
Translators: The text between start_sr_span and end_span is not shown
|
||||
in most browsers but will be read by screen readers.
|
||||
###
|
||||
@$(".discussion-flag-abuse .flag-label").html(interpolate(gettext("Misuse Reported%(start_sr_span)s, click to remove report%(end_span)s"), {"start_sr_span": "<span class='sr'>", "end_span": "</span>"}, true))
|
||||
else
|
||||
@$("[data-role=thread-flag]").removeClass("flagged")
|
||||
@$("[data-role=thread-flag]").addClass("notflagged")
|
||||
@$(".discussion-flag-abuse").attr("aria-pressed", "false")
|
||||
@$(".discussion-flag-abuse .flag-label").html(gettext("Report Misuse"))
|
||||
|
||||
renderPinned: =>
|
||||
pinElem = @$(".discussion-pin")
|
||||
pinLabelElem = pinElem.find(".pin-label")
|
||||
if @model.get("pinned")
|
||||
pinElem.addClass("pinned")
|
||||
pinElem.removeClass("notpinned")
|
||||
if @model.can("can_openclose")
|
||||
###
|
||||
Translators: The text between start_sr_span and end_span is not shown
|
||||
in most browsers but will be read by screen readers.
|
||||
###
|
||||
pinLabelElem.html(
|
||||
interpolate(
|
||||
gettext("Pinned%(start_sr_span)s, click to unpin%(end_span)s"),
|
||||
{"start_sr_span": "<span class='sr'>", "end_span": "</span>"},
|
||||
true
|
||||
)
|
||||
)
|
||||
pinElem.attr("data-tooltip", gettext("Click to unpin"))
|
||||
pinElem.attr("aria-pressed", "true")
|
||||
else
|
||||
pinLabelElem.html(gettext("Pinned"))
|
||||
pinElem.removeAttr("data-tooltip")
|
||||
pinElem.removeAttr("aria-pressed")
|
||||
else
|
||||
# If not pinned and not able to pin, pin is not shown
|
||||
pinElem.removeClass("pinned")
|
||||
pinElem.addClass("notpinned")
|
||||
pinLabelElem.html(gettext("Pin Thread"))
|
||||
pinElem.removeAttr("data-tooltip")
|
||||
pinElem.attr("aria-pressed", "false")
|
||||
|
||||
updateModelDetails: =>
|
||||
@renderVote()
|
||||
@renderFlagged()
|
||||
@renderPinned()
|
||||
|
||||
convertMath: ->
|
||||
element = @$(".post-body")
|
||||
element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.text()
|
||||
@@ -114,64 +40,6 @@ if Backbone?
|
||||
_delete: (event) ->
|
||||
@trigger "thread:_delete", event
|
||||
|
||||
togglePin: (event) =>
|
||||
event.preventDefault()
|
||||
if @model.get('pinned')
|
||||
@unPin()
|
||||
else
|
||||
@pin()
|
||||
|
||||
pin: =>
|
||||
url = @model.urlFor("pinThread")
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: @$(".discussion-pin")
|
||||
url: url
|
||||
type: "POST"
|
||||
success: (response, textStatus) =>
|
||||
if textStatus == 'success'
|
||||
@model.set('pinned', true)
|
||||
error: =>
|
||||
DiscussionUtil.discussionAlert("Sorry", "We had some trouble pinning this thread. Please try again.")
|
||||
|
||||
unPin: =>
|
||||
url = @model.urlFor("unPinThread")
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: @$(".discussion-pin")
|
||||
url: url
|
||||
type: "POST"
|
||||
success: (response, textStatus) =>
|
||||
if textStatus == 'success'
|
||||
@model.set('pinned', false)
|
||||
error: =>
|
||||
DiscussionUtil.discussionAlert("Sorry", "We had some trouble unpinning this thread. Please try again.")
|
||||
|
||||
toggleClosed: (event) ->
|
||||
$elem = $(event.target)
|
||||
url = @model.urlFor('close')
|
||||
closed = @model.get('closed')
|
||||
data = { closed: not closed }
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: $elem
|
||||
url: url
|
||||
data: data
|
||||
type: "POST"
|
||||
success: (response, textStatus) =>
|
||||
@model.set('closed', not closed)
|
||||
@model.set('ability', response.ability)
|
||||
|
||||
toggleEndorse: (event) ->
|
||||
$elem = $(event.target)
|
||||
url = @model.urlFor('endorse')
|
||||
endorsed = @model.get('endorsed')
|
||||
data = { endorsed: not endorsed }
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: $elem
|
||||
url: url
|
||||
data: data
|
||||
type: "POST"
|
||||
success: (response, textStatus) =>
|
||||
@model.set('endorsed', not endorsed)
|
||||
|
||||
highlight: (el) ->
|
||||
if el.html()
|
||||
el.html(el.html().replace(/<mark>/g, "<mark>").replace(/<\/mark>/g, "</mark>"))
|
||||
|
||||
@@ -52,6 +52,12 @@ if Backbone?
|
||||
else # mode == "inline"
|
||||
@collapse()
|
||||
|
||||
attrRenderer: $.extend({}, DiscussionContentView.prototype.attrRenderer, {
|
||||
closed: (closed) ->
|
||||
@$(".discussion-reply-new").toggle(not closed)
|
||||
@renderAddResponseButton()
|
||||
})
|
||||
|
||||
expand: (event) ->
|
||||
if event
|
||||
event.preventDefault()
|
||||
@@ -200,8 +206,8 @@ if Backbone?
|
||||
@$el.find(listSelector).append(view.el)
|
||||
view.afterInsert()
|
||||
|
||||
renderAddResponseButton: ->
|
||||
if @model.hasResponses() and @model.can('can_reply')
|
||||
renderAddResponseButton: =>
|
||||
if @model.hasResponses() and @model.can('can_reply') and !@model.get('closed')
|
||||
@$el.find('div.add-response').show()
|
||||
else
|
||||
@$el.find('div.add-response').hide()
|
||||
@@ -215,9 +221,8 @@ if Backbone?
|
||||
addComment: =>
|
||||
@model.comment()
|
||||
|
||||
endorseThread: (endorsed) =>
|
||||
is_endorsed = @$el.find(".is-endorsed").length > 0
|
||||
@model.set 'endorsed', is_endorsed
|
||||
endorseThread: =>
|
||||
@model.set 'endorsed', @$el.find(".action-answer.is-checked").length > 0
|
||||
|
||||
submitComment: (event) ->
|
||||
event.preventDefault()
|
||||
|
||||
@@ -1,39 +1,23 @@
|
||||
if Backbone?
|
||||
class @ResponseCommentShowView extends DiscussionContentView
|
||||
|
||||
events:
|
||||
"click .action-delete":
|
||||
(event) -> @_delete(event)
|
||||
"keydown .action-delete":
|
||||
(event) -> DiscussionUtil.activateOnSpace(event, @_delete)
|
||||
"click .action-edit":
|
||||
(event) -> @edit(event)
|
||||
"keydown .action-edit":
|
||||
(event) -> DiscussionUtil.activateOnSpace(event, @edit)
|
||||
|
||||
class @ResponseCommentShowView extends DiscussionContentShowView
|
||||
tagName: "li"
|
||||
|
||||
initialize: ->
|
||||
super()
|
||||
@model.on "change", @updateModelDetails
|
||||
|
||||
abilityRenderer:
|
||||
can_delete:
|
||||
enable: -> @$(".action-delete").show()
|
||||
disable: -> @$(".action-delete").hide()
|
||||
editable:
|
||||
enable: -> @$(".action-edit").show()
|
||||
disable: -> @$(".action-edit").hide()
|
||||
|
||||
render: ->
|
||||
@template = _.template($("#response-comment-show-template").html())
|
||||
params = @model.toJSON()
|
||||
@$el.html(
|
||||
@template(
|
||||
_.extend(
|
||||
{
|
||||
cid: @model.cid,
|
||||
author_display: @getAuthorDisplay()
|
||||
},
|
||||
@model.attributes
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@$el.html(@template(params))
|
||||
@delegateEvents()
|
||||
@renderAttrs()
|
||||
@renderFlagged()
|
||||
@markAsStaff()
|
||||
@$el.find(".timeago").timeago()
|
||||
@convertMath()
|
||||
@addReplyLink()
|
||||
@@ -51,31 +35,8 @@ if Backbone?
|
||||
body.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight body.text()
|
||||
MathJax.Hub.Queue ["Typeset", MathJax.Hub, body[0]]
|
||||
|
||||
markAsStaff: ->
|
||||
if DiscussionUtil.isStaff(@model.get("user_id"))
|
||||
@$el.find("a.profile-link").after('<span class="staff-label">' + gettext('staff') + '</span>')
|
||||
else if DiscussionUtil.isTA(@model.get("user_id"))
|
||||
@$el.find("a.profile-link").after('<span class="community-ta-label">' + gettext('Community TA') + '</span>')
|
||||
|
||||
_delete: (event) =>
|
||||
@trigger "comment:_delete", event
|
||||
|
||||
renderFlagged: =>
|
||||
if window.user.id in @model.get("abuse_flaggers") or (DiscussionUtil.isFlagModerator and @model.get("abuse_flaggers").length > 0)
|
||||
@$("[data-role=thread-flag]").addClass("flagged")
|
||||
@$("[data-role=thread-flag]").removeClass("notflagged")
|
||||
@$(".discussion-flag-abuse").attr("aria-pressed", "true")
|
||||
@$(".discussion-flag-abuse").attr("data-tooltip", gettext("Misuse Reported, click to remove report"))
|
||||
@$(".discussion-flag-abuse .flag-label").html(gettext("Misuse Reported, click to remove report"))
|
||||
else
|
||||
@$("[data-role=thread-flag]").removeClass("flagged")
|
||||
@$("[data-role=thread-flag]").addClass("notflagged")
|
||||
@$(".discussion-flag-abuse").attr("aria-pressed", "false")
|
||||
@$(".discussion-flag-abuse").attr("data-tooltip", gettext("Report Misuse"))
|
||||
@$(".discussion-flag-abuse .flag-label").html(gettext("Report Misuse"))
|
||||
|
||||
updateModelDetails: =>
|
||||
@renderFlagged()
|
||||
|
||||
edit: (event) =>
|
||||
@trigger "comment:edit", event
|
||||
|
||||
@@ -1,45 +1,27 @@
|
||||
if Backbone?
|
||||
class @ThreadResponseShowView extends DiscussionContentView
|
||||
events:
|
||||
"click .vote-btn":
|
||||
(event) -> @toggleVote(event)
|
||||
"keydown .vote-btn":
|
||||
(event) -> DiscussionUtil.activateOnSpace(event, @toggleVote)
|
||||
"click .action-endorse": "toggleEndorse"
|
||||
"click .action-delete": "_delete"
|
||||
"click .action-edit": "edit"
|
||||
"click .discussion-flag-abuse": "toggleFlagAbuse"
|
||||
"keydown .discussion-flag-abuse":
|
||||
(event) -> DiscussionUtil.activateOnSpace(event, @toggleFlagAbuse)
|
||||
|
||||
attrRenderer: $.extend({}, DiscussionContentView.prototype.attrRenderer, {
|
||||
endorsed: (endorsed) ->
|
||||
$endorseButton = @$(".action-endorse")
|
||||
$endorseButton.toggleClass("is-clickable", @model.canBeEndorsed())
|
||||
$endorseButton.toggleClass("is-endorsed", endorsed)
|
||||
$endorseButton.toggle(endorsed || @model.canBeEndorsed())
|
||||
})
|
||||
|
||||
$: (selector) ->
|
||||
@$el.find(selector)
|
||||
|
||||
class @ThreadResponseShowView extends DiscussionContentShowView
|
||||
initialize: ->
|
||||
super()
|
||||
@listenTo(@model, "change", @render)
|
||||
|
||||
renderTemplate: ->
|
||||
@template = _.template($("#thread-response-show-template").html())
|
||||
@template(@model.toJSON())
|
||||
context = _.extend(
|
||||
{
|
||||
cid: @model.cid,
|
||||
author_display: @getAuthorDisplay(),
|
||||
endorser_display: @getEndorserDisplay()
|
||||
},
|
||||
@model.attributes
|
||||
)
|
||||
@template(context)
|
||||
|
||||
render: ->
|
||||
@$el.html(@renderTemplate())
|
||||
@delegateEvents()
|
||||
@renderVote()
|
||||
@renderAttrs()
|
||||
@renderFlagged()
|
||||
@$el.find(".posted-details .timeago").timeago()
|
||||
@convertMath()
|
||||
@markAsStaff()
|
||||
@
|
||||
|
||||
convertMath: ->
|
||||
@@ -47,58 +29,8 @@ if Backbone?
|
||||
element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.text()
|
||||
MathJax.Hub.Queue ["Typeset", MathJax.Hub, element[0]]
|
||||
|
||||
markAsStaff: ->
|
||||
if DiscussionUtil.isStaff(@model.get("user_id"))
|
||||
@$el.addClass("staff")
|
||||
@$el.prepend('<div class="staff-banner">' + gettext('staff') + '</div>')
|
||||
else if DiscussionUtil.isTA(@model.get("user_id"))
|
||||
@$el.addClass("community-ta")
|
||||
@$el.prepend('<div class="community-ta-banner">' + gettext('Community TA') + '</div>')
|
||||
|
||||
edit: (event) ->
|
||||
@trigger "response:edit", event
|
||||
|
||||
_delete: (event) ->
|
||||
@trigger "response:_delete", event
|
||||
|
||||
toggleEndorse: (event) ->
|
||||
event.preventDefault()
|
||||
if not @model.canBeEndorsed()
|
||||
return
|
||||
$elem = $(event.target)
|
||||
url = @model.urlFor('endorse')
|
||||
endorsed = @model.get('endorsed')
|
||||
new_endorsed = not endorsed
|
||||
data = { endorsed: new_endorsed }
|
||||
endorsement = {
|
||||
"username": window.user.get("username"),
|
||||
"time": new Date().toISOString()
|
||||
}
|
||||
@model.set(
|
||||
"endorsed": new_endorsed
|
||||
"endorsement": if new_endorsed then endorsement else null
|
||||
)
|
||||
@trigger "comment:endorse", not endorsed
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: $elem
|
||||
url: url
|
||||
data: data
|
||||
type: "POST"
|
||||
|
||||
|
||||
renderFlagged: =>
|
||||
if window.user.id in @model.get("abuse_flaggers") or (DiscussionUtil.isFlagModerator and @model.get("abuse_flaggers").length > 0)
|
||||
@$("[data-role=thread-flag]").addClass("flagged")
|
||||
@$("[data-role=thread-flag]").removeClass("notflagged")
|
||||
@$(".discussion-flag-abuse").attr("aria-pressed", "true")
|
||||
@$(".discussion-flag-abuse").attr("data-tooltip", gettext("Misuse Reported, click to remove report"))
|
||||
###
|
||||
Translators: The text between start_sr_span and end_span is not shown
|
||||
in most browsers but will be read by screen readers.
|
||||
###
|
||||
@$(".discussion-flag-abuse .flag-label").html(interpolate(gettext("Misuse Reported%(start_sr_span)s, click to remove report%(end_span)s"), {"start_sr_span": "<span class='sr'>", "end_span": "</span>"}, true))
|
||||
else
|
||||
@$("[data-role=thread-flag]").removeClass("flagged")
|
||||
@$("[data-role=thread-flag]").addClass("notflagged")
|
||||
@$(".discussion-flag-abuse").attr("aria-pressed", "false")
|
||||
@$(".discussion-flag-abuse .flag-label").html(gettext("Report Misuse"))
|
||||
|
||||
Reference in New Issue
Block a user