Make discussion js work for cms as well as lms so that inline discussion xmodules load
This commit is contained in:
18
common/lib/rooted_paths.py
Normal file
18
common/lib/rooted_paths.py
Normal 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]
|
||||
@@ -17,7 +17,7 @@ class @Video
|
||||
@embed()
|
||||
else
|
||||
window.onYouTubePlayerAPIReady = =>
|
||||
$('.course-content .video').each ->
|
||||
@el.each ->
|
||||
$(this).data('video').embed()
|
||||
|
||||
youtubeId: (speed)->
|
||||
|
||||
107
common/lib/xmodule/xmodule/static_content.py
Normal file
107
common/lib/xmodule/xmodule/static_content.py
Normal 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
|
||||
163
common/static/coffee/src/discussion/content.coffee
Normal file
163
common/static/coffee/src/discussion/content.coffee
Normal 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)
|
||||
82
common/static/coffee/src/discussion/discussion.coffee
Normal file
82
common/static/coffee/src/discussion/discussion.coffee
Normal 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()
|
||||
28
common/static/coffee/src/discussion/discussion_filter.coffee
Normal file
28
common/static/coffee/src/discussion/discussion_filter.coffee
Normal 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');
|
||||
@@ -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))
|
||||
57
common/static/coffee/src/discussion/discussion_router.coffee
Normal file
57
common/static/coffee/src/discussion/discussion_router.coffee
Normal 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)
|
||||
29
common/static/coffee/src/discussion/main.coffee
Normal file
29
common/static/coffee/src/discussion/main.coffee
Normal 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)
|
||||
@@ -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()
|
||||
47
common/static/coffee/src/discussion/tooltip_manager.coffee
Normal file
47
common/static/coffee/src/discussion/tooltip_manager.coffee
Normal 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)
|
||||
30
common/static/coffee/src/discussion/user_profile.coffee
Normal file
30
common/static/coffee/src/discussion/user_profile.coffee
Normal 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"
|
||||
297
common/static/coffee/src/discussion/utils.coffee
Normal file
297
common/static/coffee/src/discussion/utils.coffee
Normal 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(/\<\;highlight\>\;/g, "<span class='search-highlight'>")
|
||||
.replace(/\<\;\/highlight\>\;/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(/^\>\;/gm, ">")
|
||||
converter = Markdown.getMathCompatibleConverter()
|
||||
text = @unescapeHighlightTag @stripLatexHighlight converter.makeHtml text
|
||||
return text.replace(/^>/gm,">")
|
||||
|
||||
@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) + '...'
|
||||
|
||||
@@ -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, @)
|
||||
@@ -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
|
||||
@@ -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(/<mark>/g, "<mark>").replace(/<\/mark>/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)
|
||||
@@ -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')
|
||||
@@ -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(/<mark>/g, "<mark>").replace(/<\/mark>/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)
|
||||
@@ -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) =>
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
177
common/static/coffee/src/discussion/views/new_post_view.coffee
Normal file
177
common/static/coffee/src/discussion/views/new_post_view.coffee
Normal 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)
|
||||
@@ -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>')
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user