TNL-171: Change topic of a previously posted post.

This commit is contained in:
polesye
2014-09-15 10:52:20 +03:00
parent 3cdfdae8bb
commit 00c7e60e07
22 changed files with 662 additions and 398 deletions

View File

@@ -0,0 +1,2 @@
!views/discussion_thread_edit_view.js
!views/discussion_topic_menu_view.js

View File

@@ -99,7 +99,13 @@ if Backbone?
@newPostForm = $('.new-post-article')
@threadviews = @discussion.map (thread) ->
new DiscussionThreadView el: @$("article#thread_#{thread.id}"), model: thread, mode: "inline"
new DiscussionThreadView(
el: @$("article#thread_#{thread.id}"),
model: thread,
mode: "inline",
course_settings: @course_settings,
topicId: discussionId
)
_.each @threadviews, (dtv) -> dtv.render()
DiscussionUtil.bulkUpdateContentInfo(window.$$annotated_content_info)
@newPostView = new NewPostView(
@@ -123,7 +129,14 @@ if Backbone?
# 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 DiscussionThreadView el: article, model: thread, mode: "inline"
threadView = new DiscussionThreadView(
el: article,
model: thread,
mode: "inline",
course_settings: @course_settings,
topicId: @$el.data("discussion-id")
)
threadView.render()
@threadviews.unshift threadView

View File

@@ -54,7 +54,12 @@ if Backbone?
if(@newPost.is(":visible"))
@newPost.fadeOut()
@main = new DiscussionThreadView(el: $(".forum-content"), model: @thread, mode: "tab")
@main = new DiscussionThreadView(
el: $(".forum-content"),
model: @thread,
mode: "tab",
course_settings: @course_settings,
)
@main.render()
@main.on "thread:responses:rendered", =>
@nav.updateSidebar()

View File

@@ -1,25 +0,0 @@
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"
@
update: (event) ->
@trigger "thread:update", event
cancel_edit: (event) ->
@trigger "thread:cancel_edit", event

View File

@@ -0,0 +1,103 @@
(function(Backbone) {
'use strict';
if (Backbone) {
this.DiscussionThreadEditView = Backbone.View.extend({
tagName: 'form',
events: {
'submit': 'updateHandler',
'click .post-cancel': 'cancelHandler'
},
attributes: {
'class': 'discussion-post edit-post-form'
},
initialize: function(options) {
this.container = options.container || $('.thread-content-wrapper');
this.mode = options.mode || 'inline';
this.course_settings = options.course_settings;
this.topicId = options.topicId;
_.bindAll(this);
return this;
},
render: function() {
this.template = _.template($('#thread-edit-template').html());
this.$el.html(this.template(this.model.toJSON())).appendTo(this.container);
this.submitBtn = this.$('.post-update');
if (this.isTabMode()) {
this.topicView = new DiscussionTopicMenuView({
topicId: this.topicId,
course_settings: this.course_settings
});
this.addField(this.topicView.render());
}
DiscussionUtil.makeWmdEditor(this.$el, $.proxy(this.$, this), 'edit-post-body');
return this;
},
addField: function(fieldView) {
this.$('.forum-edit-post-form-wrapper').append(fieldView);
return this;
},
isTabMode: function () {
return this.mode === 'tab';
},
save: function() {
var title = this.$('.edit-post-title').val(),
body = this.$('.edit-post-body textarea').val(),
commentableId = this.isTabMode() ? this.topicView.getCurrentTopicId() : null;
return DiscussionUtil.safeAjax({
$elem: this.submitBtn,
$loading: this.submitBtn,
url: DiscussionUtil.urlFor('update_thread', this.model.id),
type: 'POST',
dataType: 'json',
async: false, // @TODO when the rest of the stuff below is made to work properly..
data: {
title: title,
body: body,
commentable_id: commentableId
},
error: DiscussionUtil.formErrorHandler(this.$('.post-errors')),
success: function() {
var newAttrs = {
title: title,
body: body
};
// @TODO: Move this out of the callback, this makes it feel sluggish
this.$('.edit-post-title').val('').attr('prev-text', '');
this.$('.edit-post-body textarea').val('').attr('prev-text', '');
this.$('.wmd-preview p').html('');
if (this.isTabMode()) {
_.extend(newAttrs, {
commentable_id: commentableId,
courseware_title: this.topicView.getFullTopicName()
});
}
this.model.set(newAttrs).unset('abbreviatedBody');
this.trigger('thread:updated');
}.bind(this)
});
},
updateHandler: function(event) {
event.preventDefault();
// this event is for the moment triggered and used nowhere.
this.trigger('thread:update', event);
this.save();
return this;
},
cancelHandler: function(event) {
event.preventDefault();
this.trigger("thread:cancel_edit", event);
this.remove();
return this;
}
});
}
}).call(this, Backbone);

View File

@@ -5,7 +5,7 @@ if Backbone?
"keypress .forum-nav-browse-filter-input": (event) => DiscussionUtil.ignoreEnterKey(event)
"keyup .forum-nav-browse-filter-input": "filterTopics"
"click .forum-nav-browse-menu-wrapper": "ignoreClick"
"click .forum-nav-browse-title": "selectTopic"
"click .forum-nav-browse-title": "selectTopicHandler"
"keydown .forum-nav-search-input": "performSearch"
"change .forum-nav-sort-control": "sortThreads"
"click .forum-nav-thread-link": "threadSelected"
@@ -130,12 +130,12 @@ if Backbone?
)
@$(".forum-nav-sort-control").val(@collection.sort_preference)
$(window).bind "load", @updateSidebar
$(window).bind "scroll", @updateSidebar
$(window).bind "resize", @updateSidebar
$(window).bind "load scroll resize", @updateSidebar
@displayedCollection.on "reset", @renderThreads
@displayedCollection.on "thread:remove", @renderThreads
@displayedCollection.on "change:commentable_id", (model, commentable_id) =>
@retrieveDiscussions @discussionIds.split(",") if @mode is "commentables"
@renderThreads()
@
@@ -185,7 +185,7 @@ if Backbone?
when 'search'
options.search_text = @current_search
if @group_id
options.group_id = @group_id
options.group_id = @group_id
when 'followed'
options.user_id = window.user.id
options.group_id = "all"
@@ -196,8 +196,7 @@ if Backbone?
when 'all'
if @group_id
options.group_id = @group_id
lastThread = @collection.last()?.get('id')
if lastThread
# Pagination; focus the first thread after what was previously the last thread
@@ -262,7 +261,7 @@ if Backbone?
else
$('input.email-setting').removeAttr('checked')
thread_id = null
@trigger("thread:removed")
@trigger("thread:removed")
#select all threads
isBrowseMenuVisible: =>
@@ -359,12 +358,15 @@ if Backbone?
name = prefix + rawName + gettext("")
return name
selectTopic: (event) ->
selectTopicHandler: (event) ->
event.preventDefault()
@selectTopic $(event.target)
selectTopic: ($target) ->
@hideBrowseMenu()
@clearSearch()
item = $(event.target).closest('.forum-nav-browse-menu-item')
item = $target.closest('.forum-nav-browse-menu-item')
@setCurrentTopicDisplay(@getPathText(item))
if item.hasClass("forum-nav-browse-menu-all")
@discussionIds = ""
@@ -388,7 +390,7 @@ if Backbone?
chooseCohort: (event) =>
@group_id = @$('.forum-nav-filter-cohort-control :selected').val()
@retrieveFirstPage()
retrieveDiscussion: (discussion_id, callback=null) ->
url = DiscussionUtil.urlFor("retrieve_discussion", discussion_id)
DiscussionUtil.safeAjax
@@ -403,7 +405,7 @@ if Backbone?
if callback?
callback()
retrieveDiscussions: (discussion_ids) ->
@discussionIds = discussion_ids.join(',')
@mode = 'commentables'

View File

@@ -21,6 +21,13 @@ if Backbone?
@mode = options.mode or "inline" # allowed values are "tab" or "inline"
if @mode not in ["tab", "inline"]
throw new Error("invalid mode: " + @mode)
# Quick fix to have an actual model when we're receiving new models from
# the server.
@model.collection.on "reset", (collection) =>
id = @model.get("id")
@model = collection.get(id) if collection.get(id)
@createShowView()
@responses = new Comments()
@loadedResponses = false
@@ -254,49 +261,20 @@ if Backbone?
@createEditView()
@renderEditView()
update: (event) =>
newTitle = @editView.$(".edit-post-title").val()
newBody = @editView.$(".edit-post-body textarea").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
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.$(".wmd-preview p").html("")
@model.set
title: newTitle
body: newBody
@model.unset("abbreviatedBody")
@createShowView()
@renderShowView()
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
@editView = new DiscussionThreadEditView(
container: @$('.thread-content-wrapper')
model: @model
mode: @mode
course_settings: @options.course_settings
topicId: @model.get('commentable_id')
)
@editView.bind "thread:updated thread:cancel_edit", @closeEditView
renderSubView: (view) ->
view.setElement(@$('.thread-content-wrapper'))
@@ -304,15 +282,9 @@ if Backbone?
view.delegateEvents()
renderEditView: () ->
@renderSubView(@editView)
@editView.render()
createShowView: () ->
if @editView?
@editView.undelegateEvents()
@editView.$el.empty()
@editView = null
@showView = new DiscussionThreadShowView({model: @model, mode: @mode})
@showView.bind "thread:_delete", @_delete
@showView.bind "thread:edit", @edit
@@ -320,8 +292,7 @@ if Backbone?
renderShowView: () ->
@renderSubView(@showView)
cancelEdit: (event) =>
event.preventDefault()
closeEditView: (event) =>
@createShowView()
@renderShowView()

View File

@@ -0,0 +1,193 @@
(function(Backbone) {
'use strict';
if (Backbone) {
this.DiscussionTopicMenuView = Backbone.View.extend({
events: {
'click .post-topic-button': 'toggleTopicDropdown',
'click .topic-menu-wrapper': 'handleTopicEvent',
'click .topic-filter-label': 'ignoreClick',
'keyup .topic-filter-input': this.DiscussionFilter.filterDrop
},
attributes: {
'class': 'post-field'
},
initialize: function(options) {
this.course_settings = options.course_settings;
this.currentTopicId = options.topicId;
this.maxNameWidth = 100;
_.bindAll(this);
return this;
},
/**
* When the menu is expanded, a click on the body element (outside of the menu) or on a menu element
* should close the menu except when the target is the search field. To accomplish this, we have to ignore
* clicks on the search field by stopping the propagation of the event.
*/
ignoreClick: function(event) {
event.stopPropagation();
return this;
},
render: function() {
var context = _.clone(this.course_settings.attributes);
context.topics_html = this.renderCategoryMap(this.course_settings.get('category_map'));
this.$el.html(_.template($('#topic-template').html(), context));
this.dropdownButton = this.$('.post-topic-button');
this.topicMenu = this.$('.topic-menu-wrapper');
this.selectedTopic = this.$('.js-selected-topic');
this.hideTopicDropdown();
if (this.getCurrentTopicId()) {
this.setTopic(this.$('a.topic-title').filter('[data-discussion-id=' + this.getCurrentTopicId() + ']'));
} else {
this.setTopic(this.$('a.topic-title').first());
}
return this.$el;
},
renderCategoryMap: function(map) {
var category_template = _.template($('#new-post-menu-category-template').html()),
entry_template = _.template($('#new-post-menu-entry-template').html());
return _.map(map.children, function(name) {
var html = '', entry;
if (_.has(map.entries, name)) {
entry = map.entries[name];
html = entry_template({
text: name,
id: entry.id,
is_cohorted: entry.is_cohorted
});
} else { // subcategory
html = category_template({
text: name,
entries: this.renderCategoryMap(map.subcategories[name])
});
}
return html;
}, this).join('');
},
toggleTopicDropdown: function(event) {
event.preventDefault();
event.stopPropagation();
if (this.menuOpen) {
this.hideTopicDropdown();
} else {
this.showTopicDropdown();
}
return this;
},
showTopicDropdown: function() {
this.menuOpen = true;
this.dropdownButton.addClass('dropped');
this.topicMenu.show();
$(document.body).on('click.topicMenu', this.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
this.maxNameWidth = this.dropdownButton.width() - 40;
return this;
},
hideTopicDropdown: function() {
this.menuOpen = false;
this.dropdownButton.removeClass('dropped');
this.topicMenu.hide();
$(document.body).off('click.topicMenu');
return this;
},
handleTopicEvent: function(event) {
event.preventDefault();
event.stopPropagation();
this.setTopic($(event.target));
return this;
},
setTopic: function($target) {
if ($target.data('discussion-id')) {
this.topicText = this.getFullTopicName($target);
this.currentTopicId = $target.data('discussion-id');
this.setSelectedTopicName(this.topicText);
this.trigger('thread:topic_change', $target);
this.hideTopicDropdown();
}
return this;
},
getCurrentTopicId: function() {
return this.currentTopicId;
},
setSelectedTopicName: function(text) {
return this.selectedTopic.html(this.fitName(text));
},
/**
* Return full name for the `topicElement` if it is passed.
* Otherwise, full name for the current topic will be returned.
* @param {jQuery Element} [topicElement]
* @return {String}
*/
getFullTopicName: function(topicElement) {
var name;
if (topicElement) {
name = topicElement.html();
_.each(topicElement.parents('.topic-submenu'), function(item) {
name = $(item).siblings('.topic-title').text() + ' / ' + name;
});
return name;
} else {
return this.topicText;
}
},
// @TODO move into utils.coffee
getNameWidth: function(name) {
var test = $('<div>'),
width;
test.css({
'font-size': this.dropdownButton.css('font-size'),
'opacity': 0,
'position': 'absolute',
'left': -1000,
'top': -1000
}).html(name).appendTo(document.body);
width = test.width();
test.remove();
return width;
},
// @TODO move into utils.coffee
fitName: function(name) {
var ellipsisText = gettext('…'),
partialName, path, rawName;
if (this.getNameWidth(name) < this.maxNameWidth) {
return name;
} else {
path = _.map(name.split('/'), function(item){
return item.replace(/^\s+|\s+$/g, '');
});
while (path.length > 1) {
path.shift();
partialName = ellipsisText + ' / ' + path.join(' / ');
if (this.getNameWidth(partialName) < this.maxNameWidth) {
return partialName;
}
}
rawName = path[0];
name = ellipsisText + ' / ' + rawName;
while (this.getNameWidth(name) > this.maxNameWidth) {
rawName = rawName.slice(0, -1);
name = ellipsisText + ' / ' + rawName + ' ' + ellipsisText;
}
}
return name;
}
});
}
}).call(this, Backbone);

View File

@@ -6,7 +6,6 @@ if Backbone?
if @mode not in ["tab", "inline"]
throw new Error("invalid mode: " + @mode)
@course_settings = options.course_settings
@maxNameWidth = 100
@topicId = options.topicId
render: () ->
@@ -16,29 +15,21 @@ if Backbone?
mode: @mode,
form_id: @mode + (if @topicId then "-" + @topicId else "")
})
context.topics_html = @renderCategoryMap(@course_settings.get("category_map")) if @mode is "tab"
@$el.html(_.template($("#new-post-template").html(), context))
if @mode is "tab"
# set up the topic dropdown in tab mode
@dropdownButton = @$(".post-topic-button")
@topicMenu = @$(".topic-menu-wrapper")
@hideTopicDropdown()
@setTopic(@$("a.topic-title").first())
if @isTabMode()
@topicView = new DiscussionTopicMenuView {
topicId: @topicId
course_settings: @course_settings
}
@topicView.on('thread:topic_change', @toggleGroupDropdown)
@addField(@topicView.render())
DiscussionUtil.makeWmdEditor @$el, $.proxy(@$, @), "js-post-body"
renderCategoryMap: (map) ->
category_template = _.template($("#new-post-menu-category-template").html())
entry_template = _.template($("#new-post-menu-entry-template").html())
html = ""
for name in map.children
if name of map.entries
entry = map.entries[name]
html += entry_template({text: name, id: entry.id, is_cohorted: entry.is_cohorted})
else # subcategory
html += category_template({text: name, entries: @renderCategoryMap(map.subcategories[name])})
html
addField: (fieldView) ->
@$('.forum-new-post-form-wrapper').append fieldView
isTabMode: () ->
@mode is "tab"
getCohortOptions: () ->
if @course_settings.get("is_cohorted") and DiscussionUtil.isPrivilegedUser()
@@ -50,19 +41,15 @@ if Backbone?
events:
"submit .forum-new-post-form": "createPost"
"click .post-topic-button": "toggleTopicDropdown"
"click .topic-menu-wrapper": "handleTopicEvent"
"click .topic-filter-label": "ignoreClick"
"keyup .topic-filter-input": DiscussionFilter.filterDrop
"change .post-option-input": "postOptionChange"
"click .cancel": "cancel"
"reset .forum-new-post-form": "updateStyles"
# 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()
toggleGroupDropdown: ($target) ->
if $target.data('cohorted')
$('.js-group-select').prop('disabled', false);
else
$('.js-group-select').val('').prop('disabled', true);
postOptionChange: (event) ->
$target = $(event.target)
@@ -77,13 +64,14 @@ if Backbone?
thread_type = @$(".post-type-input:checked").val()
title = @$(".js-post-title").val()
body = @$(".js-post-body").find(".wmd-input").val()
group = @$(".js-group-select option:selected").attr("value")
group = @$(".js-group-select option:selected").attr("value")
anonymous = false || @$(".js-anon").is(":checked")
anonymous_to_peers = false || @$(".js-anon-peers").is(":checked")
follow = false || @$(".js-follow").is(":checked")
url = DiscussionUtil.urlFor('create_thread', @topicId)
topicId = if @isTabMode() then @topicView.getCurrentTopicId() else @topicId
url = DiscussionUtil.urlFor('create_thread', topicId)
DiscussionUtil.safeAjax
$elem: $(event.target)
@@ -108,97 +96,6 @@ if Backbone?
@resetForm()
@collection.add thread
toggleTopicDropdown: (event) ->
event.preventDefault()
event.stopPropagation()
if @menuOpen
@hideTopicDropdown()
else
@showTopicDropdown()
showTopicDropdown: () ->
@menuOpen = true
@dropdownButton.addClass('dropped')
@topicMenu.show()
$(".form-topic-drop-search-input").focus()
$("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() - 40
# Need a fat arrow because hideTopicDropdown is passed as a callback to bind
hideTopicDropdown: () =>
@menuOpen = false
@dropdownButton.removeClass('dropped')
@topicMenu.hide()
$("body").unbind "click", @hideTopicDropdown
handleTopicEvent: (event) ->
event.preventDefault()
event.stopPropagation()
@setTopic($(event.target))
setTopic: ($target) ->
if $target.data('discussion-id')
@topicText = $target.html()
@topicText = @getFullTopicName($target)
@topicId = $target.data('discussion-id')
@setSelectedTopic()
if $target.data("cohorted")
$(".js-group-select").prop("disabled", false)
else
$(".js-group-select").val("")
$(".js-group-select").prop("disabled", true)
@hideTopicDropdown()
setSelectedTopic: ->
@$(".js-selected-topic").html(@fitName(@topicText))
getFullTopicName: (topicElement) ->
name = topicElement.html()
topicElement.parents('.topic-submenu').each ->
name = $(this).siblings('.topic-title').text() + ' / ' + 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 = gettext("") + " / " + path.join(" / ")
if @getNameWidth(partialName) < @maxNameWidth
return partialName
rawName = path[0]
name = gettext("") + " / " + rawName
while @getNameWidth(name) > @maxNameWidth
rawName = rawName[0...rawName.length-1]
name = gettext("") + " / " + rawName + " " + gettext("")
return name
cancel: (event) ->
event.preventDefault()
if not confirm gettext("Your post will be discarded.")
@@ -210,8 +107,8 @@ if Backbone?
@$(".forum-new-post-form")[0].reset()
DiscussionUtil.clearFormErrors(@$(".post-errors"))
@$(".wmd-preview p").html("")
if @mode is "tab"
@setTopic(@$("a.topic-title").first())
if @isTabMode()
@topicView.setTopic(@$("a.topic-title").first())
updateStyles: =>
# form reset doesn't change the style of checkboxes so this event is to do that job