utils.coffee 13.9 KB
Newer Older
Rocky Duan committed
1
$ ->
2 3
  if !window.$$contents
    window.$$contents = {}
Rocky Duan committed
4
  $.fn.extend
5
    loading: (takeFocus) ->
6
      @$_loading = $("<div class='loading-animation' tabindex='0'><span class='sr'>" + gettext("Loading content") + "</span></div>")
7
      $(this).after(@$_loading)
8 9 10
      if takeFocus
        DiscussionUtil.makeFocusTrap(@$_loading)
        @$_loading.focus()
Rocky Duan committed
11
    loaded: ->
12
      @$_loading.remove()
Rocky Duan committed
13

Rocky Duan committed
14
class @DiscussionUtil
15

Rocky Duan committed
16
  @wmdEditors: {}
17

Rocky Duan committed
18 19
  @getTemplate: (id) ->
    $("script##{id}").html()
20

21 22 23
  @setUser: (user) ->
    @user = user

Greg Price committed
24 25 26
  @getUser: () ->
    @user

27 28 29 30 31 32
  @loadRoles: (roles)->
    @roleIds = roles

  @loadRolesFromContainer: ->
    @loadRoles($("#discussion-container").data("roles"))

33
  @isStaff: (user_id) ->
34
    user_id ?= @user?.id
35
    staff = _.union(@roleIds['Moderator'], @roleIds['Administrator'])
36 37
    _.include(staff, parseInt(user_id))

38
  @isTA: (user_id) ->
39
    user_id ?= @user?.id
40 41 42
    ta = _.union(@roleIds['Community TA'])
    _.include(ta, parseInt(user_id))

43 44 45
  @isPrivilegedUser: (user_id) ->
    @isStaff(user_id) || @isTA(user_id)

Rocky Duan committed
46 47
  @bulkUpdateContentInfo: (infos) ->
    for id, info of infos
48
      Content.getContent(id).updateInfo(info)
Rocky Duan committed
49 50

  @generateDiscussionLink: (cls, txt, handler) ->
Rocky Duan committed
51 52 53 54
    $("<a>").addClass("discussion-link")
            .attr("href", "javascript:void(0)")
            .addClass(cls).html(txt)
            .click -> handler(this)
55

Rocky Duan committed
56
  @urlFor: (name, param, param1, param2) ->
57
    {
Rocky Duan committed
58 59 60 61 62 63
      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"
      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"
64 65 66 67
      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"
Rocky Duan committed
68 69
      upvote_thread           : "/courses/#{$$course_id}/discussion/threads/#{param}/upvote"
      downvote_thread         : "/courses/#{$$course_id}/discussion/threads/#{param}/downvote"
Kevin Chugh committed
70
      pin_thread              : "/courses/#{$$course_id}/discussion/threads/#{param}/pin"
Your Name committed
71
      un_pin_thread           : "/courses/#{$$course_id}/discussion/threads/#{param}/unpin"
Rocky Duan committed
72 73 74 75 76 77 78 79 80 81 82
      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"
83
      users                   : "/courses/#{$$course_id}/discussion/users"
Rocky Duan committed
84 85 86
      search                  : "/courses/#{$$course_id}/discussion/forum/search"
      retrieve_discussion     : "/courses/#{$$course_id}/discussion/forum/#{param}/inline"
      retrieve_single_thread  : "/courses/#{$$course_id}/discussion/forum/#{param}/threads/#{param1}"
87 88 89
      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}"
90
      user_profile            : "/courses/#{$$course_id}/discussion/forum/users/#{param}"
91
      followed_threads        : "/courses/#{$$course_id}/discussion/forum/users/#{param}/followed"
92
      threads                 : "/courses/#{$$course_id}/discussion/forum"
93 94
      "enable_notifications"  : "/notification_prefs/enable/"
      "disable_notifications" : "/notification_prefs/disable/"
95
      "notifications_status" : "/notification_prefs/status/"
96 97
    }[name]

98 99 100 101
  @ignoreEnterKey: (event) =>
    if event.which == 13
      event.preventDefault()

102 103
  @activateOnSpace: (event, func) ->
    if event.which == 32
104
      event.preventDefault()
105 106
      func(event)

107 108 109 110 111 112 113
  @makeFocusTrap: (elem) ->
    elem.keydown(
      (event) ->
        if event.which == 9 # Tab
          event.preventDefault()
    )

114 115 116 117 118
  @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'>" +
119
        "  <button class='close-modal dismiss' aria-hidden='true'><i class='icon fa fa-times'></i></button>" +
120 121 122
        "  <header><h2/><hr/></header>" +
        "  <p id='discussion-alert-message'/>" +
        "  <hr/>" +
123
        "  <button class='dismiss'>" + gettext("OK") + "</button>" +
124 125
        "</div>"
      )
126
      @makeFocusTrap(alertDiv.find("button"))
127 128 129 130 131 132 133 134
      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()

Rocky Duan committed
135
  @safeAjax: (params) ->
136
    $elem = params.$elem
137

138
    if $elem and $elem.attr("disabled")
139 140 141 142
      deferred = $.Deferred()
      deferred.reject()
      return deferred.promise()

143
    params["url"] = URI(params["url"]).addSearch ajax: 1
Rocky Duan committed
144
    params["beforeSend"] = ->
145 146
      if $elem
        $elem.attr("disabled", "disabled")
Rocky Duan committed
147
      if params["$loading"]
148 149 150
        if params["loadingCallback"]?
          params["loadingCallback"].apply(params["$loading"])
        else
151
          params["$loading"].loading(params["takeFocus"])
152 153 154
    if !params["error"]
      params["error"] = =>
        @discussionAlert(
155 156
          gettext("Sorry"),
          gettext("We had some trouble processing your request. Please ensure you have copied any unsaved work and then reload the page.")
157
        )
158
    request = $.ajax(params).always ->
159 160
      if $elem
        $elem.removeAttr("disabled")
Rocky Duan committed
161
      if params["$loading"]
162 163 164 165
        if params["loadedCallback"]?
          params["loadedCallback"].apply(params["$loading"])
        else
          params["$loading"].loaded()
166
    return request
167

Greg Price committed
168 169 170 171 172 173 174
  @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))

Rocky Duan committed
175
  @bindLocalEvents: ($local, eventsHandler) ->
Rocky Duan committed
176 177
    for eventSelector, handler of eventsHandler
      [event, selector] = eventSelector.split(' ')
178
      $local(selector).unbind(event)[event] handler
Rocky Duan committed
179

Rocky Duan committed
180
  @formErrorHandler: (errorsField) ->
181
    (xhr, textStatus, error) ->
182
      makeErrorElem = (message) ->
Greg Price committed
183
        $("<li>").addClass("post-error").html(message)
184 185 186 187 188 189 190 191 192 193 194 195
      errorsField.empty().show()
      if xhr.status == 400
        response = JSON.parse(xhr.responseText)
        if response.errors? and response.errors.length > 0
          for error in response.errors
            errorsField.append(makeErrorElem(error))
      else
        errorsField.append(
          makeErrorElem(
            gettext("We had some trouble processing your request. Please try again.")
          )
        )
196

Rocky Duan committed
197
  @clearFormErrors: (errorsField) ->
Rocky Duan committed
198
    errorsField.empty()
199

Rocky Duan committed
200
  @postMathJaxProcessor: (text) ->
201 202
    RE_INLINEMATH = /^\$([^\$]*)\$/g
    RE_DISPLAYMATH = /^\$\$([^\$]*)\$\$/g
Rocky Duan committed
203
    @processEachMathAndCode text, (s, type) ->
204 205 206 207 208 209 210 211 212
      if type == 'display'
        s.replace RE_DISPLAYMATH, ($0, $1) ->
          "\\[" + $1 + "\\]"
      else if type == 'inline'
        s.replace RE_INLINEMATH, ($0, $1) ->
          "\\(" + $1 + "\\)"
      else
        s

Rocky Duan committed
213
  @makeWmdEditor: ($content, $local, cls_identifier) ->
214
    elem = $local(".#{cls_identifier}")
215
    placeholder = elem.data('placeholder')
216
    id = elem.attr("data-id") # use attr instead of data because we want to avoid type coercion
217
    appended_id = "-#{cls_identifier}-#{id}"
Rocky Duan committed
218 219 220 221 222
    imageUploadUrl = @urlFor('upload')
    _processor = (_this) ->
      (text) -> _this.postMathJaxProcessor(text)
    editor = Markdown.makeWmdEditor elem, appended_id, imageUploadUrl, _processor(@)
    @wmdEditors["#{cls_identifier}-#{id}"] = editor
223 224
    if placeholder?
      elem.find("#wmd-input#{appended_id}").attr('placeholder', placeholder)
225 226
    editor

Rocky Duan committed
227
  @getWmdEditor: ($content, $local, cls_identifier) ->
228
    elem = $local(".#{cls_identifier}")
229
    id = elem.attr("data-id") # use attr instead of data because we want to avoid type coercion
Rocky Duan committed
230
    @wmdEditors["#{cls_identifier}-#{id}"]
231

Rocky Duan committed
232
  @getWmdInput: ($content, $local, cls_identifier) ->
233
    elem = $local(".#{cls_identifier}")
234
    id = elem.attr("data-id") # use attr instead of data because we want to avoid type coercion
Rocky Duan committed
235 236
    $local("#wmd-input-#{cls_identifier}-#{id}")

Rocky Duan committed
237 238 239 240 241 242 243 244
  @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) ->
245

246 247
    codeArchive = []

248
    RE_DISPLAYMATH = /^([^\$]*?)\$\$([^\$]*?)\$\$(.*)$/m
249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271
    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) ->
272 273 274 275
          #processedText += $1 + processor("$$" + $2 + "$$", 'display')
          #bug fix, ordering is off
          processedText =  processor("$$" + $2 + "$$", 'display') + processedText
          processedText = $1 + processedText
276 277 278 279 280 281
          $3
      else
        processedText += text
        break

    text = processedText
Rocky Duan committed
282
    text = text.replace(new RegExp(ESCAPED_DOLLAR, 'g'), '\\$')
283 284 285 286

    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}}")
Rocky Duan committed
287
    text = text.replace(new RegExp(ESCAPED_BACKSLASH, 'g'), '\\\\\\\\')
288 289 290 291 292 293 294

    $div = $("<div>").html(text)
    cnt = 0
    $div.find("code").each (index, code) ->
      $(code).html(processor(codeArchive[cnt], 'code'))
      cnt += 1

Rocky Duan committed
295
    text = $div.html()
296

Rocky Duan committed
297
    text
Rocky Duan committed
298 299 300 301 302 303 304 305 306 307 308 309 310

  @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) ->
Arjun Singh committed
311
    text = text.replace(/^\&gt\;/gm, ">")
Rocky Duan committed
312
    converter = Markdown.getMathCompatibleConverter()
Arjun Singh committed
313 314
    text = @unescapeHighlightTag @stripLatexHighlight converter.makeHtml text
    return text.replace(/^>/gm,"&gt;")
315 316 317

  @abbreviateString: (text, minLength) ->
    # Abbreviates a string to at least minLength characters, stopping at word boundaries
Ibrahim Awwal committed
318
    if text.length<minLength
319 320 321 322
      return text
    else
      while minLength < text.length && text[minLength] != ' '
        minLength++
323
      return text.substr(0, minLength) + gettext('…')
324

325 326 327 328 329 330 331 332 333 334
  @abbreviateHTML: (html, minLength) ->
    # Abbreviates the html to at least minLength characters, stopping at word boundaries
    truncated_text = jQuery.truncate(html, {length: minLength, noBreaks: true, ellipsis: gettext('…')})
    $result = $("<div>" + truncated_text + "</div>")
    imagesToReplace = $result.find("img:not(:first)")
    if imagesToReplace.length > 0
        $result.append("<p><em>Some images in this post have been omitted</em></p>")
    imagesToReplace.replaceWith("<em>image omitted</em>")
    $result.html()

335 336 337 338 339 340 341 342 343 344 345 346 347 348 349
  @getPaginationParams: (curPage, numPages, pageUrlFunc) =>
    delta = 2
    minPage = Math.max(curPage - delta, 1)
    maxPage = Math.min(curPage + delta, numPages)
    pageInfo = (pageNum) -> {number: pageNum, url: pageUrlFunc(pageNum)}
    params =
      page: curPage
      lowPages: _.range(minPage, curPage).map(pageInfo)
      highPages: _.range(curPage+1, maxPage+1).map(pageInfo)
      previous: if curPage > 1 then pageInfo(curPage - 1) else null
      next: if curPage < numPages then pageInfo(curPage + 1) else null
      leftdots: minPage > 2
      rightdots: maxPage < numPages-1
      first: if minPage > 1 then pageInfo(1) else null
      last: if maxPage < numPages then pageInfo(numPages) else null