Commit be04740e by Greg Price

Merge pull request #4291 from edx/forum-sidebar-improvements

Forum sidebar improvements
parents 220a028b 6d87443a
describe 'All Content', -> describe 'All Content', ->
beforeEach -> beforeEach ->
# TODO: figure out a better way of handling this DiscussionSpecHelper.setUpGlobals()
# It is set up in main.coffee DiscussionApp.start
window.$$course_id = 'edX/999/test'
window.user = new DiscussionUser {id: '567'}
describe 'Content', -> describe 'Content', ->
beforeEach -> beforeEach ->
......
class @DiscussionSpecHelper
# This is sad. We should avoid dependence on global vars.
@setUpGlobals = ->
DiscussionUtil.loadRoles({"Moderator": [], "Administrator": [], "Community TA": []})
window.$$course_id = "edX/999/test"
window.user = new DiscussionUser({id: "567", upvoted_ids: []})
describe "DiscussionContentView", -> describe "DiscussionContentView", ->
beforeEach -> beforeEach ->
DiscussionSpecHelper.setUpGlobals()
setFixtures( setFixtures(
""" """
<div class="discussion-post"> <div class="discussion-post">
...@@ -36,7 +36,6 @@ describe "DiscussionContentView", -> ...@@ -36,7 +36,6 @@ describe "DiscussionContentView", ->
@thread = new Thread(@threadData) @thread = new Thread(@threadData)
@view = new DiscussionContentView({ model: @thread }) @view = new DiscussionContentView({ model: @thread })
@view.setElement($('.discussion-post')) @view.setElement($('.discussion-post'))
window.user = new DiscussionUser({id: '567', upvoted_ids: []})
it 'defines the tag', -> it 'defines the tag', ->
expect($('#jasmine-fixtures')).toExist expect($('#jasmine-fixtures')).toExist
......
describe "DiscussionThreadListView", -> describe "DiscussionThreadListView", ->
beforeEach -> beforeEach ->
DiscussionSpecHelper.setUpGlobals()
setFixtures """ setFixtures """
<script type="text/template" id="thread-list-item-template"> <script type="text/template" id="thread-list-item-template">
<a href="<%- id %>" data-id="<%- id %>"> <li data-id="<%- id %>" class="forum-nav-thread<% if (typeof(read) != "undefined" && !read) { %> is-unread<% } %>">
<span class="title"><%- title %></span> <a href="#" class="forum-nav-thread-link">
<span class="comments-count"> <div class="forum-nav-thread-wrapper-1">
<span class="forum-nav-thread-title"><%- title %></span>
<% <%
var fmt; var labels = "";
var data = { if (pinned) {
'span_sr_open': '<span class="sr">', labels += '<li class="forum-nav-thread-label-pinned"><i class="icon icon-pushpin"></i>Pinned</li> ';
'span_close': '</span>', }
'unread_comments_count': unread_comments_count, if (typeof(subscribed) != "undefined" && subscribed) {
'comments_count': comments_count labels += '<li class="forum-nav-thread-label-following"><i class="icon icon-star"></i>Following</li> ';
}; }
if (unread_comments_count > 0) { if (staff_authored) {
fmt = '%(comments_count)s %(span_sr_open)scomments (%(unread_comments_count)s unread comments)%(span_close)s'; labels += '<li class="forum-nav-thread-label-staff"><i class="icon icon-user"></i>By: Staff</li> ';
} else { }
fmt = '%(comments_count)s %(span_sr_open)scomments %(span_close)s'; if (community_ta_authored) {
} labels += '<li class="forum-nav-thread-label-community-ta"><i class="icon icon-user"></i>By: Community TA</li> ';
print(interpolate(fmt, data, true)); }
%> if (labels != "") {
</span> print('<ul class="forum-nav-thread-labels">' + labels + '</ul>');
}
<span class="votes-count">+<%= %>
interpolate( </div><div class="forum-nav-thread-wrapper-2">
'%(votes_up_count)s%(span_sr_open)s votes %(span_close)s', <% if (endorsed) { %>
{'span_sr_open': '<span class="sr">', 'span_close': '</span>', 'votes_up_count': votes['up_count']}, <span class="forum-nav-thread-endorsed"><i class="icon icon-ok"></i><span class="sr">Endorsed response</span></span>
true <% } %>
) <span class="forum-nav-thread-votes-count">+<%=
%></span> interpolate(
</a> '%(votes_up_count)s%(span_sr_open)s votes %(span_close)s',
{'span_sr_open': '<span class="sr">', 'span_close': '</span>', 'votes_up_count': votes['up_count']},
true
)
%></span>
<span class="forum-nav-thread-comments-count <% if (unread_comments_count > 0) { %>is-unread<% } %>">
<%
var fmt;
var data = {
'span_sr_open': '<span class="sr">',
'span_close': '</span>',
'unread_comments_count': unread_comments_count,
'comments_count': comments_count
};
if (unread_comments_count > 0) {
fmt = '%(comments_count)s %(span_sr_open)scomments (%(unread_comments_count)s unread comments)%(span_close)s';
} else {
fmt = '%(comments_count)s %(span_sr_open)scomments %(span_close)s';
}
print(interpolate(fmt, data, true));
%>
</span>
</div>
</a>
</li>
</script> </script>
<script type="text/template" id="thread-list-template"> <script type="text/template" id="thread-list-template">
<div class="browse-search"> <div class="forum-nav-header">
<div class="home"></div> <a href="#" class="forum-nav-browse" aria-haspopup="true">
<div class="browse is-open"></div> <i class="icon icon-reorder"></i>
<div class="search"> <span class="sr">Discussion topics; current selection is: </span>
<form class="post-search"> <span class="forum-nav-browse-current">All Discussions</span>
<label class="sr" for="search-discussions">Search</label>
<input type="text" id="search-discussions" placeholder="Search all discussions" class="post-search-field"> </a>
</form> <form class="forum-nav-search">
</div> <label>
<span class="sr">Search</span>
<input class="forum-nav-search-input" type="text" placeholder="Search all posts">
</label>
</form>
</div> </div>
<div class="sort-bar"> <div class="forum-nav-browse-menu-wrapper" style="display: none">
<span class="sort-label" id="sort-label">Sort by:</span> <form class="forum-nav-browse-filter">
<ul role="radiogroup" aria-labelledby="sort-label"> <label>
<li><a href="#" role="radio" aria-checked="false" data-sort="date">date</a></li> <span class="sr">Filter Topics</span>
<li><a href="#" role="radio" aria-checked="false" data-sort="votes">votes</a></li> <input type="text" class="forum-nav-browse-filter-input" placeholder="filter topics">
<li><a href="#" role="radio" aria-checked="false" data-sort="comments">comments</a></li> </label>
</ul> </form>
<ul class="forum-nav-browse-menu">
<li class="forum-nav-browse-menu-item forum-nav-browse-menu-all">
<a href="#" class="forum-nav-browse-title">All Discussions</a>
</li>
<li class="forum-nav-browse-menu-item forum-nav-browse-menu-flagged">
<a href="#" class="forum-nav-browse-title"><i class="icon icon-flag"></i>Flagged Discussions</a>
</li>
<li class="forum-nav-browse-menu-item forum-nav-browse-menu-following">
<a href="#" class="forum-nav-browse-title"><i class="icon icon-star"></i>Posts I'm Following</a>
</li>
<li class="forum-nav-browse-menu-item">
<a href="#" class="forum-nav-browse-title">Parent</a>
<ul class="forum-nav-browse-submenu">
<li class="forum-nav-browse-menu-item">
<a href="#" class="forum-nav-browse-title">Target</a>
<ul class="forum-nav-browse-submenu">
<li
class="forum-nav-browse-menu-item"
data-discussion-id='{"sort_key": null, "id": "child"}'
data-cohorted="false"
>
<a href="#" class="forum-nav-browse-title">Child</a>
</li>
</ul>
<li
class="forum-nav-browse-menu-item"
data-discussion-id='{"sort_key": null, "id": "sibling"}'
data-cohorted="false"
>
<a href="#" class="forum-nav-browse-title">Sibling</a>
</li>
</ul>
</li>
<li
class="forum-nav-browse-menu-item"
data-discussion-id='{"sort_key": null, "id": "other"}'
data-cohorted="false"
>
<a href="#" class="forum-nav-browse-title">Other Category</a>
</li>
</ul>
</div> </div>
<div class="search-alerts"></div> <div class="forum-nav-thread-list-wrapper">
<div class="post-list-wrapper"> <div class="forum-nav-refine-bar">
<ul class="post-list"></ul> <span class="forum-nav-sort">
<select class="forum-nav-sort-control">
<option value="date">by recent activity</option>
<option value="comments">by most activity</option>
<option value="votes">by most votes</option>
</select>
</span>
</div>
</div> </div>
<div class="search-alerts"></div>
<ul class="forum-nav-thread-list"></ul>
</script> </script>
<script aria-hidden="true" type="text/template" id="search-alert-template"> <script aria-hidden="true" type="text/template" id="search-alert-template">
<div class="search-alert" id="search-alert-<%- cid %>"> <div class="search-alert" id="search-alert-<%- cid %>">
...@@ -71,12 +150,28 @@ describe "DiscussionThreadListView", -> ...@@ -71,12 +150,28 @@ describe "DiscussionThreadListView", ->
<div class="sidebar"></div> <div class="sidebar"></div>
""" """
@threads = [ @threads = [
{id: "1", title: "Thread1", body: "dummy body", votes: {up_count: '20'}, unread_comments_count:0, comments_count:1, created_at: '2013-04-03T20:08:39Z',}, makeThreadWithProps({
{id: "2", title: "Thread2", body: "dummy body", votes: {up_count: '42'}, unread_comments_count:0, comments_count:2, created_at: '2013-04-03T20:07:39Z',}, id: "1",
{id: "3", title: "Thread3", body: "dummy body", votes: {up_count: '12'}, unread_comments_count:0, comments_count:3, created_at: '2013-04-03T20:06:39Z',}, title: "Thread1",
votes: {up_count: '20'},
comments_count: 1,
created_at: '2013-04-03T20:08:39Z',
}),
makeThreadWithProps({
id: "2",
title: "Thread2",
votes: {up_count: '42'},
comments_count: 2,
created_at: '2013-04-03T20:07:39Z',
}),
makeThreadWithProps({
id: "3",
title: "Thread3",
votes: {up_count: '12'},
comments_count: 3,
created_at: '2013-04-03T20:06:39Z',
}),
] ]
window.$$course_id = "TestOrg/TestCourse/TestRun"
window.user = new DiscussionUser({id: "567", upvoted_ids: []})
spyOn($, "ajax") spyOn($, "ajax")
...@@ -84,6 +179,21 @@ describe "DiscussionThreadListView", -> ...@@ -84,6 +179,21 @@ describe "DiscussionThreadListView", ->
@view = new DiscussionThreadListView({collection: @discussion, el: $(".sidebar")}) @view = new DiscussionThreadListView({collection: @discussion, el: $(".sidebar")})
@view.render() @view.render()
makeThreadWithProps = (props) ->
# Minimal set of properties necessary for rendering
thread = {
id: "dummy_id",
pinned: false,
endorsed: false,
votes: {up_count: '0'},
unread_comments_count: 0,
comments_count: 0,
}
$.extend(thread, props)
renderSingleThreadWithProps = (props) ->
makeView(new Discussion([new Thread(makeThreadWithProps(props))])).render()
makeView = (discussion) -> makeView = (discussion) ->
return new DiscussionThreadListView( return new DiscussionThreadListView(
el: $(".sidebar"), el: $(".sidebar"),
...@@ -91,18 +201,20 @@ describe "DiscussionThreadListView", -> ...@@ -91,18 +201,20 @@ describe "DiscussionThreadListView", ->
) )
checkThreadsOrdering = (view, sort_order, type) -> checkThreadsOrdering = (view, sort_order, type) ->
expect(view.$el.find(".post-list .list-item").children().length).toEqual(3) expect(view.$el.find(".forum-nav-thread").children().length).toEqual(3)
expect(view.$el.find(".post-list .list-item:nth-child(1) .title").text()).toEqual(sort_order[0]) expect(view.$el.find(".forum-nav-thread:nth-child(1) .forum-nav-thread-title").text()).toEqual(sort_order[0])
expect(view.$el.find(".post-list .list-item:nth-child(2) .title").text()).toEqual(sort_order[1]) expect(view.$el.find(".forum-nav-thread:nth-child(2) .forum-nav-thread-title").text()).toEqual(sort_order[1])
expect(view.$el.find(".post-list .list-item:nth-child(3) .title").text()).toEqual(sort_order[2]) expect(view.$el.find(".forum-nav-thread:nth-child(3) .forum-nav-thread-title").text()).toEqual(sort_order[2])
expect(view.$el.find(".sort-bar a.active").text()).toEqual(type) expect(view.$el.find(".forum-nav-sort-control").val()).toEqual(type)
describe "thread rendering should be correct", -> describe "thread rendering should be correct", ->
checkRender = (threads, type, sort_order) -> checkRender = (threads, type, sort_order) ->
discussion = new Discussion(threads, {pages: 1, sort: type}) discussion = new Discussion(_.map(threads, (thread) -> new Thread(thread)), {pages: 1, sort: type})
view = makeView(discussion) view = makeView(discussion)
view.render() view.render()
checkThreadsOrdering(view, sort_order, type) checkThreadsOrdering(view, sort_order, type)
expect(view.$el.find(".forum-nav-thread-comments-count:visible").length).toEqual(if type == "votes" then 0 else 3)
expect(view.$el.find(".forum-nav-thread-votes-count:visible").length).toEqual(if type == "votes" then 3 else 0)
it "with sort preference date", -> it "with sort preference date", ->
checkRender(@threads, "date", [ "Thread1", "Thread2", "Thread3"]) checkRender(@threads, "date", [ "Thread1", "Thread2", "Thread3"])
...@@ -113,12 +225,13 @@ describe "DiscussionThreadListView", -> ...@@ -113,12 +225,13 @@ describe "DiscussionThreadListView", ->
it "with sort preference comments", -> it "with sort preference comments", ->
checkRender(@threads, "comments", [ "Thread3", "Thread2", "Thread1"]) checkRender(@threads, "comments", [ "Thread3", "Thread2", "Thread1"])
describe "Sort click should be correct", -> describe "Sort change should be correct", ->
changeSorting = (threads, selected_type, new_type, sort_order) -> changeSorting = (threads, selected_type, new_type, sort_order) ->
discussion = new Discussion(threads, {pages: 1, sort: selected_type}) discussion = new Discussion(_.map(threads, (thread) -> new Thread(thread)), {pages: 1, sort: selected_type})
view = makeView(discussion) view = makeView(discussion)
view.render() view.render()
expect(view.$el.find(".sort-bar a.active").text()).toEqual(selected_type) sortControl = view.$el.find(".forum-nav-sort-control")
expect(sortControl.val()).toEqual(selected_type)
sorted_threads = [] sorted_threads = []
if new_type == 'date' if new_type == 'date'
sorted_threads = [threads[0], threads[1], threads[2]] sorted_threads = [threads[0], threads[1], threads[2]]
...@@ -132,9 +245,8 @@ describe "DiscussionThreadListView", -> ...@@ -132,9 +245,8 @@ describe "DiscussionThreadListView", ->
) )
{always: ->} {always: ->}
) )
view.$el.find(".sort-bar a[data-sort='"+new_type+"']").click() sortControl.val(new_type).change()
expect($.ajax).toHaveBeenCalled() expect($.ajax).toHaveBeenCalled()
expect(view.sortBy).toEqual(new_type)
checkThreadsOrdering(view, sort_order, new_type) checkThreadsOrdering(view, sort_order, new_type)
it "with sort preference date", -> it "with sort preference date", ->
...@@ -271,3 +383,179 @@ describe "DiscussionThreadListView", -> ...@@ -271,3 +383,179 @@ describe "DiscussionThreadListView", ->
@view.searchForUser("dummy") @view.searchForUser("dummy")
expect($.ajax).toHaveBeenCalled() expect($.ajax).toHaveBeenCalled()
expect(@view.addSearchAlert).not.toHaveBeenCalled() expect(@view.addSearchAlert).not.toHaveBeenCalled()
describe "endorsed renders correctly", ->
it "when absent", ->
renderSingleThreadWithProps({})
expect($(".forum-nav-thread-endorsed").length).toEqual(0)
it "when present", ->
renderSingleThreadWithProps({endorsed: true})
expect($(".forum-nav-thread-endorsed").length).toEqual(1)
describe "post labels render correctly", ->
beforeEach ->
@moderatorId = "42"
@administratorId = "43"
@communityTaId = "44"
DiscussionUtil.loadRoles({
"Moderator": [parseInt(@moderatorId)],
"Administrator": [parseInt(@administratorId)],
"Community TA": [parseInt(@communityTaId)],
})
it "for pinned", ->
renderSingleThreadWithProps({pinned: true})
expect($(".forum-nav-thread-label-pinned").length).toEqual(1)
it "for following", ->
renderSingleThreadWithProps({subscribed: true})
expect($(".forum-nav-thread-label-following").length).toEqual(1)
it "for moderator", ->
renderSingleThreadWithProps({user_id: @moderatorId})
expect($(".forum-nav-thread-label-staff").length).toEqual(1)
it "for administrator", ->
renderSingleThreadWithProps({user_id: @administratorId})
expect($(".forum-nav-thread-label-staff").length).toEqual(1)
it "for community TA", ->
renderSingleThreadWithProps({user_id: @communityTaId})
expect($(".forum-nav-thread-label-community-ta").length).toEqual(1)
it "when none should be present", ->
renderSingleThreadWithProps({})
expect($(".forum-nav-thread-labels").length).toEqual(0)
describe "browse menu", ->
setupAjax = (callback) ->
$.ajax.andCallFake(
(params) =>
if callback
callback(params)
params.success({discussion_data: [], page: 1, num_pages: 1})
{always: ->}
)
afterEach ->
# Remove handler added to make browse menu disappear
$("body").unbind("click")
expectBrowseMenuVisible = (isVisible) ->
expect($(".forum-nav-browse-menu:visible").length).toEqual(if isVisible then 1 else 0)
expect($(".forum-nav-thread-list-wrapper:visible").length).toEqual(if isVisible then 0 else 1)
it "should not be visible by default", ->
expectBrowseMenuVisible(false)
it "should show when header button is clicked", ->
$(".forum-nav-browse").click()
expectBrowseMenuVisible(true)
describe "when shown", ->
beforeEach ->
$(".forum-nav-browse").click()
it "should hide when header button is clicked", ->
$(".forum-nav-browse").click()
expectBrowseMenuVisible(false)
it "should hide when a click outside the menu occurs", ->
$(".forum-nav-search-input").click()
expectBrowseMenuVisible(false)
it "should hide when a search is executed", ->
setupAjax()
$(".forum-nav-search-input").trigger($.Event("keydown", {which: 13}))
expectBrowseMenuVisible(false)
it "should hide when a category is clicked", ->
$(".forum-nav-browse-title")[0].click()
expectBrowseMenuVisible(false)
it "should still be shown when filter input is clicked", ->
$(".forum-nav-browse-filter-input").click()
expectBrowseMenuVisible(true)
describe "filtering", ->
checkFilter = (filterText, expectedItems) ->
$(".forum-nav-browse-filter-input").val(filterText).keyup()
visibleItems = $(".forum-nav-browse-title:visible").map(
(i, elem) -> $(elem).text()
).get()
expect(visibleItems).toEqual(expectedItems)
it "should be case-insensitive", ->
checkFilter("flagged", ["Flagged Discussions"])
it "should match partial words", ->
checkFilter("ateg", ["Other Category"])
it "should show ancestors and descendants of matches", ->
checkFilter("Target", ["Parent", "Target", "Child"])
it "should handle multiple words regardless of order", ->
checkFilter("Following Posts", ["Posts I'm Following"])
it "should handle multiple words in different depths", ->
checkFilter("Parent Child", ["Parent", "Target", "Child"])
describe "selecting an item", ->
it "should clear the search box", ->
setupAjax()
$(".forum-nav-search-input").val("foobar")
$(".forum-nav-browse-menu-following .forum-nav-browse-title").click()
expect($(".forum-nav-search-input").val()).toEqual("")
it "should change the button text", ->
setupAjax()
$(".forum-nav-browse-menu-following .forum-nav-browse-title").click()
expect($(".forum-nav-browse-current").text()).toEqual("Posts I'm Following")
testSelectionRequest = (callback, itemText) ->
setupAjax(callback)
$(".forum-nav-browse-title:contains(#{itemText})").click()
it "should get all discussions", ->
testSelectionRequest(
(params) -> expect(params.url.path()).toEqual(DiscussionUtil.urlFor("threads")),
"All"
)
it "should get flagged threads", ->
testSelectionRequest(
(params) ->
expect(params.url.path()).toEqual(DiscussionUtil.urlFor("search"))
expect(params.data.flagged).toEqual(true)
,
"Flagged"
)
it "should get followed threads", ->
testSelectionRequest(
(params) ->
expect(params.url.path()).toEqual(
DiscussionUtil.urlFor("followed_threads", window.user.id)
)
,
"Following"
)
it "should get threads for the selected leaf", ->
testSelectionRequest(
(params) ->
expect(params.url.path()).toEqual(DiscussionUtil.urlFor("search"))
expect(params.data.commentable_ids).toEqual("child")
,
"Child"
)
it "should get threads for children of the selected intermediate node", ->
testSelectionRequest(
(params) ->
expect(params.url.path()).toEqual(DiscussionUtil.urlFor("search"))
expect(params.data.commentable_ids).toEqual("child,sibling")
,
"Parent"
)
describe "DiscussionThreadShowView", -> describe "DiscussionThreadShowView", ->
beforeEach -> beforeEach ->
DiscussionSpecHelper.setUpGlobals()
setFixtures( setFixtures(
""" """
<div class="discussion-post"> <div class="discussion-post">
...@@ -14,8 +15,6 @@ describe "DiscussionThreadShowView", -> ...@@ -14,8 +15,6 @@ describe "DiscussionThreadShowView", ->
""" """
) )
window.$$course_id = "TestOrg/TestCourse/TestRun"
window.user = new DiscussionUser({id: "567", upvoted_ids: []})
@threadData = { @threadData = {
id: "dummy", id: "dummy",
user_id: user.id, user_id: user.id,
......
describe "DiscussionUserProfileView", -> describe "DiscussionUserProfileView", ->
beforeEach -> beforeEach ->
DiscussionSpecHelper.setUpGlobals()
setFixtures( setFixtures(
""" """
<script type="text/template" id="_user_profile"> <script type="text/template" id="_user_profile">
...@@ -46,7 +47,6 @@ describe "DiscussionUserProfileView", -> ...@@ -46,7 +47,6 @@ describe "DiscussionUserProfileView", ->
<div class="user-profile-fixture"/> <div class="user-profile-fixture"/>
""" """
) )
window.$$course_id = "dummy_course_id"
spyOn(DiscussionThreadProfileView.prototype, "render") spyOn(DiscussionThreadProfileView.prototype, "render")
makeView = (threads, page, numPages) -> makeView = (threads, page, numPages) ->
......
describe 'ResponseCommentShowView', -> describe 'ResponseCommentShowView', ->
beforeEach -> beforeEach ->
DiscussionSpecHelper.setUpGlobals()
# set up the container for the response to go in # set up the container for the response to go in
setFixtures """ setFixtures """
<ol class="responses"></ol> <ol class="responses"></ol>
......
describe 'ResponseCommentView', -> describe 'ResponseCommentView', ->
beforeEach -> beforeEach ->
window.$$course_id = 'edX/999/test' DiscussionSpecHelper.setUpGlobals()
window.user = new DiscussionUser {id: '567'}
DiscussionUtil.loadRoles []
@comment = new Comment { @comment = new Comment {
id: '01234567', id: '01234567',
user_id: user.id, user_id: user.id,
......
describe "ThreadResponseShowView", -> describe "ThreadResponseShowView", ->
beforeEach -> beforeEach ->
DiscussionSpecHelper.setUpGlobals()
setFixtures( setFixtures(
""" """
<div class="discussion-post"> <div class="discussion-post">
...@@ -22,7 +23,6 @@ describe "ThreadResponseShowView", -> ...@@ -22,7 +23,6 @@ describe "ThreadResponseShowView", ->
@comment = new Comment(@commentData) @comment = new Comment(@commentData)
@view = new ThreadResponseShowView({ model: @comment }) @view = new ThreadResponseShowView({ model: @comment })
@view.setElement($(".discussion-post")) @view.setElement($(".discussion-post"))
window.user = new DiscussionUser({id: "567", upvoted_ids: []})
it "renders the vote correctly", -> it "renders the vote correctly", ->
DiscussionViewSpecHelper.checkRenderVote(@view, @comment) DiscussionViewSpecHelper.checkRenderVote(@view, @comment)
......
...@@ -53,9 +53,12 @@ if Backbone? ...@@ -53,9 +53,12 @@ if Backbone?
initialize: -> initialize: ->
Content.addContent @id, @ Content.addContent @id, @
userId = @get('user_id')
@set('staff_authored', DiscussionUtil.isStaff(userId))
@set('community_ta_authored', DiscussionUtil.isTA(userId))
if Content.getInfo(@id) if Content.getInfo(@id)
@updateInfo(Content.getInfo(@id)) @updateInfo(Content.getInfo(@id))
@set 'user_url', DiscussionUtil.urlFor('user_profile', @get('user_id')) @set 'user_url', DiscussionUtil.urlFor('user_profile', userId)
@resetComments(@get('children')) @resetComments(@get('children'))
remove: -> remove: ->
......
...@@ -62,9 +62,9 @@ if Backbone? ...@@ -62,9 +62,9 @@ if Backbone?
new_threads = [new Thread(data) for data in response.discussion_data][0] new_threads = [new Thread(data) for data in response.discussion_data][0]
new_collection = _.union(models, new_threads) new_collection = _.union(models, new_threads)
Content.loadContentInfos(response.annotated_content_info) Content.loadContentInfos(response.annotated_content_info)
@reset new_collection
@pages = response.num_pages @pages = response.num_pages
@current_page = response.page @current_page = response.page
@reset new_collection
error: error error: error
sortByDate: (thread) -> sortByDate: (thread) ->
......
...@@ -20,6 +20,8 @@ if Backbone? ...@@ -20,6 +20,8 @@ if Backbone?
Backbone.history.start({pushState: true, root: "/courses/#{$$course_id}/discussion/forum/"}) Backbone.history.start({pushState: true, root: "/courses/#{$$course_id}/discussion/forum/"})
DiscussionProfileApp = DiscussionProfileApp =
start: (elem) -> start: (elem) ->
# Roles are not included in user profile page, but they are not used for anything
DiscussionUtil.loadRoles({"Moderator": [], "Administrator": [], "Community TA": []})
element = $(elem) element = $(elem)
window.$$course_id = element.data("course-id") window.$$course_id = element.data("course-id")
threads = element.data("threads") threads = element.data("threads")
......
...@@ -32,12 +32,12 @@ class @DiscussionUtil ...@@ -32,12 +32,12 @@ class @DiscussionUtil
@loadFlagModerator($("#discussion-container").data("flag-moderator")) @loadFlagModerator($("#discussion-container").data("flag-moderator"))
@isStaff: (user_id) -> @isStaff: (user_id) ->
user_id ?= @user.id user_id ?= @user?.id
staff = _.union(@roleIds['Moderator'], @roleIds['Administrator']) staff = _.union(@roleIds['Moderator'], @roleIds['Administrator'])
_.include(staff, parseInt(user_id)) _.include(staff, parseInt(user_id))
@isTA: (user_id) -> @isTA: (user_id) ->
user_id ?= @user.id user_id ?= @user?.id
ta = _.union(@roleIds['Community TA']) ta = _.union(@roleIds['Community TA'])
_.include(ta, parseInt(user_id)) _.include(ta, parseInt(user_id))
...@@ -93,6 +93,10 @@ class @DiscussionUtil ...@@ -93,6 +93,10 @@ class @DiscussionUtil
"notifications_status" : "/notification_prefs/status/" "notifications_status" : "/notification_prefs/status/"
}[name] }[name]
@ignoreEnterKey: (event) =>
if event.which == 13
event.preventDefault()
@activateOnSpace: (event, func) -> @activateOnSpace: (event, func) ->
if event.which == 32 if event.which == 32
event.preventDefault() event.preventDefault()
......
if Backbone? if Backbone?
class @DiscussionThreadListView extends Backbone.View class @DiscussionThreadListView extends Backbone.View
events: events:
"click .search": "showSearch" "click .forum-nav-browse": "toggleBrowseMenu"
"click .home": "goHome" "keypress .forum-nav-browse-filter-input": (event) => DiscussionUtil.ignoreEnterKey(event)
"click .browse": "toggleTopicDrop" "keyup .forum-nav-browse-filter-input": "filterTopics"
"keydown .post-search-field": "performSearch" "click .forum-nav-browse-menu-wrapper": "ignoreClick"
"focus .post-search-field": "showSearch" "click .forum-nav-browse-title": "selectTopic"
"click .sort-bar a": "sortThreads" "keydown .forum-nav-search-input": "performSearch"
"click .browse-topic-drop-menu": "filterTopic" "change .forum-nav-sort-control": "sortThreads"
"click .browse-topic-drop-search-input": "ignoreClick" "click .forum-nav-thread-link": "threadSelected"
"click .post-list .list-item a": "threadSelected" "click .forum-nav-load-more-link": "loadMorePages"
"click .post-list .more-pages a": "loadMorePages" "change .forum-nav-filter-cohort-control": "chooseCohort"
"change .cohort-options": "chooseCohort"
'keyup .browse-topic-drop-search-input': DiscussionFilter.filterDrop
initialize: -> initialize: ->
@displayedCollection = new Discussion(@collection.models, pages: @collection.pages) @displayedCollection = new Discussion(@collection.models, pages: @collection.pages)
@collection.on "change", @reloadDisplayedCollection @collection.on "change", @reloadDisplayedCollection
@sortBy = "date"
@discussionIds="" @discussionIds=""
@collection.on "reset", (discussion) => @collection.on "reset", (discussion) =>
board = $(".current-board").html() board = $(".current-board").html()
...@@ -30,7 +27,6 @@ if Backbone? ...@@ -30,7 +27,6 @@ if Backbone?
# @filterTopic($.Event("filter", {'target': target[0]})) # @filterTopic($.Event("filter", {'target': target[0]}))
@collection.on "add", @addAndSelectThread @collection.on "add", @addAndSelectThread
@sidebar_padding = 10 @sidebar_padding = 10
@sidebar_header_height = 87
@boardName @boardName
@template = _.template($("#thread-list-template").html()) @template = _.template($("#thread-list-template").html())
@current_search = "" @current_search = ""
...@@ -68,9 +64,10 @@ if Backbone? ...@@ -68,9 +64,10 @@ if Backbone?
@clearSearchAlerts() @clearSearchAlerts()
thread_id = thread.get('id') thread_id = thread.get('id')
content = @renderThread(thread) content = @renderThread(thread)
current_el = @$("a[data-id=#{thread_id}]") current_el = @$(".forum-nav-thread[data-id=#{thread_id}]")
active = current_el.hasClass("active") active = current_el.has(".forum-nav-thread-link.is-active").length != 0
current_el.replaceWith(content) current_el.replaceWith(content)
@showMetadataAccordingToSort()
if active if active
@setActiveThread(thread_id) @setActiveThread(thread_id)
...@@ -78,8 +75,8 @@ if Backbone? ...@@ -78,8 +75,8 @@ if Backbone?
#TODO fix this entire chain of events #TODO fix this entire chain of events
addAndSelectThread: (thread) => addAndSelectThread: (thread) =>
commentable_id = thread.get("commentable_id") commentable_id = thread.get("commentable_id")
commentable = @$(".board-name[data-discussion_id]").filter(-> $(this).data("discussion_id").id == commentable_id) menuItem = @$(".forum-nav-browse-menu-item[data-discussion-id]").filter(-> $(this).data("discussion-id").id == commentable_id)
@setTopicHack(commentable) @setCurrentTopicDisplay(@getPathText(menuItem))
@retrieveDiscussion commentable_id, => @retrieveDiscussion commentable_id, =>
@trigger "thread:created", thread.get('id') @trigger "thread:created", thread.get('id')
...@@ -88,7 +85,7 @@ if Backbone? ...@@ -88,7 +85,7 @@ if Backbone?
scrollTop = $(window).scrollTop(); scrollTop = $(window).scrollTop();
windowHeight = $(window).height(); windowHeight = $(window).height();
discussionBody = $(".discussion-article") discussionBody = $(".discussion-column")
discussionsBodyTop = if discussionBody[0] then discussionBody.offset().top discussionsBodyTop = if discussionBody[0] then discussionBody.offset().top
discussionsBodyBottom = discussionsBodyTop + discussionBody.outerHeight() discussionsBodyBottom = discussionsBodyTop + discussionBody.outerHeight()
...@@ -98,9 +95,6 @@ if Backbone? ...@@ -98,9 +95,6 @@ if Backbone?
else else
sidebar.css('top', '0'); sidebar.css('top', '0');
sidebarWidth = .31 * $(".discussion-body").width();
sidebar.css('width', sidebarWidth + 'px');
sidebarHeight = windowHeight - Math.max(discussionsBodyTop - scrollTop, @sidebar_padding) sidebarHeight = windowHeight - Math.max(discussionsBodyTop - scrollTop, @sidebar_padding)
topOffset = scrollTop + windowHeight topOffset = scrollTop + windowHeight
...@@ -111,19 +105,22 @@ if Backbone? ...@@ -111,19 +105,22 @@ if Backbone?
sidebarHeight = Math.min(sidebarHeight + 1, discussionBody.outerHeight()) sidebarHeight = Math.min(sidebarHeight + 1, discussionBody.outerHeight())
sidebar.css 'height', sidebarHeight sidebar.css 'height', sidebarHeight
postListWrapper = @$('.post-list-wrapper') headerHeight = @$(".forum-nav-header").outerHeight()
postListWrapper.css('height', (sidebarHeight - @sidebar_header_height - 4) + 'px') refineBarHeight = @$(".forum-nav-refine-bar").outerHeight()
@$('.forum-nav-thread-list').css('height', (sidebarHeight - headerHeight - refineBarHeight - 2) + 'px')
@$('.forum-nav-browse-menu-wrapper').css('height', (sidebarHeight - headerHeight - 2) + 'px')
# Because we want the behavior that when the body is clicked the menu is # 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. # closed, we need to stop propagation of a click in any part of the menu
# Without this, clicking the search field would also close the menu. # that is not a link.
ignoreClick: (event) -> ignoreClick: (event) ->
event.stopPropagation() event.stopPropagation()
render: -> render: ->
@timer = 0 @timer = 0
@$el.html(@template()) @$el.html(@template())
@$(".forum-nav-sort-control").val(@collection.sort_preference)
$(window).bind "load", @updateSidebar $(window).bind "load", @updateSidebar
$(window).bind "scroll", @updateSidebar $(window).bind "scroll", @updateSidebar
...@@ -132,36 +129,49 @@ if Backbone? ...@@ -132,36 +129,49 @@ if Backbone?
@displayedCollection.on "reset", @renderThreads @displayedCollection.on "reset", @renderThreads
@displayedCollection.on "thread:remove", @renderThreads @displayedCollection.on "thread:remove", @renderThreads
@renderThreads() @renderThreads()
sort_element = @$('.sort-bar a[data-sort="' + this.collection.sort_preference + '"]')
sort_element.attr('aria-checked',true)
sort_element.addClass('active')
@ @
renderThreads: => renderThreads: =>
@$(".post-list").html("") @$(".forum-nav-thread-list").html("")
rendered = $("<div></div>") rendered = $("<div></div>")
for thread in @displayedCollection.models for thread in @displayedCollection.models
content = @renderThread(thread) content = @renderThread(thread)
rendered.append content rendered.append content
content.wrap("<li class='list-item' data-id='\"#{thread.get('id')}\"' />")
@$(".post-list").html(rendered.html()) @$(".forum-nav-thread-list").html(rendered.html())
@showMetadataAccordingToSort()
@renderMorePages() @renderMorePages()
@updateSidebar() @updateSidebar()
@trigger "threads:rendered" @trigger "threads:rendered"
showMetadataAccordingToSort: () =>
# Ensure that threads display metadata appropriate for the current sort
voteCounts = @$(".forum-nav-thread-votes-count")
commentCounts = @$(".forum-nav-thread-comments-count")
voteCounts.hide()
commentCounts.hide()
switch @$(".forum-nav-sort-control").val()
when "date", "comments"
commentCounts.show()
when "votes"
voteCounts.show()
renderMorePages: -> renderMorePages: ->
if @displayedCollection.hasMorePages() if @displayedCollection.hasMorePages()
@$(".post-list").append("<li class='more-pages'><a href='#'>" + gettext("Load more") + "</a></li>") @$(".forum-nav-thread-list").append("<li class='forum-nav-load-more'><a href='#' class='forum-nav-load-more-link'>" + gettext("Load more") + "</a></li>")
getLoadingContent: (srText) ->
return '<div class="forum-nav-loading" tabindex="0"><span class="icon-spinner icon-spin"/><span class="sr" role="alert">' + srText + '</span></div>'
loadMorePages: (event) -> loadMorePages: (event) =>
if event if event
event.preventDefault() event.preventDefault()
@$(".more-pages").html('<div class="loading-animation" tabindex=0><span class="sr" role="alert">' + gettext('Loading more threads') + '</span></div>') loadMoreElem = @$(".forum-nav-load-more")
@$(".more-pages").addClass("loading") loadMoreElem.html(@getLoadingContent(gettext("Loading more threads")))
loadingDiv = @$(".more-pages .loading-animation") loadingElem = loadMoreElem.find(".forum-nav-loading")
DiscussionUtil.makeFocusTrap(loadingDiv) DiscussionUtil.makeFocusTrap(loadingElem)
loadingDiv.focus() loadingElem.focus()
options = {} options = {}
switch @mode switch @mode
when 'search' when 'search'
...@@ -184,57 +194,40 @@ if Backbone? ...@@ -184,57 +194,40 @@ if Backbone?
if lastThread if lastThread
# Pagination; focus the first thread after what was previously the last thread # Pagination; focus the first thread after what was previously the last thread
@once("threads:rendered", -> @once("threads:rendered", ->
$(".post-list li:has(a[data-id='#{lastThread}']) + li a").focus() $(".forum-nav-thread[data-id='#{lastThread}'] + .forum-nav-thread .forum-nav-thread-link").focus()
) )
else else
# Totally refreshing the list (e.g. from clicking a sort button); focus the first thread # Totally refreshing the list (e.g. from clicking a sort button); focus the first thread
@once("threads:rendered", -> @once("threads:rendered", ->
$(".post-list a").first()?.focus() $(".forum-nav-thread-link").first()?.focus()
) )
error = => error = =>
@renderThreads() @renderThreads()
DiscussionUtil.discussionAlert(gettext("Sorry"), gettext("We had some trouble loading more threads. Please try again.")) DiscussionUtil.discussionAlert(gettext("Sorry"), gettext("We had some trouble loading more threads. Please try again."))
@collection.retrieveAnotherPage(@mode, options, {sort_key: @sortBy}, error) @collection.retrieveAnotherPage(@mode, options, {sort_key: @$(".forum-nav-sort-control").val()}, error)
renderThread: (thread) => renderThread: (thread) =>
content = $(_.template($("#thread-list-item-template").html())(thread.toJSON())) content = $(_.template($("#thread-list-item-template").html())(thread.toJSON()))
if thread.get('subscribed') unreadCount = thread.get('unread_comments_count') + (if thread.get("read") then 0 else 1)
content.addClass("followed")
if thread.get('endorsed')
content.addClass("resolved")
if thread.get('read')
content.addClass("read")
unreadCount = thread.get('unread_comments_count')
if unreadCount > 0 if unreadCount > 0
content.find('.comments-count').addClass("unread").attr( content.find('.forum-nav-thread-comments-count').attr(
"data-tooltip", "data-tooltip",
interpolate( interpolate(
ngettext('%(unread_count)s new comment', '%(unread_count)s new comments', unreadCount), ngettext('%(unread_count)s new comment', '%(unread_count)s new comments', unreadCount),
{unread_count: thread.get('unread_comments_count')}, {unread_count: unreadCount},
true true
) )
) )
@highlight(content) content
highlight: (el) ->
el.html(el.html().replace(/&lt;mark&gt;/g, "<mark>").replace(/&lt;\/mark&gt;/g, "</mark>"))
renderThreadListItem: (thread) =>
view = new ThreadListItemView(model: thread)
view.on "thread:selected", @threadSelected
view.on "thread:removed", @threadRemoved
view.render()
@$(".post-list").append(view.el)
threadSelected: (e) => threadSelected: (e) =>
# Use .attr('data-id') rather than .data('id') because .data does type # Use .attr('data-id') rather than .data('id') because .data does type
# coercion. Usually, this is fine, but when Mongo gives an object id with # coercion. Usually, this is fine, but when Mongo gives an object id with
# no letters, it casts it to a Number. # no letters, it casts it to a Number.
thread_id = $(e.target).closest("a").attr("data-id") thread_id = $(e.target).closest(".forum-nav-thread").attr("data-id")
@setActiveThread(thread_id) @setActiveThread(thread_id)
@trigger("thread:selected", thread_id) # This triggers a callback in the DiscussionRouter which calls the line above... @trigger("thread:selected", thread_id) # This triggers a callback in the DiscussionRouter which calls the line above...
false false
...@@ -243,21 +236,13 @@ if Backbone? ...@@ -243,21 +236,13 @@ if Backbone?
@trigger("thread:removed", thread_id) @trigger("thread:removed", thread_id)
setActiveThread: (thread_id) -> setActiveThread: (thread_id) ->
@$(".post-list a[data-id!='#{thread_id}']").removeClass("active") @$(".forum-nav-thread[data-id!='#{thread_id}'] .forum-nav-thread-link").removeClass("is-active")
@$(".post-list a[data-id='#{thread_id}']").addClass("active") @$(".forum-nav-thread[data-id='#{thread_id}'] .forum-nav-thread-link").addClass("is-active")
showSearch: ->
@$(".browse").removeClass('is-dropped')
@hideTopicDrop()
@$(".search").addClass('is-open')
@$(".browse").removeClass('is-open')
setTimeout (-> @$(".post-search-field").focus()), 200 unless @$(".post-search-field").is(":focus")
goHome: -> goHome: ->
@template = _.template($("#discussion-home").html()) @template = _.template($("#discussion-home").html())
$(".discussion-column").html(@template) $(".discussion-column").html(@template)
$(".post-list a").removeClass("active") $(".forum-nav-thread-list a").removeClass("is-active")
$("input.email-setting").bind "click", @updateEmailNotifications $("input.email-setting").bind "click", @updateEmailNotifications
url = DiscussionUtil.urlFor("notifications_status",window.user.get("id")) url = DiscussionUtil.urlFor("notifications_status",window.user.get("id"))
DiscussionUtil.safeAjax DiscussionUtil.safeAjax
...@@ -272,51 +257,64 @@ if Backbone? ...@@ -272,51 +257,64 @@ if Backbone?
@trigger("thread:removed") @trigger("thread:removed")
#select all threads #select all threads
isBrowseMenuVisible: =>
toggleTopicDrop: (event) => @$(".forum-nav-browse-menu-wrapper").is(":visible")
showBrowseMenu: =>
if not @isBrowseMenuVisible()
@$(".forum-nav-browse").addClass("is-active")
@$(".forum-nav-browse-menu-wrapper").show()
@$(".forum-nav-thread-list-wrapper").hide()
$(".forum-nav-browse-filter-input").focus()
$("body").bind "click", @hideBrowseMenu
hideBrowseMenu: =>
if @isBrowseMenuVisible()
@$(".forum-nav-browse").removeClass("is-active")
@$(".forum-nav-browse-menu-wrapper").hide()
@$(".forum-nav-thread-list-wrapper").show()
$("body").unbind "click", @hideBrowseMenu
toggleBrowseMenu: (event) =>
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
if @current_search != ""
@clearSearch() if @isBrowseMenuVisible()
@$(".search").removeClass('is-open') @hideBrowseMenu()
@$(".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 else
@hideTopicDrop() @showBrowseMenu()
hideTopicDrop: -> # Given a menu item, get the text for it and its ancestors
@$(".browse-topic-drop-menu-wrapper").hide() # (starting from the root, separated by " / ")
$("body").unbind "click", @toggleTopicDrop getPathText: (item) ->
$("body").unbind "keydown", @setActiveItem path = item.parents(".forum-nav-browse-menu-item").andSelf()
pathTitles = path.children(".forum-nav-browse-title").map((i, elem) -> $(elem).text()).get()
# TODO get rid of this asap pathText = pathTitles.join(" / ")
setTopicHack: (boardNameContainer) ->
item = $(boardNameContainer).closest('a') filterTopics: (event) =>
boardName = item.find(".board-name").html() query = $(event.target).val()
_.each item.parents('ul').not('.browse-topic-drop-menu'), (parent) -> items = @$(".forum-nav-browse-menu-item")
boardName = $(parent).siblings('a').find('.board-name').html() + ' / ' + boardName if query.length == 0
@$(".current-board").html(@fitName(boardName)) items.show()
else
setTopic: (event) -> # If all filter terms occur in the path to an item then that item and
item = $(event.target).closest('a') # all its descendants are displayed
boardName = item.find(".board-name").html() items.hide()
_.each item.parents('ul').not('.browse-topic-drop-menu'), (parent) -> items.each (i, item) =>
boardName = $(parent).siblings('a').find('.board-name').html() + ' / ' + boardName item = $(item)
@$(".current-board").html(@fitName(boardName)) if not item.is(":visible")
pathText = @getPathText(item).toLowerCase()
setSelectedTopic: (name) -> if query.split(" ").every((term) -> pathText.search(term.toLowerCase()) != -1)
@$(".current-board").html(@fitName(name)) path = item.parents(".forum-nav-browse-menu-item").andSelf()
path.add(item.find(".forum-nav-browse-menu-item")).show()
setCurrentTopicDisplay: (text) ->
@$(".forum-nav-browse-current").text(@fitName(text))
getNameWidth: (name) -> getNameWidth: (name) ->
test = $("<div>") test = $("<div>")
test.css test.css
"font-size": @$(".current-board").css('font-size') "font-size": @$(".forum-nav-browse-current").css('font-size')
opacity: 0 opacity: 0
position: 'absolute' position: 'absolute'
left: -1000 left: -1000
...@@ -328,54 +326,57 @@ if Backbone? ...@@ -328,54 +326,57 @@ if Backbone?
return width return width
fitName: (name) -> fitName: (name) ->
@maxNameWidth = (@$el.width() * .8) - 50 @maxNameWidth = @$(".forum-nav-browse").width() -
parseInt(@$(".forum-nav-browse").css("padding-left")) -
parseInt(@$(".forum-nav-browse").css("padding-right")) -
@$(".forum-nav-browse .icon").outerWidth(true) -
@$(".forum-nav-browse-drop-arrow").outerWidth(true)
width = @getNameWidth(name) width = @getNameWidth(name)
if width < @maxNameWidth if width < @maxNameWidth
return name return name
path = (x.replace /^\s+|\s+$/g, "" for x in name.split("/")) path = (x.replace /^\s+|\s+$/g, "" for x in name.split("/"))
prefix = ""
while path.length > 1 while path.length > 1
prefix = gettext("…") + "/"
path.shift() path.shift()
partialName = gettext("…") + "/" + path.join("/") partialName = prefix + path.join("/")
if @getNameWidth(partialName) < @maxNameWidth if @getNameWidth(partialName) < @maxNameWidth
return partialName return partialName
rawName = path[0] rawName = path[0]
name = gettext("…") + "/" + rawName name = prefix + rawName
while @getNameWidth(name) > @maxNameWidth while @getNameWidth(name) > @maxNameWidth
rawName = rawName[0...rawName.length-1] rawName = rawName[0...rawName.length-1]
name = gettext("…") + "/" + rawName + gettext("…") name = prefix + rawName + gettext("…")
return name return name
filterTopic: (event) -> selectTopic: (event) ->
if @current_search != "" event.preventDefault()
@setTopic(event) @hideBrowseMenu()
@clearSearch @filterTopic, event @clearSearch()
item = $(event.target).closest('.forum-nav-browse-menu-item')
@setCurrentTopicDisplay(@getPathText(item))
if item.hasClass("forum-nav-browse-menu-all")
@discussionIds = ""
@$('.forum-nav-filter-cohort').show()
@retrieveAllThreads()
else if item.hasClass("forum-nav-browse-menu-flagged")
@discussionIds = ""
@$('.forum-nav-filter-cohort').hide()
@retrieveFlaggedThreads()
else if item.hasClass("forum-nav-browse-menu-following")
@retrieveFollowed()
@$('.forum-nav-filter-cohort').hide()
else else
@setTopic(event) # just sets the title for the dropdown allItems = item.find(".forum-nav-browse-menu-item").andSelf()
item = $(event.target).closest('li') discussionIds = allItems.filter("[data-discussion-id]").map(
discussionId = item.find("span.board-name").data("discussion_id") (i, elem) -> $(elem).data("discussion-id").id
if discussionId == "#all" ).get()
@discussionIds = "" @retrieveDiscussions(discussionIds)
@$(".post-search-field").val("") @$(".forum-nav-filter-cohort").toggle(item.data('cohorted') == true)
@$('.cohort').show()
@retrieveAllThreads()
else if discussionId == "#flagged"
@discussionIds = ""
@$(".post-search-field").val("")
@$('.cohort').hide()
@retrieveFlaggedThreads()
else if discussionId == "#following"
@retrieveFollowed(event)
@$('.cohort').hide()
else
discussionIds = _.map item.find(".board-name[data-discussion_id]"), (board) -> $(board).data("discussion_id").id
if $(event.target).attr('cohorted') == "True"
@retrieveDiscussions(discussionIds, "function(){$('.cohort').show();}")
else
@retrieveDiscussions(discussionIds, "function(){$('.cohort').hide();}")
chooseCohort: (event) -> chooseCohort: (event) ->
@group_id = @$('.cohort-options :selected').val() @group_id = @$('.forum-nav-filter-cohort-control :selected').val()
@collection.current_page = 0 @collection.current_page = 0
@collection.reset() @collection.reset()
@loadMorePages(event) @loadMorePages(event)
...@@ -416,23 +417,19 @@ if Backbone? ...@@ -416,23 +417,19 @@ if Backbone?
@loadMorePages(event) @loadMorePages(event)
sortThreads: (event) -> sortThreads: (event) ->
activeSort = @$(".sort-bar a.active") @displayedCollection.setSortComparator(@$(".forum-nav-sort-control").val())
activeSort.removeClass("active")
activeSort.attr("aria-checked", "false")
newSort = $(event.target)
newSort.addClass("active")
newSort.attr("aria-checked", "true")
@sortBy = newSort.data("sort")
@displayedCollection.setSortComparator(@sortBy)
@retrieveFirstPage(event) @retrieveFirstPage(event)
performSearch: (event) -> performSearch: (event) ->
if event.which == 13 if event.which == 13
event.preventDefault() event.preventDefault()
text = @$(".post-search-field").val() @hideBrowseMenu()
@setCurrentTopicDisplay(gettext("Search Results"))
text = @$(".forum-nav-search-input").val()
@searchFor(text) @searchFor(text)
searchFor: (text, callback, value) -> searchFor: (text) ->
@clearSearchAlerts() @clearSearchAlerts()
@mode = 'search' @mode = 'search'
@current_search = text @current_search = text
...@@ -441,17 +438,16 @@ if Backbone? ...@@ -441,17 +438,16 @@ if Backbone?
# Mainly because this currently does not reset any pagination variables which could cause problems. # Mainly because this currently does not reset any pagination variables which could cause problems.
# This doesn't use pagination either. # This doesn't use pagination either.
DiscussionUtil.safeAjax DiscussionUtil.safeAjax
$elem: @$(".post-search-field") $elem: @$(".forum-nav-search-input")
data: { text: text } data: { text: text }
url: url url: url
type: "GET" type: "GET"
dataType: 'json' dataType: 'json'
$loading: $ $loading: $
loadingCallback: => loadingCallback: =>
@$(".post-list").html('<li class="loading"><div class="loading-animation"><span class="sr">' + gettext('Loading thread list') + '</span></div></li>') @$(".forum-nav-thread-list").html("<li class='forum-nav-load-more'>" + @getLoadingContent(gettext("Loading thread list")) + "</li>")
loadedCallback: => loadedCallback: =>
if callback @$(".forum-nav-thread-list .forum-nav-load-more").remove()
callback.apply @, [value]
success: (response, textStatus) => success: (response, textStatus) =>
if textStatus == 'success' if textStatus == 'success'
# TODO: Augment existing collection? # TODO: Augment existing collection?
...@@ -497,40 +493,13 @@ if Backbone? ...@@ -497,40 +493,13 @@ if Backbone?
) )
@addSearchAlert(message) @addSearchAlert(message)
clearSearch: (callback, value) -> clearSearch: ->
@$(".post-search-field").val("") @$(".forum-nav-search-input").val("")
@searchFor("", callback, value) @current_search = ""
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)=> retrieveFollowed: () =>
@mode = 'followed' @mode = 'followed'
@retrieveFirstPage(event) @retrieveFirstPage()
updateEmailNotifications: () => updateEmailNotifications: () =>
if $('input.email-setting').attr('checked') if $('input.email-setting').attr('checked')
......
...@@ -179,19 +179,20 @@ class DiscussionSortPreferencePage(CoursePage): ...@@ -179,19 +179,20 @@ class DiscussionSortPreferencePage(CoursePage):
""" """
Return true if the browser is on the right page else false. Return true if the browser is on the right page else false.
""" """
return self.q(css="body.discussion .sort-bar").present return self.q(css="body.discussion .forum-nav-sort-control").present
def get_selected_sort_preference_text(self): def get_selected_sort_preference(self):
""" """
Return the text of option that is selected for sorting. Return the text of option that is selected for sorting.
""" """
return self.q(css="body.discussion .sort-bar a.active").text[0].lower() options = self.q(css="body.discussion .forum-nav-sort-control option")
return options.filter(lambda el: el.is_selected())[0].get_attribute("value")
def change_sort_preference(self, sort_by): def change_sort_preference(self, sort_by):
""" """
Change the option of sorting by clicking on new option. Change the option of sorting by clicking on new option.
""" """
self.q(css="body.discussion .sort-bar a[data-sort='{0}']".format(sort_by)).click() self.q(css="body.discussion .forum-nav-sort-control option[value='{0}']".format(sort_by)).click()
def refresh_page(self): def refresh_page(self):
""" """
...@@ -352,12 +353,7 @@ class DiscussionTabHomePage(CoursePage, DiscussionPageMixin): ...@@ -352,12 +353,7 @@ class DiscussionTabHomePage(CoursePage, DiscussionPageMixin):
return self.q(css=".discussion-body section.home-header").present return self.q(css=".discussion-body section.home-header").present
def perform_search(self, text="dummy"): def perform_search(self, text="dummy"):
self.q(css=".discussion-body .sidebar .search").first.click() self.q(css=".forum-nav-search-input").fill(text + chr(10))
EmptyPromise(
lambda: self.q(css=".discussion-body .sidebar .search.is-open").present,
"waiting for search input to be available"
).fulfill()
self.q(css="#search-discussions").fill(text + chr(10))
EmptyPromise( EmptyPromise(
self.is_ajax_finished, self.is_ajax_finished,
"waiting for server to return result" "waiting for server to return result"
......
...@@ -509,7 +509,7 @@ class DiscussionSortPreferenceTest(UniqueCourseTest): ...@@ -509,7 +509,7 @@ class DiscussionSortPreferenceTest(UniqueCourseTest):
""" """
Test to check the default sorting preference of user. (Default = date ) Test to check the default sorting preference of user. (Default = date )
""" """
selected_sort = self.sort_page.get_selected_sort_preference_text() selected_sort = self.sort_page.get_selected_sort_preference()
self.assertEqual(selected_sort, "date") self.assertEqual(selected_sort, "date")
def test_change_sort_preference(self): def test_change_sort_preference(self):
...@@ -520,7 +520,7 @@ class DiscussionSortPreferenceTest(UniqueCourseTest): ...@@ -520,7 +520,7 @@ class DiscussionSortPreferenceTest(UniqueCourseTest):
for sort_type in ["votes", "comments", "date"]: for sort_type in ["votes", "comments", "date"]:
self.assertNotEqual(selected_sort, sort_type) self.assertNotEqual(selected_sort, sort_type)
self.sort_page.change_sort_preference(sort_type) self.sort_page.change_sort_preference(sort_type)
selected_sort = self.sort_page.get_selected_sort_preference_text() selected_sort = self.sort_page.get_selected_sort_preference()
self.assertEqual(selected_sort, sort_type) self.assertEqual(selected_sort, sort_type)
def test_last_preference_saved(self): def test_last_preference_saved(self):
...@@ -531,8 +531,8 @@ class DiscussionSortPreferenceTest(UniqueCourseTest): ...@@ -531,8 +531,8 @@ class DiscussionSortPreferenceTest(UniqueCourseTest):
for sort_type in ["votes", "comments", "date"]: for sort_type in ["votes", "comments", "date"]:
self.assertNotEqual(selected_sort, sort_type) self.assertNotEqual(selected_sort, sort_type)
self.sort_page.change_sort_preference(sort_type) self.sort_page.change_sort_preference(sort_type)
selected_sort = self.sort_page.get_selected_sort_preference_text() selected_sort = self.sort_page.get_selected_sort_preference()
self.assertEqual(selected_sort, sort_type) self.assertEqual(selected_sort, sort_type)
self.sort_page.refresh_page() self.sort_page.refresh_page()
selected_sort = self.sort_page.get_selected_sort_preference_text() selected_sort = self.sort_page.get_selected_sort_preference()
self.assertEqual(selected_sort, sort_type) self.assertEqual(selected_sort, sort_type)
...@@ -48,8 +48,12 @@ ...@@ -48,8 +48,12 @@
@import 'views/shoppingcart'; @import 'views/shoppingcart';
// applications // applications
@import 'discussion/discussion'; @import "discussion/utilities/variables";
@import 'discussion/discussion-developer'; @import 'discussion/discussion'; // Process old file after definitions but before everything else
@import "discussion/elements/navigation";
@import 'discussion/utilities/developer';
@import 'discussion/utilities/shame';
@import 'news'; @import 'news';
// temp - shame and developer // temp - shame and developer
......
...@@ -595,563 +595,6 @@ body.discussion { ...@@ -595,563 +595,6 @@ body.discussion {
box-shadow: none; box-shadow: none;
line-height: 1.4; line-height: 1.4;
.sidebar {
@include box-sizing(border-box);
float: left;
border: 1px solid #aaa;
border-right: 1px solid #bcbcbc;
border-radius: 3px;
width: 31%;
height: 550px;
background: #f6f6f6;
box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
}
.browse-search {
position: relative;
display: block;
border-bottom: 1px solid #a3a3a3;
border-radius: 3px 0 0 0;
height: 60px;
.home, .browse,
.search {
@include linear-gradient(top, rgba(255, 255, 255, .5), rgba(255, 255, 255, 0));
@include transition(all .2s ease-out);
position: relative;
float: left;
width: 20%;
height: 100%;
background-color: #dedede;
&:hover, &:focus {
background-color: $white;
}
}
.icon {
@include transition(all .2s ease-out);
z-index: 100;
display: inline-block;
width: 100%;
color: #aeaeae;
text-align: center;
font-size: 28px;
line-height: 60px;
opacity: 1;
}
.home {
border-radius: 3px 0 0 0;
box-shadow: -1px 0 0 #aaa inset;
cursor: pointer;
}
.home-icon {
width: 100%;
height: 100%;
display: block;
}
.browse {
border-radius: 3px 0 0 0;
box-shadow: -1px 0 0 #aaa inset;
&.is-open {
width:60%;
.browse-topic-drop-btn {
visibility: visible;
}
.browse-topic-drop-icon {
visibility: hidden;
}
&.is-dropped {
.browse-topic-drop-btn {
span {
color: $white;
text-shadow: none;
}
border-color: #4b4b4b;
}
}
}
&.is-dropped {
.browse-topic-drop-btn {
background-color: #616161;
}
}
}
.search {
cursor: pointer;
border-radius: 0 3px 0 0;
&.is-open {
cursor: auto;
width: 60%;
.home {
width:0%;
}
.post-search {
padding: 0 $baseline/2;
max-width: 1000px;
}
.post-search-field {
cursor: text;
pointer-events: auto;
&::-webkit-input-placeholder,
&:-moz-placeholder,
&:-ms-input-placeholder {
opacity: 1.0;
}
}
}
}
.browse-topic-drop-btn {
@include transition(none);
position: absolute;
top: -1px;
left: -1px;
display: block;
visibility: hidden;
overflow: hidden;
width: 100%;
height: 100%;
border: 1px solid transparent;
text-align: center;
span {
font-size: 14px;
font-weight: 700;
line-height: 58px;
color: #333;
text-shadow: 0 1px 0 rgba(255, 255, 255, .8);
}
.drop-arrow {
font-size: 16px;
}
}
.browse-topic-drop-icon {
display: block;
visibility: visible;
@include transition(none);
}
.browse-topic-drop-menu-wrapper {
display: none;
position: absolute;
top: 60px;
left: -1px;
z-index: 9999;
width: 100%;
background: #797979;
border: 1px solid #4b4b4b;
border-left: none;
border-radius: 0 0 3px 3px;
box-shadow: 1px 0 0 #4b4b4b inset;
.browse-topic-drop-menu {
max-height: 400px;
overflow-y: scroll;
}
ul {
position: inline;
}
> li:first-child a {
border-top: none;
}
a {
display: block;
padding: 0 $baseline;
border-top: 1px solid #5f5f5f;
font-size: 12px;
font-weight: 700;
line-height: 22px;
color: $white;
@include clearfix;
@include transition(none);
&.hidden {
display: none;
}
&:hover, &:focus {
background-color: #636363;
}
.board-name {
float: left;
width: 80%;
margin: 13px 0;
color: $white;
}
.unread {
float: right;
padding: 0 5px;
margin-top: 13px;
font-size: 11px;
line-height: 22px;
border-radius: 2px;
@include linear-gradient(top, #4c4c4c, #5a5a5a);
}
}
li li {
a {
padding-left: 44px;
background: url(../images/nested-icon.png) no-repeat 22px 14px;
}
}
li li li {
a {
padding-left: 68px;
background: url(../images/nested-icon.png) no-repeat 46px 14px;
}
}
}
.browse-topic-drop-search {
padding: $baseline/2;
}
.browse-topic-drop-search-input {
width: 100%;
height: 30px;
padding: 0 15px;
@include box-sizing(border-box);
border-radius: 30px;
border: 1px solid #333;
box-shadow: 0 1px 3px rgba(0, 0, 0, .25) inset;
background: -webkit-linear-gradient(top, #eee, $white);
font-size: 11px;
line-height: 16px;
color: #333;
}
.post-search {
width: 100%;
max-width: 30px;
margin: auto;
@include box-sizing(border-box);
@include transition(all .2s linear 0s);
}
.post-search-field {
display: block;
width: 100%;
height: 30px;
padding: 0 0 0 30px;
margin: 14px auto;
@include box-sizing(border-box);
border: 1px solid #acacac;
border-radius: 30px;
box-shadow: 0 1px 3px rgba(0, 0, 0, .1) inset, 0 1px 0 rgba(255, 255, 255, .5);
background: url(../images/search-icon.png) no-repeat 7px center #fff;
font-family: 'Open Sans', sans-serif;
font-weight: 400;
font-size: 13px;
line-height: 20px;
color: #333;
cursor: pointer;
pointer-events: none;
@include transition(all .2s ease-out 0s);
&::-webkit-input-placeholder,
&:-moz-placeholder,
&:-ms-input-placeholder {
opacity: 0.0;
@include transition(opacity .2s linear 0s);
}
&:focus {
border-color: #4697c1;
}
}
}
.sort-bar {
height: auto;
min-height: 27px;
border-bottom: 1px solid #a3a3a3;
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0));
background-color: #aeaeae;
box-shadow: 0 1px 0 rgba(255, 255, 255, .2) inset;
span,
a {
font-size: 9px;
font-weight: bold;
line-height: 25px;
color: #333;
text-transform: uppercase;
text-shadow: 0 1px 0 rgba(255, 255, 255, .4);
}
.sort-label {
display: block;
float: left;
margin: 0 $baseline/2;
}
li {
float: left;
margin: 4px 4px 0 0;
}
a {
display: block;
height: 18px;
padding: 0 9px;
border-radius: 19px;
color: #333;
line-height: 17px;
&:hover, &:focus {
@include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, .2));
color: #333;
}
&.active {
@include linear-gradient(top, rgba(0, 0, 0, .3), rgba(0, 0, 0, 0));
background-color: #999;
color: $white;
text-shadow: 0 -1px 0 rgba(0, 0, 0, .3);
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 1px rgba(0, 0, 0, .2) inset;
}
}
.group-filter-label {
width: 40px;
margin-left: $baseline/2;
}
.group-filter-select {
margin: 5px 0px 5px 5px;
width: 80px;
font-size:10px;
background: transparent;
border-color: #ccc;
}
}
.post-list-wrapper {
overflow-y: scroll;
overflow-x: hidden;
//border-right: 1px solid transparent;
}
.post-list {
background-color: #ddd;
.loading {
padding: ($baseline*.75) 0;
background: $gray-l6;
.loading-animation {
background-image: url(../images/spinner-on-grey.gif);
}
}
.more-pages a {
background: #eee;
font-size: 12px;
line-height: 33px;
text-align: center;
&:hover, &:focus {
background-image: none;
background-color: #e6e6e6;
}
}
a {
display: block;
position: relative;
float: left;
clear: both;
width: 100%;
padding: 0 ($baseline/2) 0 18px;
margin-bottom: 1px;
margin-right: -1px;
@include linear-gradient(top, rgba(255, 255, 255, .7), rgba(255, 255, 255, 0));
background-color: $white;
@include clearfix;
&:hover, &:focus {
@include linear-gradient(top, rgba(255, 255, 255, .7), rgba(255, 255, 255, 0));
background-color: #eee;
}
&.staff-post.staff-response {
.staff-post-icon {
top: 5px;
}
.staff-response-icon {
top: 18px;
}
}
.staff-post-icon,
.staff-response-icon {
position: absolute;
top: 11px;
left: 3px;
width: 13px;
height: 13px;
background: url(../images/staff-icons.png) no-repeat;
}
.staff-post-icon {
left: 2px;
background-position: 0 0;
}
.staff-response-icon {
background-position: -13px 0;
}
.title {
display: block;
float: left;
width: 70%;
margin: ($baseline/2) 0 ($baseline/2);
font-size: 13px;
font-weight: 700;
line-height: 1.4;
color: $dark-gray;
}
&.read {
background: #f2f2f2;
.title {
font-weight: 400;
color: #737373;
}
}
&.resolved:before {
content: '';
position: absolute;
top: 12px;
right: 75px;
width: 9px;
height: 8px;
background: url(../images/sidebar-resolved-icons.png) no-repeat;
}
&.followed:after {
content: '';
position: absolute;
top: 0;
right: 0;
width: 12px;
height: 12px;
background: url(../images/following-flag.png) no-repeat;
}
&.active {
@include linear-gradient(top, #96e0fd, #61c7fc);
border-color: #4697c1;
box-shadow: 0 1px 0 #4697c1, 0 -1px 0 #4697c1;
.title {
color: #333;
}
.staff-post-icon {
background-position: 0 -13px;
}
.staff-response-icon {
background-position: -13px -13px;
}
.comments-count {
@include linear-gradient(top, #3994c7, #4da7d3);
color: $white;
&:after {
background-position: 0 0;
}
}
&.followed:after {
background-position: 0 -12px;
}
&.resolved:before {
background-position: 0 -8px;
}
}
}
.votes-count,
.comments-count {
display: block;
float: right;
margin-top: 8px;
border-radius: 2px;
width: 36px;
height: 20px;
color: #767676;
text-align: center;
font-size: 11px;
line-height: 20px;
}
.comments-count {
@include linear-gradient(top, #d4d4d4, #dfdfdf);
position: relative;
margin-left: ($baseline/4);
margin-right:($baseline/4);
width: 28px;
font-weight: 700;
&:after {
content: '';
display: block;
position: absolute;
top: 20px;
right: 3px;
width: 5px;
height: 5px;
background: url(../images/comment-icon-bottoms.png) no-repeat;
background-position: 0 -5px;
}
&.unread {
@include linear-gradient(top, #84d7fe, #60a8d6);
color: #333;
&:after {
color: #99e0fe;
background-position: 0 0px;
}
}
}
}
.bottom-post-status { .bottom-post-status {
padding: 30px; padding: 30px;
font-size: 20px; font-size: 20px;
...@@ -1440,7 +883,7 @@ body.discussion { ...@@ -1440,7 +883,7 @@ body.discussion {
&.community-ta{ &.community-ta{
padding-top: 38px; padding-top: 38px;
border-color: #449944; border-color: $forum-color-community-ta;
} }
.staff-banner { .staff-banner {
...@@ -1468,7 +911,7 @@ body.discussion { ...@@ -1468,7 +911,7 @@ body.discussion {
padding: 1px 5px; padding: 1px 5px;
@include box-sizing(border-box); @include box-sizing(border-box);
border-radius: 2px 2px 0 0; border-radius: 2px 2px 0 0;
background: #449944; background: $forum-color-community-ta;
font-size: 9px; font-size: 9px;
font-weight: 700; font-weight: 700;
color: $white; color: $white;
...@@ -1672,7 +1115,7 @@ body.discussion { ...@@ -1672,7 +1115,7 @@ body.discussion {
margin-left: ($baseline/10); margin-left: ($baseline/10);
padding: 0 ($baseline/5); padding: 0 ($baseline/5);
border-radius: 2px; border-radius: 2px;
background: #449944; background: $forum-color-community-ta;
font-size: 9px; font-size: 9px;
font-weight: 700; font-weight: 700;
font-style: normal; font-style: normal;
......
.forum-nav {
@include box-sizing(border-box);
float: left;
border: 1px solid #aaa;
border-radius: 3px;
}
// ------
// Header
// ------
.forum-nav-header {
@include box-sizing(border-box);
display: table;
border-bottom: 1px solid $gray-l2;
background-color: $gray-l3;
}
.forum-nav-browse {
@include box-sizing(border-box);
display: table-cell;
vertical-align: middle;
width: 50%;
padding: ($baseline/4);
&:hover, &:focus, &.is-active {
background-color: $gray-l5;
}
.icon {
margin-right: ($baseline/4);
}
}
.forum-nav-browse-current {
@include font-size(12);
}
.forum-nav-browse-drop-arrow {
margin-left: ($baseline/4);
}
.forum-nav-search {
@include box-sizing(border-box);
display: table-cell;
position: relative;
vertical-align: middle;
width: 50%;
padding: ($baseline/4);
}
.forum-nav-search .icon {
@include font-size(12);
position: absolute;
margin-top: -6px;
top: 50%;
right: ($baseline/4 + 1px + $baseline / 4); // Wrapper padding + border + input padding
}
.forum-nav-search-input {
width: 100%;
}
// -----------
// Browse menu
// -----------
.forum-nav-browse-menu-wrapper {
overflow-y: scroll;
border-bottom: 1px solid $gray-l3;
background: $gray-l5;
}
.forum-nav-browse-filter {
position: relative;
border-bottom: 1px solid $gray-l2;
padding: ($baseline/4);
}
.forum-nav-browse-filter .icon {
@include font-size(12);
position: absolute;
margin-top: -6px;
top: 50%;
right: ($baseline/4 + 1px + $baseline / 4); // Wrapper padding + border + input padding
}
.forum-nav-browse-filter-input {
width: 100%;
}
.forum-nav-browse-title .icon {
margin-right: ($baseline/2);
}
// -------------------
// Sort and filter bar
// -------------------
.forum-nav-refine-bar {
@include clearfix();
@include font-size(11);
border-bottom: 1px solid $gray-l3;
background-color: $gray-l5;
padding: ($baseline/4) ($baseline/2);
color: $black;
}
%forum-nav-select {
border: none;
max-width: 100%;
background-color: transparent;
font: inherit;
}
.forum-nav-filter-cohort-control {
@extend %forum-nav-select;
}
.forum-nav-sort {
float: right;
}
.forum-nav-sort-control {
@extend %forum-nav-select;
}
// -----------
// Thread list
// -----------
.forum-nav-thread-list {
overflow-y: scroll;
}
.forum-nav-thread {
border-bottom: 1px solid $gray-l3;
}
.forum-nav-thread-link {
@include clearfix();
}
%forum-nav-thread-wrapper {
display: inline-block;
vertical-align: middle;
}
.forum-nav-thread-wrapper-1 {
@extend %forum-nav-thread-wrapper;
width: 70%;
}
.forum-nav-thread-wrapper-2 {
@extend %forum-nav-thread-wrapper;
width: 30%;
text-align: right;
}
.forum-nav-thread-title {
@extend %t-title7;
display: block;
}
%forum-nav-thread-label {
@extend %t-weight4;
@include font-size(9);
display: inline;
border: 1px solid;
border-radius: 3px;
text-transform: uppercase;
white-space: nowrap;
&:last-child {
margin-right: 0;
}
.icon {
margin-right: ($baseline/5);
}
}
.forum-nav-thread-label-pinned {
@extend %forum-nav-thread-label;
border-color: $forum-color-pinned;
color: $forum-color-pinned;
}
.forum-nav-thread-label-following {
@extend %forum-nav-thread-label;
border-color: $forum-color-following;
color: $forum-color-following;
}
.forum-nav-thread-label-staff {
@extend %forum-nav-thread-label;
border-color: $forum-color-staff;
color: $forum-color-staff;
}
.forum-nav-thread-label-community-ta {
@extend %forum-nav-thread-label;
border-color: $forum-color-community-ta;
color: $forum-color-community-ta;
}
%forum-nav-thread-wrapper-2-content {
@include font-size(11);
display: inline-block;
margin-right: ($baseline/4);
text-align: center;
color: $black;
&:last-child {
margin-right: 0;
}
}
.forum-nav-thread-endorsed {
@extend %forum-nav-thread-wrapper-2-content;
color: $green-d1;
}
.forum-nav-thread-votes-count {
@extend %forum-nav-thread-wrapper-2-content;
}
.forum-nav-thread-comments-count {
@extend %forum-nav-thread-wrapper-2-content;
@extend %t-weight4;
position: relative;
margin-left: ($baseline/4);
margin-bottom: ($baseline/4); // Because tail is position: absolute
border-radius: 2px;
padding: ($baseline/10) ($baseline/5);
min-width: 2em; // Fit most comment counts but allow expansion if necessary
background-color: $gray-l3;
// Speech bubble tail
&:after {
content: '';
display: block;
position: absolute;
bottom: (-$baseline/4);
right: ($baseline/4);
width: 0;
height: 0;
border-style: solid;
border-width: 0 ($baseline/4) ($baseline/4) 0;
border-color: transparent $gray-l3 transparent transparent;
}
&.is-unread {
background-color: $white;
&:after {
border-right-color: $white
}
}
}
.forum-nav-thread.is-unread .forum-nav-thread-comments-count {
background-color: $blue;
color: $white;
&:after {
border-right-color: $blue;
}
}
%forum-nav-load-more-content {
text-align: center;
}
.forum-nav-load-more-link {
@extend %forum-nav-load-more-content;
color: $link-color;
}
.forum-nav-loading {
@extend %forum-nav-load-more-content;
}
// -------------------
// navigation - header
// -------------------
// Override global a rules
.forum-nav-browse {
color: $black !important;
}
// Override global label rules
.forum-nav-search label {
margin-bottom: 0;
}
// Override global input rules
.forum-nav-search-input {
box-shadow: none !important;
border: 1px solid $gray-l2 !important;
border-radius: 3px !important;
height: auto !important;
padding-left: ($baseline/4) !important;
padding-right: ($baseline/2 + 12px) !important; // Leave room for icon
font-size: 12px !important;
}
// Firefox does not compute the correct containing box for absolute positioning
// of .forum-nav-search .icon, so there's an extra div to make it happy
.forum-nav-search-ff-position-fix {
position: relative;
}
// The sidebar class does a lot of things that we don't want in the thread list;
// the following rules contain styling that is necessary and would otherwise
// reside in elements/_navigation.scss if the sidebar styling did not make the
// !important directive necessary.
.forum-nav {
width: 31% !important;
}
// ------------------------
// navigation - browse menu
// ------------------------
// Override global a rules
.forum-nav-browse-title {
color: inherit !important;
}
// Override global label rules
.forum-nav-browse-filter label {
margin-bottom: 0;
}
// Override global input rules
.forum-nav-browse-filter-input {
box-shadow: none !important;
border-radius: 3px !important;
height: auto !important;
padding-left: ($baseline/4) !important;
padding-right: ($baseline/2 + 12px) !important; // Leave room for icon
font-size: 12px !important;
}
// The sidebar class does a lot of things that we don't want in the thread list;
// the following rules contain styling that is necessary and would otherwise
// reside in elements/_navigation.scss if the sidebar styling did not make the
// !important directive necessary.
.forum-nav-browse-title {
border-bottom: 1px solid $gray-l3 !important;
padding: ($baseline/2) ($baseline/2) !important;
&:hover, &:focus {
background: $forum-color-active-thread !important;
}
}
.forum-nav-browse-submenu {
padding-left: $baseline !important;
}
// --------------------------------
// navigation - sort and filter bar
// --------------------------------
// Override global span rules
.forum-nav-sort-label {
color: inherit;
}
// --------------------------------
// navigation - thread list
// --------------------------------
// The sidebar class does a lot of things that we don't want in the thread list;
// the following rules contain styling that is necessary and would otherwise
// reside in elements/_navigation.scss if the sidebar styling did not make the
// !important directive necessary.
.forum-nav-thread {
background-color: $gray-l5 !important;
&.is-unread {
background-color: $white !important;
}
}
.forum-nav-thread-link {
padding: ($baseline/4) ($baseline/2) !important;
&.is-active, &:hover, &:focus {
background-color: $forum-color-active-thread !important;
}
}
li[class*=forum-nav-thread-label-] {
margin-top: ($baseline/4) !important;
padding: 1px 6px !important;
}
.forum-nav-load-more {
border-bottom: 1px solid $gray-l3 !important;
background-color: $gray-l5 !important;
}
.forum-nav-load-more-link {
&:hover, &:focus {
color: $link-color !important;
background-color: $forum-color-active-thread !important;
}
}
.forum-nav-load-more-link, .forum-nav-loading {
padding: $baseline 0 !important;
}
// The following rules would be unnecessary but for broadly scoped rules defined
// elsewhere in our CSS.
li[class*=forum-nav-thread-label-] {
// Override global span rules
span {
color: inherit;
}
// Override clearfix stuff in .sidebar ul li rules
&:before, &:after {
display: none !important;
}
}
$forum-color-active-thread: tint($blue, 85%);
$forum-color-pinned: $pink;
$forum-color-following: $blue;
$forum-color-staff: $blue;
$forum-color-community-ta: $green-d1;
...@@ -12,41 +12,43 @@ ...@@ -12,41 +12,43 @@
</%def> </%def>
<%def name="render_entry(entries, entry)"> <%def name="render_entry(entries, entry)">
<li><a href="#" class="drop-menu-entry"><span class="board-name" data-discussion_id='${json.dumps(entries[entry])}' cohorted = "${str(entries[entry]['is_cohorted']).lower()}">${entry}</span></a></li> <li
class="forum-nav-browse-menu-item"
data-discussion-id='${json.dumps(entries[entry])}'
data-cohorted="${str(entries[entry]['is_cohorted']).lower()}"
>
<a href="#" class="forum-nav-browse-title">${entry}</a>
</li>
</%def> </%def>
<%def name="render_category(categories, category)"> <%def name="render_category(categories, category)">
<li> <li class="forum-nav-browse-menu-item">
<a href="#" class="drop-menu-parent-category"><span class="board-name">${category}</span></a> <a href="#" class="forum-nav-browse-title">${category}</a>
<ul> <ul class="forum-nav-browse-submenu">
${render_dropdown(categories[category])} ${render_dropdown(categories[category])}
</ul> </ul>
</li> </li>
</%def> </%def>
<div class="browse-topic-drop-menu-wrapper"> <div class="forum-nav-browse-menu-wrapper" style="display: none">
<div class="browse-topic-drop-search"> <form class="forum-nav-browse-filter">
<label class="sr" for="browse-topic">${_("Filter Topics")}</label> <label>
<input type="text" id="browse-topic" class="browse-topic-drop-search-input" placeholder="${_('filter topics')}"> <span class="sr">${_("Filter Topics")}</span>
</div> <input type="text" class="forum-nav-browse-filter-input" placeholder="${_("filter topics")}">
<ul class="browse-topic-drop-menu"> <i class="icon icon-filter"></i>
<li> </label>
<a href="#" class="drop-menu-meta-category"> </form>
<span class="board-name" data-discussion_id='#all'>${_("Show All Discussions")}</span> <ul class="forum-nav-browse-menu">
</a> <li class="forum-nav-browse-menu-item forum-nav-browse-menu-all">
<a href="#" class="forum-nav-browse-title">${_("All Discussions")}</a>
</li> </li>
%if flag_moderator: %if flag_moderator:
<li> <li class="forum-nav-browse-menu-item forum-nav-browse-menu-flagged">
<a href="#"> <a href="#" class="forum-nav-browse-title"><i class="icon icon-flag"></i>${_("Flagged Discussions")}</a>
<span class="board-name" data-discussion_id='#flagged'><i class="icon-flag" style="padding-right:5px;"></i>${_("Show Flagged Discussions")}</span>
</a>
</li> </li>
%endif %endif
<li> <li class="forum-nav-browse-menu-item forum-nav-browse-menu-following">
<a href="#" class="drop-menu-meta-category"> <a href="#" class="forum-nav-browse-title"><i class="icon icon-star"></i>${_("Posts I'm Following")}</a>
<span class="board-name" data-discussion_id='#following'><i class="icon-star" style="padding-right:5px;"></i>${_("Posts I'm Following")}</span>
</a>
</li> </li>
${render_dropdown(category_map)} ${render_dropdown(category_map)}
</ul> </ul>
......
<%! from django.utils.translation import ugettext as _ %> <%! from django.utils.translation import ugettext as _ %>
<script type="text/template" id="thread-list-template"> <script type="text/template" id="thread-list-template">
<div class="browse-search"> <div class="forum-nav-header">
<div class="home"> <a href="#" class="forum-nav-browse" aria-haspopup="true">
<a href="#" class="home-icon"> ## There is no whitespace between these because the front-end JS code
<i class="icon icon-home"></i> ## needs to precisely compute the available width for forum-nav-
<span class="sr">${_("Discussion Home")}</span> ## browse-current in order to do truncation of topic names.
</a> <i class="icon icon-reorder"></i><span class="sr">${_("Discussion topics; current selection is: ")}</span><span class="forum-nav-browse-current">${_("All Discussions")}</span><span class="forum-nav-browse-drop-arrow">▾</span>
</div> </a>
<div class="browse is-open"> <form class="forum-nav-search">
<a href="#" class="browse-topic-drop-icon"> <div class="forum-nav-search-ff-position-fix">
<i class="icon icon-reorder"></i> <label>
<span class="sr">${_("Discussion Topics")}</span> <span class="sr">${_("Search")}</span>
</a> <input class="forum-nav-search-input" type="text" placeholder="${_("Search all posts")}">
<a href="#" class="browse-topic-drop-btn" aria-haspopup="true" aria-owns="browse-topic-drop-menu"> <i class="icon icon-search"></i>
<span class="sr">${_("Discussion topics; current selection is: ")}</span> </label>
<span class="current-board">${_("Show All Discussions")}</span> </div>
<span class="drop-arrow" aria-hidden="true"></span> </form>
</a>
</div>
<%include file="_filter_dropdown.html" />
<div class="search">
<form class="post-search">
<label class="sr" for="search-discussions">${_("Search")}</label>
<input type="text" id="search-discussions" placeholder="${_("Search all discussions")}" class="post-search-field">
</form>
</div>
</div> </div>
<div class="sort-bar"> <%include file="_filter_dropdown.html" />
<span class="sort-label" id="sort-label">${_("Sort by:")}</span> <div class="forum-nav-thread-list-wrapper">
<ul role="radiogroup" aria-labelledby="sort-label"> <div class="forum-nav-refine-bar">
<li><a href="#" role="radio" aria-checked="false" data-sort="date">${_("date")}</a></li> %if is_course_cohorted and is_moderator:
<li><a href="#" role="radio" aria-checked="false" data-sort="votes">${_("votes")}</a></li> <span class="forum-nav-filter-cohort">
<li><a href="#" role="radio" aria-checked="false" data-sort="comments">${_("comments")}</a></li> <select class="forum-nav-filter-cohort-control">
</ul> <option value="all">${_("View all cohorts")}</option>
%for c in cohorts:
<option value="${c['id']}">${_("View as {cohort_name}").format(cohort_name=c['name'])}</option>
%endfor
</select>
</span>
%endif
%if is_course_cohorted and is_moderator: <span class="forum-nav-sort">
<span class="group-filter-label cohort">${_("Show:")}</span> <select class="forum-nav-sort-control">
<select class="group-filter-select cohort-options cohort"> ## Translators: This is a menu option for sorting forum threads
<option value="all">${_("View All")}</option> <option value="date">${_("by recent activity")}</option>
%for c in cohorts: ## Translators: This is a menu option for sorting forum threads
<option value="${c['id']}">${_("View as {name}").format(name=c['name'])}</option> <option value="comments">${_("by most activity")}</option>
%endfor ## Translators: This is a menu option for sorting forum threads
</select> <option value="votes">${_("by most votes")}</option>
%endif </select>
</div> </span>
<div class="search-alerts"></div> </div>
<div class="post-list-wrapper"> <div class="search-alerts"></div>
<ul class="post-list"> <ul class="forum-nav-thread-list"></ul>
</ul>
</div> </div>
</script> </script>
...@@ -205,16 +205,67 @@ ...@@ -205,16 +205,67 @@
</script> </script>
<script aria-hidden="true" type="text/template" id="thread-list-item-template"> <script aria-hidden="true" type="text/template" id="thread-list-item-template">
<a href="${'<%- id %>'}" data-id="${'<%- id %>'}"> <li data-id="${'<%- id %>'}" class="forum-nav-thread${'<% if (typeof(read) != "undefined" && !read) { %> is-unread<% } %>'}">
<span class="title">${"<%- title %>"}</span> <a href="#" class="forum-nav-thread-link">
<div class="forum-nav-thread-wrapper-1">
<span class="forum-nav-thread-title">${"<%- title %>"}</span>
<% <%
js_block = u"""
var labels = "";
if (pinned) {{
labels += '<li class="forum-nav-thread-label-pinned"><i class="icon icon-pushpin"></i>{pinned_text}</li> ';
}}
if (typeof(subscribed) != "undefined" && subscribed) {{
labels += '<li class="forum-nav-thread-label-following"><i class="icon icon-star"></i>{following_text}</li> ';
}}
if (staff_authored) {{
labels += '<li class="forum-nav-thread-label-staff"><i class="icon icon-user"></i>{staff_text}</li> ';
}}
if (community_ta_authored) {{
labels += '<li class="forum-nav-thread-label-community-ta"><i class="icon icon-user"></i>{community_ta_text}</li> ';
}}
if (labels != "") {{
print('<ul class="forum-nav-thread-labels">' + labels + '</ul>');
}}
""".format(
## Translators: This is a label for a forum thread that has been pinned
pinned_text=escapejs(_("Pinned")),
## Translators: This is a label for a forum thread that the user is subscribed to
following_text=escapejs(_("Following")),
## Translators: This is a label for a forum thread that was authored by a member of the course staff
staff_text=escapejs(_("By: Staff")),
## Translators: This is a label for a forum thread that was authored by a community TA
community_ta_text=escapejs(_("By: Community TA"))
)
%>
${"<%"}${js_block}${"%>"}
</div><div class="forum-nav-thread-wrapper-2">
${"<% if (endorsed) { %>"}
## Translators: This is a label for a forum thread with a response that was endorsed by the course staff
<span class="forum-nav-thread-endorsed"><i class="icon icon-ok"></i><span class="sr">${_("Endorsed response")}</span></span>
${"<% } %>"}
<%
js_block = u"""
interpolate(
'{}',
{{'span_sr_open': '<span class=\"sr\">', 'span_close': '</span>', 'votes_up_count': votes['up_count']}},
true
)
""".format(
## Translators: 'votes_up_count' is a numerical placeholder for a specific discussion thread; 'span_*' placeholders refer to HTML markup. Please translate the word 'votes'.
escapejs( _('%(votes_up_count)s%(span_sr_open)s votes %(span_close)s'))
)
%>
<span class="forum-nav-thread-votes-count">+${'<%='}${js_block}${'%>'}</span>
<%
js_block = u""" js_block = u"""
var fmt; var fmt;
// Counts in data do not include the post itself, but the UI should
var data = {{ var data = {{
'span_sr_open': '<span class=\"sr\">', 'span_sr_open': '<span class=\"sr\">',
'span_close': '</span>', 'span_close': '</span>',
'unread_comments_count': unread_comments_count, 'unread_comments_count': unread_comments_count + (read ? 0 : 1),
'comments_count': comments_count 'comments_count': comments_count + 1
}}; }};
if (unread_comments_count > 0) {{ if (unread_comments_count > 0) {{
fmt = '{markup_with_unread}'; fmt = '{markup_with_unread}';
...@@ -229,24 +280,14 @@ ...@@ -229,24 +280,14 @@
markup_none_unread=escapejs(_('%(comments_count)s %(span_sr_open)scomments %(span_close)s')) markup_none_unread=escapejs(_('%(comments_count)s %(span_sr_open)scomments %(span_close)s'))
) )
%> %>
<span class="comments-count"> <span class="forum-nav-thread-comments-count ${'<% if (unread_comments_count > 0) { %>is-unread<% } %>'}">
${'<%'}${js_block}${'%>'} ${'<%'}${js_block}${'%>'}
</span> </span>
<% </div>
js_block = u"""
interpolate(
'{}',
{{'span_sr_open': '<span class=\"sr\">', 'span_close': '</span>', 'votes_up_count': votes['up_count']}},
true
)
""".format(
## Translators: 'votes_up_count' is a numerical placeholder for a specific discussion thread; 'span_*' placeholders refer to HTML markup. Please translate the word 'votes'.
escapejs( _('%(votes_up_count)s%(span_sr_open)s votes %(span_close)s'))
)
%>
<span class="votes-count">+${'<%='}${js_block}${'%>'}</span>
</a> </a>
</li>
</script> </script>
<script aria-hidden="true" type="text/template" id="discussion-home"> <script aria-hidden="true" type="text/template" id="discussion-home">
<div class="discussion-article blank-slate"> <div class="discussion-article blank-slate">
<section class="home-header"> <section class="home-header">
......
...@@ -37,7 +37,7 @@ ...@@ -37,7 +37,7 @@
data-user-cohort-id="${user_cohort}" data-user-cohort-id="${user_cohort}"
data-course-settings="${course_settings}"> data-course-settings="${course_settings}">
<div class="discussion-body"> <div class="discussion-body">
<div class="sidebar"></div> <div class="sidebar forum-nav"></div>
<div class="discussion-column"> <div class="discussion-column">
</div> </div>
</div> </div>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment