Make discussion js work for cms as well as lms so that inline discussion xmodules load

This commit is contained in:
Calen Pennington
2012-09-25 13:11:48 -04:00
parent c0ca7fe697
commit 44805ca383
34 changed files with 191 additions and 172 deletions

View File

@@ -0,0 +1,18 @@
import glob2
def rooted_glob(root, glob):
"""
Returns the results of running `glob` rooted in the directory `root`.
All returned paths are relative to `root`.
Uses glob2 globbing
"""
return remove_root(root, glob2.glob('{root}/{glob}'.format(root=root, glob=glob)))
def remove_root(root, paths):
"""
Returns `paths` made relative to `root`
"""
return [pth.replace(root + '/', '') for pth in paths]

View File

@@ -17,7 +17,7 @@ class @Video
@embed()
else
window.onYouTubePlayerAPIReady = =>
$('.course-content .video').each ->
@el.each ->
$(this).data('video').embed()
youtubeId: (speed)->

View File

@@ -0,0 +1,107 @@
"""
This module has utility functions for gathering up the static content
that is defined by XModules and XModuleDescriptors (javascript and css)
"""
import hashlib
import os
import errno
from collections import defaultdict
from .x_module import XModuleDescriptor
def write_module_styles(output_root, extra_descriptors):
return _write_styles(output_root, _list_modules(extra_descriptors))
def write_module_js(output_root, extra_descriptors):
return _write_js(output_root, _list_modules(extra_descriptors))
def write_descriptor_styles(output_root, extra_descriptors):
return _write_styles(output_root, _list_descriptors(extra_descriptors))
def write_descriptor_js(output_root, extra_descriptors):
return _write_js(output_root, _list_descriptors(extra_descriptors))
def _list_descriptors(extra_descriptors):
return [
desc for desc in [
desc for (_, desc) in XModuleDescriptor.load_classes()
] + extra_descriptors
]
def _list_modules(extra_descriptors):
return [
desc.module_class
for desc
in _list_descriptors(extra_descriptors)
]
def _ensure_dir(dir_):
try:
os.makedirs(dir_)
except OSError as exc:
if exc.errno == errno.EEXIST:
pass
else:
raise
def _write_styles(output_root, classes):
_ensure_dir(output_root)
css_fragments = defaultdict(set)
for class_ in classes:
class_css = class_.get_css()
for filetype in ('sass', 'scss', 'css'):
for idx, fragment in enumerate(class_css.get(filetype, [])):
css_fragments[idx, filetype, fragment].add(class_.__name__)
css_imports = defaultdict(set)
for (idx, filetype, fragment), classes in sorted(css_fragments.items()):
fragment_name = "{idx}-{hash}.{type}".format(
idx=idx,
hash=hashlib.md5(fragment).hexdigest(),
type=filetype)
# Prepend _ so that sass just includes the files into a single file
with open(output_root / '_' + fragment_name, 'w') as css_file:
css_file.write(fragment)
for class_ in classes:
css_imports[class_].add(fragment_name)
with open(output_root / '_module-styles.scss', 'w') as module_styles:
for class_, fragment_names in css_imports.items():
imports = "\n".join('@import "{0}";'.format(name) for name in fragment_names)
module_styles.write(""".xmodule_{class_} {{ {imports} }}""".format(
class_=class_, imports=imports
))
def _write_js(output_root, classes):
_ensure_dir(output_root)
js_fragments = set()
for class_ in classes:
module_js = class_.get_javascript()
for filetype in ('coffee', 'js'):
for idx, fragment in enumerate(module_js.get(filetype, [])):
js_fragments.add((idx, filetype, fragment))
module_js = []
for idx, filetype, fragment in sorted(js_fragments):
path = output_root / "{idx}-{hash}.{type}".format(
idx=idx,
hash=hashlib.md5(fragment).hexdigest(),
type=filetype)
with open(path, 'w') as js_file:
js_file.write(fragment)
module_js.append(path)
return module_js

View File

@@ -0,0 +1,163 @@
if Backbone?
class @Content extends Backbone.Model
@contents: {}
@contentInfos: {}
template: -> DiscussionUtil.getTemplate('_content')
actions:
editable: '.admin-edit'
can_reply: '.discussion-reply'
can_endorse: '.admin-endorse'
can_delete: '.admin-delete'
can_openclose: '.admin-openclose'
urlMappers: {}
urlFor: (name) ->
@urlMappers[name].apply(@)
can: (action) ->
(@get('ability') || {})[action]
updateInfo: (info) ->
if info
@set('ability', info.ability)
@set('voted', info.voted)
@set('subscribed', info.subscribed)
addComment: (comment, options) ->
options ||= {}
if not options.silent
thread = @get('thread')
comments_count = parseInt(thread.get('comments_count'))
thread.set('comments_count', comments_count + 1)
@get('children').push comment
model = new Comment $.extend {}, comment, { thread: @get('thread') }
@get('comments').add model
@trigger "comment:add"
model
removeComment: (comment) ->
thread = @get('thread')
comments_count = parseInt(thread.get('comments_count'))
thread.set('comments_count', comments_count - 1 - comment.getCommentsCount())
@trigger "comment:remove"
resetComments: (children) ->
@set 'children', []
@set 'comments', new Comments()
for comment in (children || [])
@addComment comment, { silent: true }
initialize: ->
Content.addContent @id, @
if Content.getInfo(@id)
@updateInfo(Content.getInfo(@id))
@set 'user_url', DiscussionUtil.urlFor('user_profile', @get('user_id'))
@resetComments(@get('children'))
remove: ->
if @get('type') == 'comment'
@get('thread').removeComment(@)
@get('thread').trigger "comment:remove", @
else
@trigger "thread:remove", @
@addContent: (id, content) -> @contents[id] = content
@getContent: (id) -> @contents[id]
@getInfo: (id) ->
@contentInfos[id]
@loadContentInfos: (infos) ->
for id, info of infos
if @getContent(id)
@getContent(id).updateInfo(info)
$.extend @contentInfos, infos
class @Thread extends @Content
urlMappers:
'retrieve' : -> DiscussionUtil.urlFor('retrieve_single_thread', @discussion.id, @id)
'reply' : -> DiscussionUtil.urlFor('create_comment', @id)
'unvote' : -> DiscussionUtil.urlFor("undo_vote_for_#{@get('type')}", @id)
'upvote' : -> DiscussionUtil.urlFor("upvote_#{@get('type')}", @id)
'downvote' : -> DiscussionUtil.urlFor("downvote_#{@get('type')}", @id)
'close' : -> DiscussionUtil.urlFor('openclose_thread', @id)
'update' : -> DiscussionUtil.urlFor('update_thread', @id)
'delete' : -> DiscussionUtil.urlFor('delete_thread', @id)
'follow' : -> DiscussionUtil.urlFor('follow_thread', @id)
'unfollow' : -> DiscussionUtil.urlFor('unfollow_thread', @id)
initialize: ->
@set('thread', @)
super()
comment: ->
@set("comments_count", parseInt(@get("comments_count")) + 1)
follow: ->
@set('subscribed', true)
unfollow: ->
@set('subscribed', false)
vote: ->
@get("votes")["up_count"] = parseInt(@get("votes")["up_count"]) + 1
@trigger "change", @
unvote: ->
@get("votes")["up_count"] = parseInt(@get("votes")["up_count"]) - 1
@trigger "change", @
display_body: ->
if @has("highlighted_body")
String(@get("highlighted_body")).replace(/<highlight>/g, '<mark>').replace(/<\/highlight>/g, '</mark>')
else
@get("body")
display_title: ->
if @has("highlighted_title")
String(@get("highlighted_title")).replace(/<highlight>/g, '<mark>').replace(/<\/highlight>/g, '</mark>')
else
@get("title")
toJSON: ->
json_attributes = _.clone(@attributes)
_.extend(json_attributes, { title: @display_title(), body: @display_body() })
created_at_date: ->
new Date(@get("created_at"))
created_at_time: ->
new Date(@get("created_at")).getTime()
class @Comment extends @Content
urlMappers:
'reply': -> DiscussionUtil.urlFor('create_sub_comment', @id)
'unvote': -> DiscussionUtil.urlFor("undo_vote_for_#{@get('type')}", @id)
'upvote': -> DiscussionUtil.urlFor("upvote_#{@get('type')}", @id)
'downvote': -> DiscussionUtil.urlFor("downvote_#{@get('type')}", @id)
'endorse': -> DiscussionUtil.urlFor('endorse_comment', @id)
'update': -> DiscussionUtil.urlFor('update_comment', @id)
'delete': -> DiscussionUtil.urlFor('delete_comment', @id)
getCommentsCount: ->
count = 0
@get('comments').each (comment) ->
count += comment.getCommentsCount() + 1
count
class @Comments extends Backbone.Collection
model: Comment
initialize: ->
@bind "add", (item) =>
item.collection = @
find: (id) ->
_.first @where(id: id)

View File

@@ -0,0 +1,82 @@
if Backbone?
class @Discussion extends Backbone.Collection
model: Thread
initialize: (models, options={})->
@pages = options['pages'] || 1
@current_page = 1
@bind "add", (item) =>
item.discussion = @
@comparator = @sortByDateRecentFirst
@on "thread:remove", (thread) =>
@remove(thread)
find: (id) ->
_.first @where(id: id)
hasMorePages: ->
@current_page < @pages
addThread: (thread, options) ->
# TODO: Check for existing thread with same ID in a faster way
if not @find(thread.id)
options ||= {}
model = new Thread thread
@add model
model
retrieveAnotherPage: (mode, options={}, sort_options={})->
@current_page += 1
data = { page: @current_page }
switch mode
when 'search'
url = DiscussionUtil.urlFor 'search'
data['text'] = options.search_text
when 'commentables'
url = DiscussionUtil.urlFor 'search'
data['commentable_ids'] = options.commentable_ids
when 'all'
url = DiscussionUtil.urlFor 'threads'
when 'followed'
url = DiscussionUtil.urlFor 'followed_threads', options.user_id
data['sort_key'] = sort_options.sort_key || 'date'
data['sort_order'] = sort_options.sort_order || 'desc'
DiscussionUtil.safeAjax
$elem: @$el
url: url
data: data
dataType: 'json'
success: (response, textStatus) =>
models = @models
new_threads = [new Thread(data) for data in response.discussion_data][0]
new_collection = _.union(models, new_threads)
Content.loadContentInfos(response.annotated_content_info)
@reset new_collection
@pages = response.num_pages
@current_page = response.page
sortByDate: (thread) ->
thread.get("created_at")
sortByDateRecentFirst: (thread) ->
-(new Date(thread.get("created_at")).getTime())
#return String.fromCharCode.apply(String,
# _.map(thread.get("created_at").split(""),
# ((c) -> return 0xffff - c.charChodeAt()))
#)
sortByVotes: (thread1, thread2) ->
thread1_count = parseInt(thread1.get("votes")['up_count'])
thread2_count = parseInt(thread2.get("votes")['up_count'])
if thread2_count != thread1_count
thread2_count - thread1_count
else
thread2.created_at_time() - thread1.created_at_time()
sortByComments: (thread1, thread2) ->
thread1_count = parseInt(thread1.get("comments_count"))
thread2_count = parseInt(thread2.get("comments_count"))
if thread2_count != thread1_count
thread2_count - thread1_count
else
thread2.created_at_time() - thread1.created_at_time()

View File

@@ -0,0 +1,28 @@
class @DiscussionFilter
@filterDrop: (e) ->
$drop = $(e.target).parents('.topic_menu_wrapper, .browse-topic-drop-menu-wrapper')
query = $(e.target).val()
$items = $drop.find('a')
if(query.length == 0)
$items.removeClass('hidden')
return;
$items.addClass('hidden')
$items.each (i) ->
thisText = $(this).not('.unread').text()
$(this).parents('ul').siblings('a').not('.unread').each (i) ->
thisText = thisText + ' ' + $(this).text();
test = true
terms = thisText.split(' ')
if(thisText.toLowerCase().search(query.toLowerCase()) == -1)
test = false
if(test)
$(this).removeClass('hidden')
# show children
$(this).parent().find('a').removeClass('hidden');
# show parents
$(this).parents('ul').siblings('a').removeClass('hidden');

View File

@@ -0,0 +1,124 @@
if Backbone?
class @DiscussionModuleView extends Backbone.View
events:
"click .discussion-show": "toggleDiscussion"
"click .new-post-btn": "toggleNewPost"
"click .new-post-cancel": "hideNewPost"
"click .discussion-paginator a": "navigateToPage"
paginationTemplate: -> DiscussionUtil.getTemplate("_pagination")
page_re: /\?discussion_page=(\d+)/
initialize: ->
@toggleDiscussionBtn = @$(".discussion-show")
# Set the page if it was set in the URL. This is used to allow deep linking to pages
match = @page_re.exec(window.location.href)
if match
@page = parseInt(match[1])
else
@page = 1
toggleNewPost: (event) ->
event.preventDefault()
if !@newPostForm
@toggleDiscussion()
@isWaitingOnNewPost = true;
return
if @showed
@newPostForm.slideDown(300)
else
@newPostForm.show()
@toggleDiscussionBtn.addClass('shown')
@toggleDiscussionBtn.find('.button-text').html("Hide Discussion")
@$("section.discussion").slideDown()
@showed = true
hideNewPost: (event) ->
event.preventDefault()
@newPostForm.slideUp(300)
toggleDiscussion: (event) ->
if @showed
@$("section.discussion").slideUp()
@toggleDiscussionBtn.removeClass('shown')
@toggleDiscussionBtn.find('.button-text').html("Show Discussion")
@showed = false
else
@toggleDiscussionBtn.addClass('shown')
@toggleDiscussionBtn.find('.button-text').html("Hide Discussion")
if @retrieved
@$("section.discussion").slideDown()
@showed = true
else
$elem = @toggleDiscussionBtn
@loadPage $elem
loadPage: ($elem)=>
discussionId = @$el.data("discussion-id")
url = DiscussionUtil.urlFor('retrieve_discussion', discussionId) + "?page=#{@page}"
DiscussionUtil.safeAjax
$elem: $elem
$loading: $elem
url: url
type: "GET"
dataType: 'json'
success: (response, textStatus, jqXHR) => @renderDiscussion($elem, response, textStatus, discussionId)
renderDiscussion: ($elem, response, textStatus, discussionId) =>
window.user = new DiscussionUser(response.user_info)
Content.loadContentInfos(response.annotated_content_info)
DiscussionUtil.loadRoles(response.roles)
allow_anonymous = response.allow_anonymous
allow_anonymous_to_peers = response.allow_anonymous_to_peers
# $elem.html("Hide Discussion")
@discussion = new Discussion()
@discussion.reset(response.discussion_data, {silent: false})
$discussion = $(Mustache.render $("script#_inline_discussion").html(), {'threads':response.discussion_data, 'discussionId': discussionId, 'allow_anonymous_to_peers': allow_anonymous_to_peers, 'allow_anonymous': allow_anonymous})
if @$('section.discussion').length
@$('section.discussion').replaceWith($discussion)
else
$(".discussion-module").append($discussion)
@newPostForm = $('.new-post-article')
@threadviews = @discussion.map (thread) ->
new DiscussionThreadInlineView el: @$("article#thread_#{thread.id}"), model: thread
_.each @threadviews, (dtv) -> dtv.render()
DiscussionUtil.bulkUpdateContentInfo(window.$$annotated_content_info)
@newPostView = new NewPostInlineView el: @$('.new-post-article'), collection: @discussion
@discussion.on "add", @addThread
@retrieved = true
@showed = true
@renderPagination(2, response.num_pages)
if @isWaitingOnNewPost
@newPostForm.show()
addThread: (thread, collection, options) =>
# TODO: When doing pagination, this will need to repaginate. Perhaps just reload page 1?
article = $("<article class='discussion-thread' id='thread_#{thread.id}'></article>")
@$('section.discussion > .threads').prepend(article)
threadView = new DiscussionThreadInlineView el: article, model: thread
threadView.render()
@threadviews.unshift threadView
renderPagination: (delta, numPages) =>
minPage = Math.max(@page - delta, 1)
maxPage = Math.min(@page + delta, numPages)
pageUrl = (number) ->
"?discussion_page=#{number}"
params =
page: @page
lowPages: _.range(minPage, @page).map (n) -> {number: n, url: pageUrl(n)}
highPages: _.range(@page+1, maxPage+1).map (n) -> {number: n, url: pageUrl(n)}
previous: if @page-1 >= 1 then {url: pageUrl(@page-1), number: @page-1} else false
next: if @page+1 <= numPages then {url: pageUrl(@page+1), number: @page+1} else false
leftdots: minPage > 2
rightdots: maxPage < numPages-1
first: if minPage > 1 then {url: pageUrl(1)} else false
last: if maxPage < numPages then {number: numPages, url: pageUrl(numPages)} else false
thing = Mustache.render @paginationTemplate(), params
@$('section.pagination').html(thing)
navigateToPage: (event) =>
event.preventDefault()
window.history.pushState({}, window.document.title, event.target.href)
@page = $(event.target).data('page-number')
@loadPage($(event.target))

View File

@@ -0,0 +1,57 @@
if Backbone?
class @DiscussionRouter extends Backbone.Router
routes:
"": "allThreads"
":forum_name/threads/:thread_id" : "showThread"
initialize: (options) ->
@discussion = options['discussion']
@nav = new DiscussionThreadListView(collection: @discussion, el: $(".sidebar"))
@nav.on "thread:selected", @navigateToThread
@nav.on "thread:removed", @navigateToAllThreads
@nav.on "threads:rendered", @setActiveThread
@nav.render()
@newPostView = new NewPostView(el: $(".new-post-article"), collection: @discussion)
@nav.on "thread:created", @navigateToThread
@newPost = $('.new-post-article')
$('.new-post-btn').bind "click", @showNewPost
$('.new-post-cancel').bind "click", @hideNewPost
allThreads: ->
@nav.updateSidebar()
setActiveThread: =>
if @thread
@nav.setActiveThread(@thread.get("id"))
showThread: (forum_name, thread_id) ->
@thread = @discussion.get(thread_id)
@thread.set("unread_comments_count", 0)
@thread.set("read", true)
@setActiveThread()
if(@main)
@main.cleanup()
@main.undelegateEvents()
@main = new DiscussionThreadView(el: $(".discussion-column"), model: @thread)
@main.render()
@main.on "thread:responses:rendered", =>
@nav.updateSidebar()
@main.on "tag:selected", (tag) =>
search = "[#{tag}]"
@nav.setAndSearchFor(search)
navigateToThread: (thread_id) =>
thread = @discussion.get(thread_id)
@navigate("#{thread.get("commentable_id")}/threads/#{thread_id}", trigger: true)
navigateToAllThreads: =>
@navigate("", trigger: true)
showNewPost: (event) =>
@newPost.slideDown(300)
$('.new-post-title').focus()
hideNewPost: (event) =>
@newPost.slideUp(300)

View File

@@ -0,0 +1,29 @@
if Backbone?
DiscussionApp =
start: (elem)->
# TODO: Perhaps eliminate usage of global variables when possible
DiscussionUtil.loadRolesFromContainer()
element = $(elem)
window.$$course_id = element.data("course-id")
user_info = element.data("user-info")
threads = element.data("threads")
thread_pages = element.data("thread-pages")
content_info = element.data("content-info")
window.user = new DiscussionUser(user_info)
Content.loadContentInfos(content_info)
discussion = new Discussion(threads, pages: thread_pages)
new DiscussionRouter({discussion: discussion})
Backbone.history.start({pushState: true, root: "/courses/#{$$course_id}/discussion/forum/"})
DiscussionProfileApp =
start: (elem) ->
element = $(elem)
window.$$course_id = element.data("course-id")
threads = element.data("threads")
user_info = element.data("user-info")
window.user = new DiscussionUser(user_info)
new DiscussionUserProfileView(el: element, collection: threads)
$ ->
$("section.discussion").each (index, elem) ->
DiscussionApp.start(elem)
$("section.discussion-user-threads").each (index, elem) ->
DiscussionProfileApp.start(elem)

View File

@@ -0,0 +1,15 @@
if Backbone?
class @DiscussionUser extends Backbone.Model
following: (thread) ->
_.include(@get('subscribed_thread_ids'), thread.id)
voted: (thread) ->
_.include(@get('upvoted_ids'), thread.id)
vote: (thread) ->
@get('upvoted_ids').push(thread.id)
thread.vote()
unvote: (thread) ->
@set('upvoted_ids', _.without(@get('upvoted_ids'), thread.id))
thread.unvote()

View File

@@ -0,0 +1,47 @@
$ ->
new TooltipManager
class @TooltipManager
constructor: () ->
@$body = $('body')
@$tooltip = $('<div class="tooltip"></div>')
@$body.delegate '[data-tooltip]',
'mouseover': @showTooltip,
'mousemove': @moveTooltip,
'mouseout': @hideTooltip,
'click': @hideTooltip
showTooltip: (e) =>
tooltipText = $(e.target).attr('data-tooltip')
@$tooltip.html(tooltipText)
@$body.append(@$tooltip)
$(e.target).children().css('pointer-events', 'none')
tooltipCoords =
x: e.pageX - (@$tooltip.outerWidth() / 2)
y: e.pageY - (@$tooltip.outerHeight() + 15)
@$tooltip.css
'left': tooltipCoords.x,
'top': tooltipCoords.y
@tooltipTimer = setTimeout ()=>
@$tooltip.show().css('opacity', 1)
@tooltipTimer = setTimeout ()=>
@hideTooltip()
, 3000
, 500
moveTooltip: (e) =>
tooltipCoords =
x: e.pageX - (@$tooltip.outerWidth() / 2)
y: e.pageY - (@$tooltip.outerHeight() + 15)
@$tooltip.css
'left': tooltipCoords.x
'top': tooltipCoords.y
hideTooltip: (e) =>
@$tooltip.hide().css('opacity', 0)
clearTimeout(@tooltipTimer)

View File

@@ -0,0 +1,30 @@
if Backbone?
class @DiscussionUserProfileView extends Backbone.View
toggleModeratorStatus: (event) ->
confirmValue = confirm("Are you sure?")
if not confirmValue then return
$elem = $(event.target)
if $elem.hasClass("sidebar-promote-moderator-button")
isModerator = true
else if $elem.hasClass("sidebar-revoke-moderator-button")
isModerator = false
else
console.error "unrecognized moderator status"
return
url = DiscussionUtil.urlFor('update_moderator_status', $$profiled_user_id)
DiscussionUtil.safeAjax
$elem: $elem
url: url
type: "POST"
dataType: 'json'
data:
is_moderator: isModerator
error: (response, textStatus, e) ->
console.log e
success: (response, textStatus) =>
parent = @$el.parent()
@$el.replaceWith(response.html)
view = new DiscussionUserProfileView el: parent.children(".user-profile")
events:
"click .sidebar-toggle-moderator-button": "toggleModeratorStatus"

View File

@@ -0,0 +1,297 @@
$ ->
if !window.$$contents
window.$$contents = {}
$.fn.extend
loading: ->
@$_loading = $("<div class='loading-animation'></div>")
$(this).after(@$_loading)
loaded: ->
@$_loading.remove()
class @DiscussionUtil
@wmdEditors: {}
@getTemplate: (id) ->
$("script##{id}").html()
@loadRoles: (roles)->
@roleIds = roles
@loadRolesFromContainer: ->
@loadRoles($("#discussion-container").data("roles"))
@isStaff: (user_id) ->
staff = _.union(@roleIds['Staff'], @roleIds['Moderator'], @roleIds['Administrator'])
_.include(staff, 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"
upvote_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/upvote"
downvote_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/downvote"
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"
}[name]
@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()
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
@get: ($elem, url, data, success) ->
@safeAjax
$elem: $elem
url: url
type: "GET"
dataType: "json"
data: data
success: success
@post: ($elem, url, data, success) ->
@safeAjax
$elem: $elem
url: url
type: "POST"
dataType: "json"
data: data
success: success
@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.data("id")
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.data("id")
@wmdEditors["#{cls_identifier}-#{id}"]
@getWmdInput: ($content, $local, cls_identifier) ->
elem = $local(".#{cls_identifier}")
id = elem.data("id")
$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()
@subscriptionLink: (type, id) ->
followLink = ->
@generateDiscussionLink("discussion-follow-#{type}", "Follow", handleFollow)
unfollowLink = ->
@generateDiscussionLink("discussion-unfollow-#{type}", "Unfollow", handleUnfollow)
handleFollow = (elem) ->
@safeAjax
$elem: $(elem)
url: @urlFor("follow_#{type}", id)
type: "POST"
success: (response, textStatus) ->
if textStatus == "success"
$(elem).replaceWith unfollowLink()
dataType: 'json'
handleUnfollow = (elem) ->
@safeAjax
$elem: $(elem)
url: @urlFor("unfollow_#{type}", id)
type: "POST"
success: (response, textStatus) ->
if textStatus == "success"
$(elem).replaceWith followLink()
dataType: 'json'
if @isSubscribed(id, type)
unfollowLink()
else
followLink()
@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')
$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) + '...'

View File

@@ -0,0 +1,100 @@
if Backbone?
class @DiscussionContentView extends Backbone.View
attrRenderer:
endorsed: (endorsed) ->
if endorsed
@$(".action-endorse").show().addClass("is-endorsed")
else
if @model.get('ability')?.can_endorse
@$(".action-endorse").show()
else
@$(".action-endorse").hide()
@$(".action-endorse").removeClass("is-endorsed")
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("Close", "Open"))
@$(".discussion-reply-new").hide()
else
@$(".post-status-closed").hide()
@$(".action-openclose").html(@$(".action-openclose").html().replace("Open", "Close"))
@$(".discussion-reply-new").show()
voted: (voted) ->
votes_point: (votes_point) ->
comments_count: (comments_count) ->
subscribed: (subscribed) ->
if subscribed
@$(".dogear").addClass("is-followed")
else
@$(".dogear").removeClass("is-followed")
ability: (ability) ->
for action, selector of @abilityRenderer
if not ability[action]
selector.disable.apply(@)
else
selector.enable.apply(@)
abilityRenderer:
editable:
enable: -> @$(".action-edit").closest("li").show()
disable: -> @$(".action-edit").closest("li").hide()
can_delete:
enable: -> @$(".action-delete").closest("li").show()
disable: -> @$(".action-delete").closest("li").hide()
can_endorse:
enable: ->
@$(".action-endorse").show().css("cursor", "auto")
disable: ->
@$(".action-endorse").css("cursor", "default")
if not @model.get('endorsed')
@$(".action-endorse").hide()
else
@$(".action-endorse").show()
can_openclose:
enable: -> @$(".action-openclose").closest("li").show()
disable: -> @$(".action-openclose").closest("li").hide()
renderPartialAttrs: ->
for attr, value of @model.changedAttributes()
if @attrRenderer[attr]
@attrRenderer[attr].apply(@, [value])
renderAttrs: ->
for attr, value of @model.attributes
if @attrRenderer[attr]
@attrRenderer[attr].apply(@, [value])
$: (selector) ->
@$local.find(selector)
initLocal: ->
@$local = @$el.children(".local")
if not @$local.length
@$local = @$el
@$delegateElement = @$local
makeWmdEditor: (cls_identifier) =>
if not @$el.find(".wmd-panel").length
DiscussionUtil.makeWmdEditor @$el, $.proxy(@$, @), cls_identifier
getWmdEditor: (cls_identifier) =>
DiscussionUtil.getWmdEditor @$el, $.proxy(@$, @), cls_identifier
getWmdContent: (cls_identifier) =>
DiscussionUtil.getWmdContent @$el, $.proxy(@$, @), cls_identifier
setWmdContent: (cls_identifier, text) =>
DiscussionUtil.setWmdContent @$el, $.proxy(@$, @), cls_identifier, text
initialize: ->
@initLocal()
@model.bind('change', @renderPartialAttrs, @)

View File

@@ -0,0 +1,26 @@
if Backbone?
class @DiscussionThreadEditView extends Backbone.View
events:
"click .post-update": "update"
"click .post-cancel": "cancel_edit"
$: (selector) ->
@$el.find(selector)
initialize: ->
super()
render: ->
@template = _.template($("#thread-edit-template").html())
@$el.html(@template(@model.toJSON()))
@delegateEvents()
DiscussionUtil.makeWmdEditor @$el, $.proxy(@$, @), "edit-post-body"
@$(".edit-post-tags").tagsInput DiscussionUtil.tagsInputOptions()
@
update: (event) ->
@trigger "thread:update", event
cancel_edit: (event) ->
@trigger "thread:cancel_edit", event

View File

@@ -0,0 +1,382 @@
if Backbone?
class @DiscussionThreadListView extends Backbone.View
events:
"click .search": "showSearch"
"click .browse": "toggleTopicDrop"
"keydown .post-search-field": "performSearch"
"click .sort-bar a": "sortThreads"
"click .browse-topic-drop-menu": "filterTopic"
"click .browse-topic-drop-search-input": "ignoreClick"
"click .post-list .list-item a": "threadSelected"
"click .post-list .more-pages a": "loadMorePages"
'keyup .browse-topic-drop-search-input': DiscussionFilter.filterDrop
initialize: ->
@displayedCollection = new Discussion(@collection.models, pages: @collection.pages)
@collection.on "change", @reloadDisplayedCollection
@sortBy = "date"
@discussionIds=""
@collection.on "reset", (discussion) =>
board = $(".current-board").html()
@displayedCollection.current_page = discussion.current_page
@displayedCollection.pages = discussion.pages
@displayedCollection.reset discussion.models
# TODO: filter correctly
# target = _.filter($("a.topic:contains('#{board}')"), (el) -> el.innerText == "General" || el.innerHTML == "General")
# if target.length > 0
# @filterTopic($.Event("filter", {'target': target[0]}))
@collection.on "add", @addAndSelectThread
@sidebar_padding = 10
@sidebar_header_height = 87
@boardName
@template = _.template($("#thread-list-template").html())
@current_search = ""
@mode = 'all'
reloadDisplayedCollection: (thread) =>
thread_id = thread.get('id')
content = @renderThread(thread)
current_el = @$("a[data-id=#{thread_id}]")
active = current_el.hasClass("active")
current_el.replaceWith(content)
if active
@setActiveThread(thread_id)
#TODO fix this entire chain of events
addAndSelectThread: (thread) =>
commentable_id = thread.get("commentable_id")
commentable = @$(".board-name[data-discussion_id]").filter(-> $(this).data("discussion_id").id == commentable_id)
@setTopicHack(commentable)
@retrieveDiscussion commentable_id, =>
@trigger "thread:created", thread.get('id')
updateSidebar: =>
scrollTop = $(window).scrollTop();
windowHeight = $(window).height();
discussionBody = $(".discussion-article")
discussionsBodyTop = if discussionBody[0] then discussionBody.offset().top
discussionsBodyBottom = discussionsBodyTop + discussionBody.outerHeight()
sidebar = $(".sidebar")
if scrollTop > discussionsBodyTop - @sidebar_padding
sidebar.addClass('fixed');
sidebar.css('top', @sidebar_padding);
else
sidebar.removeClass('fixed');
sidebar.css('top', '0');
sidebarWidth = .31 * $(".discussion-body").width();
sidebar.css('width', sidebarWidth + 'px');
sidebarHeight = windowHeight - Math.max(discussionsBodyTop - scrollTop, @sidebar_padding)
topOffset = scrollTop + windowHeight
discussionBottomOffset = discussionsBodyBottom + @sidebar_padding
amount = Math.max(topOffset - discussionBottomOffset, 0)
sidebarHeight = sidebarHeight - @sidebar_padding - amount
sidebarHeight = Math.min(sidebarHeight + 1, discussionBody.outerHeight())
sidebar.css 'height', sidebarHeight
postListWrapper = @$('.post-list-wrapper')
postListWrapper.css('height', (sidebarHeight - @sidebar_header_height - 4) + 'px')
# Because we want the behavior that when the body is clicked the menu is
# closed, we need to ignore clicks in the search field and stop propagation.
# Without this, clicking the search field would also close the menu.
ignoreClick: (event) ->
event.stopPropagation()
render: ->
@timer = 0
@$el.html(@template())
$(window).bind "scroll", @updateSidebar
$(window).bind "resize", @updateSidebar
@displayedCollection.on "reset", @renderThreads
@displayedCollection.on "thread:remove", @renderThreads
@renderThreads()
@
renderThreads: =>
@$(".post-list").html("")
rendered = $("<div></div>")
for thread in @displayedCollection.models
content = @renderThread(thread)
rendered.append content
content.wrap("<li class='list-item' data-id='\"#{thread.get('id')}\"' />")
@$(".post-list").html(rendered.html())
@renderMorePages()
@updateSidebar()
@trigger "threads:rendered"
renderMorePages: ->
if @displayedCollection.hasMorePages()
@$(".post-list").append("<li class='more-pages'><a href='#'>Load more</a></li>")
loadMorePages: (event) ->
if event
event.preventDefault()
@$(".more-pages").html('<div class="loading-animation"></div>')
@$(".more-pages").addClass("loading")
options = {}
switch @mode
when 'search'
options.search_text = @current_search
when 'followed'
options.user_id = window.user.id
when 'commentables'
options.commentable_ids = @discussionIds
@collection.retrieveAnotherPage(@mode, options, {sort_key: @sortBy})
renderThread: (thread) =>
content = $(_.template($("#thread-list-item-template").html())(thread.toJSON()))
if thread.get('subscribed')
content.addClass("followed")
if thread.get('endorsed')
content.addClass("resolved")
if thread.get('read')
content.addClass("read")
@highlight(content)
highlight: (el) ->
el.html(el.html().replace(/&lt;mark&gt;/g, "<mark>").replace(/&lt;\/mark&gt;/g, "</mark>"))
renderThreadListItem: (thread) =>
view = new ThreadListItemView(model: thread)
view.on "thread:selected", @threadSelected
view.on "thread:removed", @threadRemoved
view.render()
@$(".post-list").append(view.el)
threadSelected: (e) =>
thread_id = $(e.target).closest("a").data("id")
@setActiveThread(thread_id)
@trigger("thread:selected", thread_id) # This triggers a callback in the DiscussionRouter which calls the line above...
false
threadRemoved: (thread_id) =>
@trigger("thread:removed", thread_id)
setActiveThread: (thread_id) ->
@$(".post-list a[data-id!='#{thread_id}']").removeClass("active")
@$(".post-list a[data-id='#{thread_id}']").addClass("active")
showSearch: ->
@$(".browse").removeClass('is-dropped')
@hideTopicDrop()
@$(".search").addClass('is-open')
@$(".browse").removeClass('is-open')
setTimeout (-> @$(".post-search-field").focus()), 200
toggleTopicDrop: (event) =>
event.preventDefault()
event.stopPropagation()
if @current_search != ""
@clearSearch()
@$(".search").removeClass('is-open')
@$(".browse").addClass('is-open')
@$(".browse").toggleClass('is-dropped')
if @$(".browse").hasClass('is-dropped')
@$(".browse-topic-drop-menu-wrapper").show()
$(".browse-topic-drop-search-input").focus()
$("body").bind "click", @toggleTopicDrop
$("body").bind "keydown", @setActiveItem
else
@hideTopicDrop()
hideTopicDrop: ->
@$(".browse-topic-drop-menu-wrapper").hide()
$("body").unbind "click", @toggleTopicDrop
$("body").unbind "keydown", @setActiveItem
# TODO get rid of this asap
setTopicHack: (boardNameContainer) ->
item = $(boardNameContainer).closest('a')
boardName = item.find(".board-name").html()
_.each item.parents('ul').not('.browse-topic-drop-menu'), (parent) ->
boardName = $(parent).siblings('a').find('.board-name').html() + ' / ' + boardName
@$(".current-board").html(@fitName(boardName))
setTopic: (event) ->
item = $(event.target).closest('a')
boardName = item.find(".board-name").html()
_.each item.parents('ul').not('.browse-topic-drop-menu'), (parent) ->
boardName = $(parent).siblings('a').find('.board-name').html() + ' / ' + boardName
@$(".current-board").html(@fitName(boardName))
setSelectedTopic: (name) ->
@$(".current-board").html(@fitName(name))
getNameWidth: (name) ->
test = $("<div>")
test.css
"font-size": @$(".current-board").css('font-size')
opacity: 0
position: 'absolute'
left: -1000
top: -1000
$("body").append(test)
test.html(name)
width = test.width()
test.remove()
return width
fitName: (name) ->
@maxNameWidth = (@$el.width() * .8) - 50
width = @getNameWidth(name)
if width < @maxNameWidth
return name
path = (x.replace /^\s+|\s+$/g, "" for x in name.split("/"))
while path.length > 1
path.shift()
partialName = "…/" + path.join("/")
if @getNameWidth(partialName) < @maxNameWidth
return partialName
rawName = path[0]
name = "…/" + rawName
while @getNameWidth(name) > @maxNameWidth
rawName = rawName[0...rawName.length-1]
name = "…/" + rawName + ""
return name
filterTopic: (event) ->
if @current_search != ""
@setTopic(event)
@clearSearch @filterTopic, event
else
@setTopic(event) # just sets the title for the dropdown
item = $(event.target).closest('li')
discussionId = item.find("span.board-name").data("discussion_id")
if discussionId == "#all"
@discussionIds = ""
@$(".post-search-field").val("")
@retrieveAllThreads()
else if discussionId == "#following"
@retrieveFollowed(event)
else
discussionIds = _.map item.find(".board-name[data-discussion_id]"), (board) -> $(board).data("discussion_id").id
@retrieveDiscussions(discussionIds)
retrieveDiscussion: (discussion_id, callback=null) ->
url = DiscussionUtil.urlFor("retrieve_discussion", discussion_id)
DiscussionUtil.safeAjax
url: url
type: "GET"
success: (response, textStatus) =>
@collection.current_page = response.page
@collection.pages = response.num_pages
@collection.reset(response.discussion_data)
Content.loadContentInfos(response.annotated_content_info)
@displayedCollection.reset(@collection.models)# Don't think this is necessary because it's called on collection.reset
if callback?
callback()
retrieveDiscussions: (discussion_ids) ->
@discussionIds = discussion_ids.join(',')
@mode = 'commentables'
@retrieveFirstPage()
retrieveAllThreads: () ->
@mode = 'all'
@retrieveFirstPage()
retrieveFirstPage: (event)->
@collection.current_page = 0
@collection.reset()
@loadMorePages(event)
sortThreads: (event) ->
@$(".sort-bar a").removeClass("active")
$(event.target).addClass("active")
@sortBy = $(event.target).data("sort")
@displayedCollection.comparator = switch @sortBy
when 'date' then @displayedCollection.sortByDateRecentFirst
when 'votes' then @displayedCollection.sortByVotes
when 'comments' then @displayedCollection.sortByComments
@retrieveFirstPage(event)
performSearch: (event) ->
if event.which == 13
event.preventDefault()
text = @$(".post-search-field").val()
@searchFor(text)
setAndSearchFor: (text) ->
@showSearch()
@$(".post-search-field").val(text)
@searchFor(text)
searchFor: (text, callback, value) ->
@mode = 'search'
@current_search = text
url = DiscussionUtil.urlFor("search")
#TODO: This might be better done by setting discussion.current_page=0 and calling discussion.loadMorePages
# Mainly because this currently does not reset any pagination variables which could cause problems.
# This doesn't use pagination either.
DiscussionUtil.safeAjax
$elem: @$(".post-search-field")
data: { text: text }
url: url
type: "GET"
$loading: $
loadingCallback: =>
@$(".post-list").html('<li class="loading"><div class="loading-animation"></div></li>')
loadedCallback: =>
if callback
callback.apply @, [value]
success: (response, textStatus) =>
if textStatus == 'success'
# TODO: Augment existing collection?
@collection.reset(response.discussion_data)
Content.loadContentInfos(response.annotated_content_info)
@collection.current_page = response.page
@collection.pages = response.num_pages
# TODO: Perhaps reload user info so that votes can be updated.
# In the future we might not load all of a user's votes at once
# so this would probably be necessary anyway
@displayedCollection.reset(@collection.models) # Don't think this is necessary
clearSearch: (callback, value) ->
@$(".post-search-field").val("")
@searchFor("", callback, value)
setActiveItem: (event) ->
if event.which == 13
$(".browse-topic-drop-menu-wrapper .focused").click()
return
if event.which != 40 && event.which != 38
return
event.preventDefault()
items = $.makeArray($(".browse-topic-drop-menu-wrapper a").not(".hidden"))
index = items.indexOf($('.browse-topic-drop-menu-wrapper .focused')[0])
if event.which == 40
index = Math.min(index + 1, items.length - 1)
if event.which == 38
index = Math.max(index - 1, 0)
$(".browse-topic-drop-menu-wrapper .focused").removeClass("focused")
$(items[index]).addClass("focused")
itemTop = $(items[index]).parent().offset().top
scrollTop = $(".browse-topic-drop-menu").scrollTop()
itemFromTop = $(".browse-topic-drop-menu").offset().top - itemTop
scrollTarget = Math.min(scrollTop - itemFromTop, scrollTop)
scrollTarget = Math.max(scrollTop - itemFromTop - $(".browse-topic-drop-menu").height() + $(items[index]).height(), scrollTarget)
$(".browse-topic-drop-menu").scrollTop(scrollTarget)
retrieveFollowed: (event)=>
@mode = 'followed'
@retrieveFirstPage(event)

View File

@@ -0,0 +1,144 @@
if Backbone?
class @DiscussionThreadProfileView extends DiscussionContentView
expanded = false
events:
"click .discussion-vote": "toggleVote"
"click .action-follow": "toggleFollowing"
"click .expand-post": "expandPost"
"click .collapse-post": "collapsePost"
initLocal: ->
@$local = @$el.children(".discussion-article").children(".local")
@$delegateElement = @$local
initialize: ->
super()
@model.on "change", @updateModelDetails
render: ->
@template = DiscussionUtil.getTemplate("_profile_thread")
if not @model.has('abbreviatedBody')
@abbreviateBody()
params = $.extend(@model.toJSON(),{expanded: @expanded, permalink: @model.urlFor('retrieve')})
if not @model.get('anonymous')
params = $.extend(params, user:{username: @model.username, user_url: @model.user_url})
@$el.html(Mustache.render(@template, params))
@initLocal()
@delegateEvents()
@renderDogear()
@renderVoted()
@renderAttrs()
@$("span.timeago").timeago()
@convertMath()
if @expanded
@renderResponses()
@
renderDogear: ->
if window.user.following(@model)
@$(".dogear").addClass("is-followed")
renderVoted: =>
if window.user.voted(@model)
@$("[data-role=discussion-vote]").addClass("is-cast")
else
@$("[data-role=discussion-vote]").removeClass("is-cast")
updateModelDetails: =>
@renderVoted()
@$("[data-role=discussion-vote] .votes-count-number").html(@model.get("votes")["up_count"])
convertMath: ->
element = @$(".post-body")
element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.html()
MathJax.Hub.Queue ["Typeset", MathJax.Hub, element[0]]
renderResponses: ->
DiscussionUtil.safeAjax
url: "/courses/#{$$course_id}/discussion/forum/#{@model.get('commentable_id')}/threads/#{@model.id}"
$loading: @$el
success: (data, textStatus, xhr) =>
@$el.find(".loading").remove()
Content.loadContentInfos(data['annotated_content_info'])
comments = new Comments(data['content']['children'])
comments.each @renderResponse
@trigger "thread:responses:rendered"
renderResponse: (response) =>
response.set('thread', @model)
view = new ThreadResponseView(model: response)
view.on "comment:add", @addComment
view.render()
@$el.find(".responses").append(view.el)
addComment: =>
@model.comment()
toggleVote: (event) ->
event.preventDefault()
if window.user.voted(@model)
@unvote()
else
@vote()
toggleFollowing: (event) ->
$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"
vote: ->
window.user.vote(@model)
url = @model.urlFor("upvote")
DiscussionUtil.safeAjax
$elem: @$(".discussion-vote")
url: url
type: "POST"
success: (response, textStatus) =>
if textStatus == 'success'
@model.set(response)
unvote: ->
window.user.unvote(@model)
url = @model.urlFor("unvote")
DiscussionUtil.safeAjax
$elem: @$(".discussion-vote")
url: url
type: "POST"
success: (response, textStatus) =>
if textStatus == 'success'
@model.set(response)
edit: ->
abbreviateBody: ->
abbreviated = DiscussionUtil.abbreviateString @model.get('body'), 140
@model.set('abbreviatedBody', abbreviated)
expandPost: (event) ->
@expanded = true
@$el.addClass('expanded')
@$el.find('.post-body').html(@model.get('body'))
@convertMath()
@$el.find('.expand-post').css('display', 'none')
@$el.find('.collapse-post').css('display', 'block')
@$el.find('.post-extended-content').show()
if @$el.find('.loading').length
@renderResponses()
collapsePost: (event) ->
@expanded = false
@$el.removeClass('expanded')
@$el.find('.post-body').html(@model.get('abbreviatedBody'))
@convertMath()
@$el.find('.collapse-post').css('display', 'none')
@$el.find('.post-extended-content').hide()
@$el.find('.expand-post').css('display', 'block')

View File

@@ -0,0 +1,139 @@
if Backbone?
class @DiscussionThreadShowView extends DiscussionContentView
events:
"click .discussion-vote": "toggleVote"
"click .action-follow": "toggleFollowing"
"click .action-edit": "edit"
"click .action-delete": "delete"
"click .action-openclose": "toggleClosed"
$: (selector) ->
@$el.find(selector)
initialize: ->
super()
@model.on "change", @updateModelDetails
renderTemplate: ->
@template = _.template($("#thread-show-template").html())
@template(@model.toJSON())
render: ->
@$el.html(@renderTemplate())
@delegateEvents()
@renderDogear()
@renderVoted()
@renderAttrs()
@$("span.timeago").timeago()
@convertMath()
@highlight @$(".post-body")
@highlight @$("h1,h3")
@
renderDogear: ->
if window.user.following(@model)
@$(".dogear").addClass("is-followed")
renderVoted: =>
if window.user.voted(@model)
@$("[data-role=discussion-vote]").addClass("is-cast")
else
@$("[data-role=discussion-vote]").removeClass("is-cast")
updateModelDetails: =>
@renderVoted()
@$("[data-role=discussion-vote] .votes-count-number").html(@model.get("votes")["up_count"])
convertMath: ->
element = @$(".post-body")
element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.html()
MathJax.Hub.Queue ["Typeset", MathJax.Hub, element[0]]
toggleVote: (event) ->
event.preventDefault()
if window.user.voted(@model)
@unvote()
else
@vote()
toggleFollowing: (event) ->
$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"
vote: ->
window.user.vote(@model)
url = @model.urlFor("upvote")
DiscussionUtil.safeAjax
$elem: @$(".discussion-vote")
url: url
type: "POST"
success: (response, textStatus) =>
if textStatus == 'success'
@model.set(response, {silent: true})
unvote: ->
window.user.unvote(@model)
url = @model.urlFor("unvote")
DiscussionUtil.safeAjax
$elem: @$(".discussion-vote")
url: url
type: "POST"
success: (response, textStatus) =>
if textStatus == 'success'
@model.set(response, {silent: true})
edit: (event) ->
@trigger "thread:edit", event
delete: (event) ->
@trigger "thread:delete", event
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(/&lt;mark&gt;/g, "<mark>").replace(/&lt;\/mark&gt;/g, "</mark>"))
class @DiscussionThreadInlineShowView extends DiscussionThreadShowView
renderTemplate: ->
@template = DiscussionUtil.getTemplate('_inline_thread_show')
params = @model.toJSON()
if @model.get('username')?
params = $.extend(params, user:{username: @model.username, user_url: @model.user_url})
Mustache.render(@template, params)

View File

@@ -0,0 +1,215 @@
if Backbone?
class @DiscussionThreadView extends DiscussionContentView
events:
"click .discussion-submit-post": "submitComment"
# TODO tags
# Until we decide what to do w/ tags, removing them.
#"click .thread-tag": "tagSelected"
$: (selector) ->
@$el.find(selector)
initialize: ->
super()
@createShowView()
renderTemplate: ->
@template = _.template($("#thread-template").html())
@template(@model.toJSON())
render: ->
@$el.html(@renderTemplate())
@$el.find(".loading").hide()
@delegateEvents()
@renderShowView()
@renderAttrs()
# TODO tags
# Until we decide what to do w/ tags, removing them.
#@renderTags()
@$("span.timeago").timeago()
@makeWmdEditor "reply-body"
@renderResponses()
@
cleanup: ->
if @responsesRequest?
@responsesRequest.abort()
# TODO tags
# Until we decide what to do w/ tags, removing them.
#renderTags: ->
# # tags
# for tag in @model.get("tags")
# if !tags
# tags = $('<div class="thread-tags">')
# tags.append("<a href='#' class='thread-tag'>#{tag}</a>")
# @$(".post-body").after(tags)
# TODO tags
# Until we decide what to do w tags, removing them.
#tagSelected: (e) ->
# @trigger "tag:selected", $(e.target).html()
renderResponses: ->
setTimeout(=>
@$el.find(".loading").show()
, 200)
@responsesRequest = DiscussionUtil.safeAjax
url: DiscussionUtil.urlFor('retrieve_single_thread', @model.get('commentable_id'), @model.id)
success: (data, textStatus, xhr) =>
@responsesRequest = null
@$el.find(".loading").remove()
Content.loadContentInfos(data['annotated_content_info'])
comments = new Comments(data['content']['children'])
comments.each @renderResponse
@trigger "thread:responses:rendered"
renderResponse: (response) =>
response.set('thread', @model)
view = new ThreadResponseView(model: response)
view.on "comment:add", @addComment
view.on "comment:endorse", @endorseThread
view.render()
@$el.find(".responses").append(view.el)
view.afterInsert()
addComment: =>
@model.comment()
endorseThread: (endorsed) =>
is_endorsed = @$el.find(".is-endorsed").length
@model.set 'endorsed', is_endorsed
submitComment: (event) ->
event.preventDefault()
url = @model.urlFor('reply')
body = @getWmdContent("reply-body")
return if not body.trim().length
@setWmdContent("reply-body", "")
comment = new Comment(body: body, created_at: (new Date()).toISOString(), username: window.user.get("username"), votes: { up_count: 0 }, endorsed: false, user_id: window.user.get("id"))
comment.set('thread', @model.get('thread'))
@renderResponse(comment)
@model.addComment()
DiscussionUtil.safeAjax
$elem: $(event.target)
url: url
type: "POST"
dataType: 'json'
data:
body: body
success: (data, textStatus) =>
comment.updateInfo(data.annotated_content_info)
comment.set(data.content)
edit: (event) =>
@createEditView()
@renderEditView()
update: (event) =>
newTitle = @editView.$(".edit-post-title").val()
newBody = @editView.$(".edit-post-body textarea").val()
# TODO tags
# Until we decide what to do w/ tags, removing them.
#newTags = @editView.$(".edit-post-tags").val()
url = DiscussionUtil.urlFor('update_thread', @model.id)
DiscussionUtil.safeAjax
$elem: $(event.target)
$loading: $(event.target) if event
url: url
type: "POST"
dataType: 'json'
async: false # TODO when the rest of the stuff below is made to work properly..
data:
title: newTitle
body: newBody
# TODO tags
# Until we decide what to do w/ tags, removing them.
#tags: newTags
error: DiscussionUtil.formErrorHandler(@$(".edit-post-form-errors"))
success: (response, textStatus) =>
# TODO: Move this out of the callback, this makes it feel sluggish
@editView.$(".edit-post-title").val("").attr("prev-text", "")
@editView.$(".edit-post-body textarea").val("").attr("prev-text", "")
@editView.$(".edit-post-tags").val("")
@editView.$(".edit-post-tags").importTags("")
@editView.$(".wmd-preview p").html("")
@model.set
title: newTitle
body: newBody
tags: response.content.tags
@createShowView()
@renderShowView()
# TODO tags
# Until we decide what to do w/ tags, removing them.
#@renderTags()
createEditView: () ->
if @showView?
@showView.undelegateEvents()
@showView.$el.empty()
@showView = null
@editView = new DiscussionThreadEditView(model: @model)
@editView.bind "thread:update", @update
@editView.bind "thread:cancel_edit", @cancelEdit
renderSubView: (view) ->
view.setElement(@$('.thread-content-wrapper'))
view.render()
view.delegateEvents()
renderEditView: () ->
@renderSubView(@editView)
createShowView: () ->
if @editView?
@editView.undelegateEvents()
@editView.$el.empty()
@editView = null
@showView = new DiscussionThreadShowView(model: @model)
@showView.bind "thread:delete", @delete
@showView.bind "thread:edit", @edit
renderShowView: () ->
@renderSubView(@showView)
cancelEdit: (event) =>
event.preventDefault()
@createShowView()
@renderShowView()
delete: (event) =>
url = @model.urlFor('delete')
if not @model.can('can_delete')
return
if not confirm "Are you sure to delete thread \"#{@model.get('title')}\"?"
return
@model.remove()
@showView.undelegateEvents()
@undelegateEvents()
@$el.empty()
$elem = $(event.target)
DiscussionUtil.safeAjax
$elem: $elem
url: url
type: "POST"
success: (response, textStatus) =>

View File

@@ -0,0 +1,124 @@
if Backbone?
class @DiscussionThreadInlineView extends DiscussionThreadView
expanded = false
events:
"click .discussion-submit-post": "submitComment"
"click .expand-post": "expandPost"
"click .collapse-post": "collapsePost"
initialize: ->
super()
initLocal: ->
@$local = @$el.children(".discussion-article").children(".local")
if not @$local.length
@$local = @$el
@$delegateElement = @$local
render: ->
@template = DiscussionUtil.getTemplate("_inline_thread")
if not @model.has('abbreviatedBody')
@abbreviateBody()
params = @model.toJSON()
@$el.html(Mustache.render(@template, params))
#@createShowView()
@initLocal()
@delegateEvents()
@renderShowView()
@renderAttrs()
# TODO tags commenting out til we decide what to do with tags
#@renderTags()
@$("span.timeago").timeago()
@$el.find('.post-extended-content').hide()
if @expanded
@makeWmdEditor "reply-body"
@renderResponses()
@
createShowView: () ->
if @editView?
@editView.undelegateEvents()
@editView.$el.empty()
@editView = null
@showView = new DiscussionThreadInlineShowView(model: @model)
@showView.bind "thread:delete", @delete
@showView.bind "thread:edit", @edit
renderResponses: ->
#TODO: threadview
DiscussionUtil.safeAjax
url: "/courses/#{$$course_id}/discussion/forum/#{@model.get('commentable_id')}/threads/#{@model.id}"
$loading: @$el
success: (data, textStatus, xhr) =>
# @$el.find(".loading").remove()
Content.loadContentInfos(data['annotated_content_info'])
comments = new Comments(data['content']['children'])
comments.each @renderResponse
@trigger "thread:responses:rendered"
@$('.loading').remove()
toggleClosed: (event) ->
#TODO: showview
$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) ->
#TODO: showview
$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)
abbreviateBody: ->
abbreviated = DiscussionUtil.abbreviateString @model.get('body'), 140
@model.set('abbreviatedBody', abbreviated)
expandPost: (event) =>
@expanded = true
@$el.addClass('expanded')
@$el.find('.post-body').html(@model.get('body'))
@showView.convertMath()
@$el.find('.expand-post').css('display', 'none')
@$el.find('.collapse-post').css('display', 'block')
@$el.find('.post-extended-content').show()
@makeWmdEditor "reply-body"
@renderAttrs()
if @$el.find('.loading').length
@renderResponses()
collapsePost: (event) ->
@expanded = false
@$el.removeClass('expanded')
@$el.find('.post-body').html(@model.get('abbreviatedBody'))
@showView.convertMath()
@$el.find('.collapse-post').css('display', 'none')
@$el.find('.post-extended-content').hide()
@$el.find('.expand-post').css('display', 'block')
createEditView: () ->
super()
@editView.bind "thread:update", @expandPost
@editView.bind "thread:update", @abbreviateBody
@editView.bind "thread:cancel_edit", @expandPost

View File

@@ -0,0 +1,23 @@
if Backbone?
class @DiscussionUserProfileView extends Backbone.View
# events:
# "":""
initialize: (options) ->
@renderThreads @$el, @collection
renderThreads: ($elem, threads) =>
#Content.loadContentInfos(response.annotated_content_info)
@discussion = new Discussion()
@discussion.reset(threads, {silent: false})
$discussion = $(Mustache.render $("script#_user_profile").html(), {'threads':threads})
$elem.append($discussion)
@threadviews = @discussion.map (thread) ->
new DiscussionThreadProfileView el: @$("article#thread_#{thread.id}"), model: thread
_.each @threadviews, (dtv) -> dtv.render()
addThread: (thread, collection, options) =>
# TODO: When doing pagination, this will need to repaginate. Perhaps just reload page 1?
article = $("<article class='discussion-thread' id='thread_#{thread.id}'></article>")
@$('section.discussion > .threads').prepend(article)
threadView = new DiscussionThreadInlineView el: article, model: thread
threadView.render()
@threadviews.unshift threadView

View File

@@ -0,0 +1,68 @@
if Backbone?
class @NewPostInlineView extends Backbone.View
initialize: () ->
@topicId = @$(".topic").first().data("discussion-id")
@maxNameWidth = 100
DiscussionUtil.makeWmdEditor @$el, $.proxy(@$, @), "new-post-body"
# TODO tags: commenting out til we know what to do with them
#@$(".new-post-tags").tagsInput DiscussionUtil.tagsInputOptions()
events:
"submit .new-post-form": "createPost"
# Because we want the behavior that when the body is clicked the menu is
# closed, we need to ignore clicks in the search field and stop propagation.
# Without this, clicking the search field would also close the menu.
ignoreClick: (event) ->
event.stopPropagation()
createPost: (event) ->
event.preventDefault()
title = @$(".new-post-title").val()
body = @$(".new-post-body").find(".wmd-input").val()
# TODO tags: commenting out til we know what to do with them
#tags = @$(".new-post-tags").val()
anonymous = false || @$("input.discussion-anonymous").is(":checked")
anonymous_to_peers = false || @$("input.discussion-anonymous-to-peers").is(":checked")
follow = false || @$("input.discussion-follow").is(":checked")
url = DiscussionUtil.urlFor('create_thread', @topicId)
DiscussionUtil.safeAjax
$elem: $(event.target)
$loading: $(event.target) if event
url: url
type: "POST"
dataType: 'json'
async: false # TODO when the rest of the stuff below is made to work properly..
data:
title: title
body: body
# TODO tags: commenting out til we know what to do with them
#tags: tags
anonymous: anonymous
anonymous_to_peers: anonymous_to_peers
auto_subscribe: follow
error: DiscussionUtil.formErrorHandler(@$(".new-post-form-errors"))
success: (response, textStatus) =>
# TODO: Move this out of the callback, this makes it feel sluggish
thread = new Thread response['content']
DiscussionUtil.clearFormErrors(@$(".new-post-form-errors"))
@$el.hide()
@$(".new-post-title").val("").attr("prev-text", "")
@$(".new-post-body textarea").val("").attr("prev-text", "")
# TODO tags, commenting out til we know what to do with them
#@$(".new-post-tags").val("")
#@$(".new-post-tags").importTags("")
@collection.add thread

View File

@@ -0,0 +1,177 @@
if Backbone?
class @NewPostView extends Backbone.View
initialize: () ->
@dropdownButton = @$(".topic_dropdown_button")
@topicMenu = @$(".topic_menu_wrapper")
@menuOpen = @dropdownButton.hasClass('dropped')
@topicId = @$(".topic").first().data("discussion_id")
@topicText = @getFullTopicName(@$(".topic").first())
@maxNameWidth = 100
@setSelectedTopic()
DiscussionUtil.makeWmdEditor @$el, $.proxy(@$, @), "new-post-body"
@$(".new-post-tags").tagsInput DiscussionUtil.tagsInputOptions()
events:
"submit .new-post-form": "createPost"
"click .topic_dropdown_button": "toggleTopicDropdown"
"click .topic_menu_wrapper": "setTopic"
"click .topic_menu_search": "ignoreClick"
"keyup .form-topic-drop-search-input": DiscussionFilter.filterDrop
# Because we want the behavior that when the body is clicked the menu is
# closed, we need to ignore clicks in the search field and stop propagation.
# Without this, clicking the search field would also close the menu.
ignoreClick: (event) ->
event.stopPropagation()
toggleTopicDropdown: (event) ->
event.stopPropagation()
if @menuOpen
@hideTopicDropdown()
else
@showTopicDropdown()
showTopicDropdown: () ->
@menuOpen = true
@dropdownButton.addClass('dropped')
@topicMenu.show()
$(".form-topic-drop-search-input").focus()
$("body").bind "keydown", @setActiveItem
$("body").bind "click", @hideTopicDropdown
# Set here because 1) the window might get resized and things could
# change and 2) can't set in initialize because the button is hidden
@maxNameWidth = @dropdownButton.width() * 0.9
# Need a fat arrow because hideTopicDropdown is passed as a callback to bind
hideTopicDropdown: () =>
@menuOpen = false
@dropdownButton.removeClass('dropped')
@topicMenu.hide()
$("body").unbind "keydown", @setActiveItem
$("body").unbind "click", @hideTopicDropdown
setTopic: (event) ->
$target = $(event.target)
if $target.data('discussion_id')
@topicText = $target.html()
@topicText = @getFullTopicName($target)
@topicId = $target.data('discussion_id')
@setSelectedTopic()
setSelectedTopic: ->
@dropdownButton.html(@fitName(@topicText) + ' <span class="drop-arrow">▾</span>')
getFullTopicName: (topicElement) ->
name = topicElement.html()
topicElement.parents('ul').not('.topic_menu').each ->
name = $(this).siblings('a').html() + ' / ' + name
return name
getNameWidth: (name) ->
test = $("<div>")
test.css
"font-size": @dropdownButton.css('font-size')
opacity: 0
position: 'absolute'
left: -1000
top: -1000
$("body").append(test)
test.html(name)
width = test.width()
test.remove()
return width
fitName: (name) ->
width = @getNameWidth(name)
if width < @maxNameWidth
return name
path = (x.replace /^\s+|\s+$/g, "" for x in name.split("/"))
while path.length > 1
path.shift()
partialName = "... / " + path.join(" / ")
if @getNameWidth(partialName) < @maxNameWidth
return partialName
rawName = path[0]
name = "... / " + rawName
while @getNameWidth(name) > @maxNameWidth
rawName = rawName[0...rawName.length-1]
name = "... / " + rawName + " ..."
return name
createPost: (event) ->
event.preventDefault()
title = @$(".new-post-title").val()
body = @$(".new-post-body").find(".wmd-input").val()
tags = @$(".new-post-tags").val()
anonymous = false || @$("input.discussion-anonymous").is(":checked")
anonymous_to_peers = false || @$("input.discussion-anonymous-to-peers").is(":checked")
follow = false || @$("input.discussion-follow").is(":checked")
url = DiscussionUtil.urlFor('create_thread', @topicId)
DiscussionUtil.safeAjax
$elem: $(event.target)
$loading: $(event.target) if event
url: url
type: "POST"
dataType: 'json'
async: false # TODO when the rest of the stuff below is made to work properly..
data:
title: title
body: body
tags: tags
anonymous: anonymous
anonymous_to_peers: anonymous_to_peers
auto_subscribe: follow
error: DiscussionUtil.formErrorHandler(@$(".new-post-form-errors"))
success: (response, textStatus) =>
# TODO: Move this out of the callback, this makes it feel sluggish
thread = new Thread response['content']
DiscussionUtil.clearFormErrors(@$(".new-post-form-errors"))
@$el.hide()
@$(".new-post-title").val("").attr("prev-text", "")
@$(".new-post-body textarea").val("").attr("prev-text", "")
@$(".new-post-tags").val("")
@$(".new-post-tags").importTags("")
@$(".wmd-preview p").html("")
@collection.add thread
setActiveItem: (event) ->
if event.which == 13
$(".topic_menu_wrapper .focused").click()
return
if event.which != 40 && event.which != 38
return
event.preventDefault()
items = $.makeArray($(".topic_menu_wrapper a").not(".hidden"))
index = items.indexOf($('.topic_menu_wrapper .focused')[0])
if event.which == 40
index = Math.min(index + 1, items.length - 1)
if event.which == 38
index = Math.max(index - 1, 0)
$(".topic_menu_wrapper .focused").removeClass("focused")
$(items[index]).addClass("focused")
itemTop = $(items[index]).parent().offset().top
scrollTop = $(".topic_menu").scrollTop()
itemFromTop = $(".topic_menu").offset().top - itemTop
scrollTarget = Math.min(scrollTop - itemFromTop, scrollTop)
scrollTarget = Math.max(scrollTop - itemFromTop - $(".topic_menu").height() + $(items[index]).height() + 20, scrollTarget)
$(".topic_menu").scrollTop(scrollTarget)

View File

@@ -0,0 +1,34 @@
if Backbone?
class @ResponseCommentShowView extends DiscussionContentView
tagName: "li"
render: ->
@template = _.template($("#response-comment-show-template").html())
params = @model.toJSON()
@$el.html(@template(params))
@initLocal()
@delegateEvents()
@renderAttrs()
@markAsStaff()
@$el.find(".timeago").timeago()
@convertMath()
@addReplyLink()
@
addReplyLink: () ->
if @model.hasOwnProperty('parent')
name = @model.parent.get('username') ? "anonymous"
html = "<a href='#comment_#{@model.parent.id}'>@#{name}</a>: "
p = @$('.response-body p:first')
p.prepend(html)
convertMath: ->
body = @$el.find(".response-body")
body.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight body.html()
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">staff</span>')

View File

@@ -0,0 +1,31 @@
if Backbone?
class @ResponseCommentView extends DiscussionContentView
tagName: "li"
$: (selector) ->
@$el.find(selector)
initialize: ->
super()
@createShowView()
render: ->
@renderShowView()
@
createShowView: () ->
if @editView?
@editView.undelegateEvents()
@editView.$el.empty()
@editView = null
@showView = new ResponseCommentShowView(model: @model)
renderSubView: (view) ->
view.setElement(@$el)
view.render()
view.delegateEvents()
renderShowView: () ->
@renderSubView(@showView)

View File

@@ -0,0 +1,25 @@
if Backbone?
class @ThreadResponseEditView extends Backbone.View
events:
"click .post-update": "update"
"click .post-cancel": "cancel_edit"
$: (selector) ->
@$el.find(selector)
initialize: ->
super()
render: ->
@template = _.template($("#thread-response-edit-template").html())
@$el.html(@template(@model.toJSON()))
@delegateEvents()
DiscussionUtil.makeWmdEditor @$el, $.proxy(@$, @), "edit-post-body"
@
update: (event) ->
@trigger "response:update", event
cancel_edit: (event) ->
@trigger "response:cancel_edit", event

View File

@@ -0,0 +1,91 @@
if Backbone?
class @ThreadResponseShowView extends DiscussionContentView
events:
"click .vote-btn": "toggleVote"
"click .action-endorse": "toggleEndorse"
"click .action-delete": "delete"
"click .action-edit": "edit"
$: (selector) ->
@$el.find(selector)
initialize: ->
super()
@model.on "change", @updateModelDetails
renderTemplate: ->
@template = _.template($("#thread-response-show-template").html())
@template(@model.toJSON())
render: ->
@$el.html(@renderTemplate())
@delegateEvents()
if window.user.voted(@model)
@$(".vote-btn").addClass("is-cast")
@renderAttrs()
@$el.find(".posted-details").timeago()
@convertMath()
@markAsStaff()
@
convertMath: ->
element = @$(".response-body")
element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.html()
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">staff</div>')
toggleVote: (event) ->
event.preventDefault()
@$(".vote-btn").toggleClass("is-cast")
if @$(".vote-btn").hasClass("is-cast")
@vote()
else
@unvote()
vote: ->
url = @model.urlFor("upvote")
@$(".votes-count-number").html(parseInt(@$(".votes-count-number").html()) + 1)
DiscussionUtil.safeAjax
$elem: @$(".discussion-vote")
url: url
type: "POST"
success: (response, textStatus) =>
if textStatus == 'success'
@model.set(response)
unvote: ->
url = @model.urlFor("unvote")
@$(".votes-count-number").html(parseInt(@$(".votes-count-number").html()) - 1)
DiscussionUtil.safeAjax
$elem: @$(".discussion-vote")
url: url
type: "POST"
success: (response, textStatus) =>
if textStatus == 'success'
@model.set(response)
edit: (event) ->
@trigger "response:edit", event
delete: (event) ->
@trigger "response:delete", event
toggleEndorse: (event) ->
event.preventDefault()
if not @model.can('can_endorse')
return
$elem = $(event.target)
url = @model.urlFor('endorse')
endorsed = @model.get('endorsed')
data = { endorsed: not endorsed }
@model.set('endorsed', not endorsed)
@trigger "comment:endorse", not endorsed
DiscussionUtil.safeAjax
$elem: $elem
url: url
data: data
type: "POST"

View File

@@ -0,0 +1,188 @@
if Backbone?
class @ThreadResponseView extends DiscussionContentView
tagName: "li"
events:
"click .discussion-submit-comment": "submitComment"
"focus .wmd-input": "showEditorChrome"
$: (selector) ->
@$el.find(selector)
initialize: ->
@createShowView()
renderTemplate: ->
@template = _.template($("#thread-response-template").html())
templateData = @model.toJSON()
templateData.wmdId = @model.id ? (new Date()).getTime()
@template(templateData)
render: ->
@$el.html(@renderTemplate())
@delegateEvents()
@renderShowView()
@renderAttrs()
@renderComments()
@
afterInsert: ->
@makeWmdEditor "comment-body"
@hideEditorChrome()
hideEditorChrome: ->
@$('.wmd-button-row').hide()
@$('.wmd-preview').hide()
@$('.wmd-input').css({
height: '35px',
padding: '5px'
})
@$('.comment-post-control').hide()
showEditorChrome: ->
@$('.wmd-button-row').show()
@$('.wmd-preview').show()
@$('.comment-post-control').show()
@$('.wmd-input').css({
height: '125px',
padding: '10px'
})
renderComments: ->
comments = new Comments()
comments.comparator = (comment) ->
comment.get('created_at')
collectComments = (comment) ->
comments.add(comment)
children = new Comments(comment.get('children'))
children.each (child) ->
child.parent = comment
collectComments(child)
@model.get('comments').each collectComments
comments.each (comment) => @renderComment(comment, false, null)
renderComment: (comment) =>
comment.set('thread', @model.get('thread'))
view = new ResponseCommentView(model: comment)
view.render()
@$el.find(".comments .new-comment").before(view.el)
view
submitComment: (event) ->
event.preventDefault()
url = @model.urlFor('reply')
body = @getWmdContent("comment-body")
return if not body.trim().length
@setWmdContent("comment-body", "")
comment = new Comment(body: body, created_at: (new Date()).toISOString(), username: window.user.get("username"), user_id: window.user.get("id"), id:"unsaved")
view = @renderComment(comment)
@hideEditorChrome()
@trigger "comment:add", comment
DiscussionUtil.safeAjax
$elem: $(event.target)
url: url
type: "POST"
dataType: 'json'
data:
body: body
success: (response, textStatus) ->
comment.set(response.content)
view.render() # This is just to update the id for the most part, but might be useful in general
delete: (event) =>
event.preventDefault()
if not @model.can('can_delete')
return
if not confirm "Are you sure to delete this response? "
return
url = @model.urlFor('delete')
@model.remove()
@$el.remove()
$elem = $(event.target)
DiscussionUtil.safeAjax
$elem: $elem
url: url
type: "POST"
success: (response, textStatus) =>
createEditView: () ->
if @showView?
@showView.undelegateEvents()
@showView.$el.empty()
@showView = null
@editView = new ThreadResponseEditView(model: @model)
@editView.bind "response:update", @update
@editView.bind "response:cancel_edit", @cancelEdit
renderSubView: (view) ->
view.setElement(@$('.discussion-response'))
view.render()
view.delegateEvents()
renderEditView: () ->
@renderSubView(@editView)
hideCommentForm: () ->
@$('.comment-form').closest('li').hide()
showCommentForm: () ->
@$('.comment-form').closest('li').show()
createShowView: () ->
if @editView?
@editView.undelegateEvents()
@editView.$el.empty()
@editView = null
@showView = new ThreadResponseShowView(model: @model)
@showView.bind "response:delete", @delete
@showView.bind "response:edit", @edit
renderShowView: () ->
@renderSubView(@showView)
cancelEdit: (event) =>
event.preventDefault()
@createShowView()
@renderShowView()
@showCommentForm()
edit: (event) =>
@createEditView()
@renderEditView()
@hideCommentForm()
update: (event) =>
newBody = @editView.$(".edit-post-body textarea").val()
url = DiscussionUtil.urlFor('update_comment', @model.id)
DiscussionUtil.safeAjax
$elem: $(event.target)
$loading: $(event.target) if event
url: url
type: "POST"
dataType: 'json'
async: false # TODO when the rest of the stuff below is made to work properly..
data:
body: newBody
error: DiscussionUtil.formErrorHandler(@$(".edit-post-form-errors"))
success: (response, textStatus) =>
# TODO: Move this out of the callback, this makes it feel sluggish
@editView.$(".edit-post-body textarea").val("").attr("prev-text", "")
@editView.$(".wmd-preview p").html("")
@model.set
body: newBody
@createShowView()
@renderShowView()
@showCommentForm()