This repository has been archived on 2022-05-20. You can view files and clone it, but cannot push or open issues or pull requests.
Calculators/Download/Calculators A First Look at the TI-Nspire CX II_files/blog_views.js

1670 lines
44 KiB
JavaScript

/**
* Views used in constructing AoPS Blogs
*
* Relies heavily on views used for forums; many functions are mixed in throughout
* these views. An understanding of Views.TopicFull and Views.Post will be very
* helpful in sorting out what's going on here.
**/
AoPS.Community.Views = (function (ComViews) {
var Lang = AoPS.Community.Lang,
Modal = AoPS.Ui.Modal;
/**
* Construct a loader for the blogs. We have the AoPS default as
* the background image on this div, but users will be able to change that
* with their own CSS.
**/
ComViews.buildBlogLoader = function () {
return $('<div class="blog-loader"></div>');
};
/**
* Build a scrollbar in the blogs that isn't in default community style.
* Used in the shoutbox currently.
*/
ComViews.buildBlogScrollbar = function ($obj) {
var scrollbar_defaults = {
slider_end_tolerance: 2,
stop_scroll_propagation: true,
},
scrollbar_args =
arguments.length === 1
? scrollbar_defaults
: _.extend(scrollbar_defaults, arguments[1]),
$scrollwrap = $obj.wrapWithScroll(scrollbar_args);
/** Extra div needed for firefox centering and scrolling to work with stuff in the scrollbar **/
$scrollwrap.addClass("blog-scroll-outer");
return $scrollwrap;
};
ComViews.toggleBlogSubscription = function (obj) {
var new_text = obj.blog.get("is_subscribed")
? Lang["Subscribe"]
: Lang["Unsubscribe"];
obj.blog.set("is_subscribed", !obj.blog.get("is_subscribed"));
obj.user.setBlogSubscriptionStatus({
blog_id: obj.blog.get("category_id"),
status: obj.blog.get("is_subscribed"),
});
AoPS.Ui.Flyout.display(
Lang[
obj.blog.get("is_subscribed")
? "blog-subscribed-notify"
: "blog-unsubscribed-notify"
]
);
$("#blog-subscribe")[0].title =
Lang[
obj.blog.get("is_subscribed")
? "blog-unsubscribe-tooltip"
: "blog-subscribe-tooltip"
];
$("#blog-subscribe").text(new_text);
};
/**
* Top bit of a blog, very simple.
**/
ComViews.CategoryCellBlogHeading = AoPS.View.extend({
template_id: "#blog-category-cell-heading-tpl",
id: "header",
initialize: function () {
this.$el.html(
this.getTemplate(this.template_id, {
blog_title: this.model.get("category_name"),
category_id: this.model.get("category_id"),
})
);
},
});
/**
* A list of Blog Topics; may be filtered by tag.
*
* Collection : FilteredTopicList
*
* Each Topic in the list produces a TopicCellBlog
*/
ComViews.TopicsListBlog = AoPS.View.extend({
template_id: "#blog-topic-list-tpl",
id: "main",
initialize: function (options) {
var self = this,
no_topics_msg;
this.blog = options.blog;
// eslint-disable-next-line eqeqeq
var is_blog_category = options.blog == this.collection.category;
no_topics_msg =
Lang[is_blog_category ? "blog-no-topics" : "blog-no-results"];
this.$no_topics = $(
'<div class="blog-no-topics">' + no_topics_msg + "</div>"
);
this.$el.html(
this.getTemplate(this.template_id, {
can_post: this.blog.getPermission("c_can_start_topic"),
lang_post_new: Lang["blog-post-new-entry"],
})
);
this.topic_cells = [];
this.$loader = ComViews.buildBlogLoader();
// LOOP through existing topics in collection, add them.
this.collection.each(function (topic) {
self.addTopic(topic);
});
if (this.collection.length === 0) {
this.$el.append(this.$no_topics);
}
this.listenTo(this.collection, "add", this.addTopic);
this.listenTo(this.collection, "remove", this.removeTopic);
// When we scroll to the bottom of the browser, we go get more topics.
$(window).scroll(function () {
if (self.$el.is(":visible")) {
if (
window.innerHeight + window.pageYOffset + 50 >=
$(document).height()
) {
self.fetchMoreTopics();
}
}
});
},
events: {
"click #post-new-entry": "onClickNewTopic",
},
/**
* onClickNewTopic fires up the post topic atop the page in a modal.
*/
onClickNewTopic: function (e) {
this.openNewBlogPost(this.blog);
e.preventDefault();
e.stopPropagation();
},
openNewBlogPost: function (category) {
if (!AoPS.session.logged_in) {
AoPS.Ui.Modal.showMessage(Lang["new-topic-not-logged-in"]);
} else if (!category.getPermission("c_can_start_topic")) {
AoPS.Ui.Modal.showMessage(Lang["cat-cell-no-perm-start-topic"]);
} else {
// eslint-disable-next-line no-new
new ComViews.NewBlogPost({
model: category,
master: category.get("master"),
});
}
},
/**
* Any time this collection has a new topic, we build a new cell for the topic.
*
* @param AoPS.Community.Models.Topic
**/
addTopic: function (topic) {
var new_topic = new ComViews.TopicCellBlog({
model: topic,
category: this.blog,
}),
i,
first_post_time = topic.get("first_post_time"),
n_topics = this.topic_cells.length,
topic_placed = false,
current_topic;
// placing the topic in the DOM in the right place
for (i = 0; i < n_topics; i++) {
current_topic = this.topic_cells[i];
if (current_topic.model.get("first_post_time") < first_post_time) {
current_topic.$el.before(new_topic.$el);
this.topic_cells.splice(i, 0, new_topic);
topic_placed = true;
i = n_topics;
}
}
if (!topic_placed) {
// Put this topic at the end.
this.topic_cells.push(new_topic);
this.$el.append(new_topic.$el);
}
this.$loader.detach();
this.$no_topics.detach();
},
removeTopic: function (topic) {
var topic_cell = _.find(this.topic_cells, function (cell) {
return cell.model === topic;
});
if (!_.isUndefined(topic_cell)) {
this.topic_cells = _.without(this.topic_cells, topic_cell);
topic_cell.close();
}
},
/**
* Go get more topics for this awesome blog!
**/
fetchMoreTopics: function () {
var self = this;
if (!this.collection.topics_loading) {
if (!this.collection.all_topics_fetched) {
this.$el.append(this.$loader);
this.collection.fetchMoreTopics({
onFinish: _.bind(function () {
this.$loader.detach();
}, this),
onError: function (data) {
var msg;
self.$loader.detach();
if (
typeof Lang["topic-fetch-err-" + data.error_code] === "string"
) {
msg = Lang["topic-fetch-err-" + data.error_code];
} else {
msg = Lang["unexpected-error-code"] + data.error_code;
}
// Error; don't want to try fetching any more.
self.collection.all_topics_fetched = true;
ComViews.showError(msg);
},
});
}
}
},
});
ComViews.BlogTopicTagbox = AoPS.Community.Views.TopicTagbox.extend({
always_show_edit: false,
});
/**
* A single Topic in the blog: we render the full first post of the topic.
*
* model : AoPS.Community.Models.Topic
*/
ComViews.TopicCellBlog = AoPS.View.extend({
template_id: "#blog-topic-cell-tpl",
push_state_attribute: "data-blog",
staticTagboxView: AoPS.Community.Views.BlogTopicTagbox,
editableTagboxView: AoPS.Community.Views.EditableTagBoxExistingTopic,
initialize: function (options) {
this.category = options.category;
this.buildRoutes();
this.render();
this.listenTo(this.model, "change:num_posts", this.buildNumComments);
this.listenTo(this.model, "change:locked", this.render);
this.listenTo(
this.model.get("posts").first(),
"change:post_rendered",
this.render
);
},
render: function () {
var can_edit = this.model.getPermission("c_can_edit"),
can_moderate =
this.model.getPermission("c_can_lock_topic") ||
this.model.getPermission("c_can_delete");
this.has_poll = this.model.get("poll_id") > 0;
this.first_post = this.model.get("posts").first();
this.$el.html(
this.getTemplate(this.template_id, {
username: this.model.get("first_poster_name"),
user_id: this.model.get("first_poster_id"),
date: AoPS.Community.Utils.makePrettyTimeStatic(
this.model.get("first_post_time")
),
lang_permanent_link: Lang["permanent-link"],
lang_by: Lang["by"],
has_settleable_reports:
this.model.get("num_reports") > 0 &&
this.model.getPermission("c_can_settle_report"),
route_url: AoPS.Community.Views.makeLinkUrl(this.route_url),
topic_title: this.model.get("topic_title"),
can_reply:
!this.model.get("locked") &&
this.model.getPermission("c_can_reply"), //get from permissions
route_reply: AoPS.Community.Views.makeLinkUrl(this.comment_url), //TODO
post_comment_text: this.category.get("blog_post_comment_text"), //get from category
lang_edit_post: Lang["blog-edit-post"],
lang_moderate_post: Lang["blog-moderate-post"],
has_mod_actions: can_edit || can_moderate,
can_edit: can_edit,
has_poll: this.has_poll,
can_moderate: can_moderate,
topic_id: this.model.get("topic_id"),
has_tags: this.category.get("show_tags"), //(this.model.get('tags').length > 0),
})
);
this.finishRenderingPost();
if (this.category.get("show_tags")) {
this.constructStaticTagbox();
this.tagbox_state = "static";
}
if (this.has_poll) {
this.constructPoll();
}
this.buildNumComments();
this.checkSearchHighlighting();
},
/**
* We build polls in blogs by mixing in the relevant pieces of ComViews.Post.
*
* To get this all to work, we make a poll_object property of this view
* such that poll_object has all the pieces needed by the functions we're
* mixing in (initializePoll, constructPoll, onClickPollVote, onClickTogglePollResults).
***/
constructPoll: function () {
this.poll_object = {
$el: this.$el,
topic: {
model: this.model,
},
$: this.$,
listenTo: this.listenTo,
getTemplate: this.getTemplate,
constructPoll: ComViews.Post.prototype.constructPoll,
};
ComViews.Post.prototype.initializePoll.apply(this.poll_object);
},
onClickPollVote: function () {
ComViews.Post.prototype.onClickPollVote.apply(this.poll_object);
},
onClickTogglePollResults: function () {
ComViews.Post.prototype.onClickTogglePollResults.apply(this.poll_object);
},
checkSearchHighlighting: function () {
var search_text;
if (this.model.get("is_search_result")) {
search_text = this.model.fetchSearchText("post_text");
if (search_text.length > 0) {
this.$(".entry h1").extendedHighlightText(
"cmty-highlight",
search_text
);
this.$(".message").extendedHighlightText(
"cmty-highlight",
search_text
);
this.$(".cmty-tags-itembox-wrapper").extendedHighlightText(
"cmty-highlight",
search_text
);
/*if (this.model.get('post_id') === this.topic.model.get('focus_post').get('post_id')) {// pre-open hidden text in focus text
this.$('.cmty-hide-heading').trigger('click');
}*/
}
}
},
/** Mixed in to BlogComment. **/
finishRenderingPost: function () {
// pywindow causes this to be rendered separately. We will put this back in handlebars if we
// stick with onclick
var $m = this.$(".message");
$m.html(this.first_post.get("post_rendered"));
this.parseEditData();
this.listenTo(
this.first_post,
"change:last_edit_time_rendered",
this.parseEditData
);
this.$attachments = this.$(".cmty-post-attachments");
this.highlight($m);
this.parseAttachments();
this.listenTo(
this.first_post,
"change:attachment change:attachments",
this.parseAttachments
);
},
events: {
"click .blog-edit-post": "onClickEdit",
"click .blog-moderate-topic": "onClickModerate",
"click .cmty-itembox .cmty-edit-tag": "toggleTagbox",
"click .cmty-poll-results-toggle span": "onClickTogglePollResults",
"click .cmty-poll-vote-row .btn": "onClickPollVote",
},
/* eslint-disable no-undef */
highlight: function ($m) {
if ($m.length > 0 && typeof Prism !== "undefined") {
Prism.plugins.autoloader.languages_path =
"https://cdnjs.cloudflare.com/ajax/libs/prism/1.17.1/components/";
var elements = $m[0].querySelectorAll("pre code");
for (var i = 0; i < elements.length; i++) {
Prism.highlightElement(elements[i]);
}
}
},
/* eslint-enable no-undef */
parseEditData: function () {
ComViews.Post.prototype.parseEditData.apply({
$el: this.$el,
model: this.first_post,
watching_edit_time: false,
});
},
parseAttachments: function () {
ComViews.Post.prototype.parseAttachments.apply({
$el: this.$el,
model: this.first_post,
$attachments: this.$attachments,
isAttachmentImage: ComViews.Post.prototype.isAttachmentImage,
});
},
/**
* We mix in the Views.Post.onClickEdit function here; that function
* only uses the "model" and "topic" properties of View.Post,
* so we create an object with those properties here, and apply
* the function to the new object.
**/
onClickEdit: function (e) {
var obj;
e.stopPropagation();
e.preventDefault;
obj = {
topic: this,
model: this.model.get("posts").first(),
};
AoPS.Community.Views.Post.prototype.onClickEdit.apply(obj, [e]);
},
/**
* Throws up a modal for moderating this topic.
**/
onClickModerate: function (e) {
e.stopPropagation();
e.preventDefault();
// eslint-disable-next-line no-new
new AoPS.Community.Views.ModerateTopic({
model: this.model,
topic_category_at_initialization: this.model.get("category_id"),
});
},
constructStaticTagbox:
AoPS.Community.Views.TopicFull.prototype.constructStaticTagbox,
constructEditableTagbox:
AoPS.Community.Views.TopicFull.prototype.constructEditableTagbox,
toggleTagbox: AoPS.Community.Views.TopicFull.prototype.toggleTagbox,
/**
* Called in the mixed-in functions above; we don't need to do anything here.
**/
setHeight: function () {
return;
},
/**
* Create the number of comments text
**/
buildNumComments: function () {
var num_comments_text,
num_comments = this.model.get("num_posts") - 1;
if (num_comments === 0) {
num_comments_text = this.category.get("blog_no_comments_text");
} else if (num_comments === 1) {
num_comments_text = this.category.get("blog_one_comment_text");
} else {
num_comments_text = this.category
.get("blog_comments_text")
.replace("%s", num_comments);
}
this.$el.find(".post-replies").text(num_comments_text);
},
/**
* Build the routes that will be used in the template
**/
buildRoutes: function () {
var title =
"_" + ComViews.convertToUrlFragment(this.model.get("topic_title")),
route_base =
"/c" +
this.model.get("category_id") +
"h" +
this.model.get("topic_id");
this.route_url = route_base + title;
this.comment_url = route_base + "s3" + title;
},
});
/**
* Sidebar panel of a blog. Constructed from pieces the user chooses.
**/
ComViews.BlogSidebar = AoPS.View.extend({
template_id: "#blog-sidebar-tpl",
id: "side",
initialize: function () {
// ADD title. Always shown?
// if (this.model.get('show_custom_block')) {
this.sidebar_title = new AoPS.Community.Views.BlogSidebarTitle({
model: this.model,
});
this.$el.append(this.sidebar_title.$el);
// }
this.archive = new AoPS.Community.Views.BlogArchive({
model: this.model,
});
this.$el.append(this.archive.$el);
// Add Shoutbox
if (this.model.get("show_shoutbox")) {
this.shoutbox = new AoPS.Community.Views.BlogShoutbox({
model: this.model.get("shoutbox_topic"),
});
this.$el.append(this.shoutbox.$el);
}
// Only show the contributors if it's not silly (more than one contributor)
if (
this.model.get("show_contributors") &&
this.model.get("contributors").length > 1
) {
this.contributors = new AoPS.Community.Views.BlogContributors({
model: this.model,
});
this.$el.append(this.contributors.$el);
}
if (this.model.get("show_tags")) {
this.tags = new AoPS.Community.Views.BlogTagbox({
model: this.model,
});
this.$el.append(this.tags.$el);
}
// Add user profile
if (this.model.get("show_profile_info")) {
this.about_owner = new AoPS.Community.Views.BlogAboutOwner({
model: this.model,
});
this.$el.append(this.about_owner.$el);
}
// Add stats
if (this.model.get("show_stats")) {
this.stats = new AoPS.Community.Views.BlogStats({
model: this.model,
});
this.$el.append(this.stats.$el);
}
this.search = new AoPS.Community.Views.BlogSearch({
model: this.model,
});
this.$el.append(this.search.$el);
// TODO : RSS; Maybe spike?
},
onClose: function () {
this.sidebar_title.close();
if (this.hasOwnProperty("about_owner")) {
this.about_owner.close();
}
if (this.hasOwnProperty("stats")) {
this.stats.close();
}
if (this.hasOwnProperty("contributors")) {
this.contributors.close();
}
if (this.hasOwnProperty("shoutbox")) {
this.shoutbox.close();
}
if (this.hasOwnProperty("tags")) {
this.tags.close();
}
this.search.close();
},
});
ComViews.BlogContributors = AoPS.View.extend({
template_id: "#blog-contributors-tpl",
id: "contrib-widget",
className: "widget",
initialize: function () {
var contributors = _.map(this.model.get("contributors"), function (user) {
return (
'<a href="/community/user/' +
user.user_id +
'">' +
user.username +
"</a>"
);
}).join(" &bull; ");
this.$el.html(
this.getTemplate(this.template_id, {
lang_contributors: Lang["blog-contributors-title"],
contributors: contributors,
})
);
},
});
/**
* Used to build the tagbox on the side.
**/
ComViews.BlogItembox = AoPS.Community.Views.Itembox.extend({
scrollbarCreator: AoPS.Community.Views.buildBlogScrollbar,
push_state_attribute: "data-blog",
});
/**
* Creates the title box at the top.
**/
ComViews.BlogSidebarTitle = AoPS.View.extend({
template_id: "#blog-sidebar-title-tpl",
id: "user-menu-widget",
className: "block user_menu",
initialize: function () {
var user_route =
AoPS.Community.Constants.user_path + this.model.get("created_by");
this.$el.html(
this.getTemplate(this.template_id, {
description: this.model.get("short_description"),
avatar: this.model.get("creator_avatar"),
user_route: user_route,
username: this.model.get("creator_username"),
})
);
},
});
/**
* Holds the details for the blog owner.
*/
ComViews.BlogAboutOwner = AoPS.View.extend({
template_id: "#blog-about-owner-tpl",
id: "about-owner-widget",
className: "block widget",
initialize: function () {
// TODO: Sort out location crap when we have it on the back end.
this.$el.html(
this.getTemplate(this.template_id, {
lang_about_owner: Lang["blog-about-owner"],
lang_posts: Lang["blog-posts"],
posts_count: this.model.get("creator_num_posts"),
joined_date: AoPS.Community.Utils.makePrettyTimeStaticDateOnly(
this.model.get("creator_joined_time")
),
lang_joined: Lang["blog-joined"],
has_location: this.model.get("creator_location").length > 0,
lang_location: Lang["blog-location"],
location: this.model.get("creator_location"),
})
);
},
});
/**
* Holds the stats for the blog.
*/
ComViews.BlogStats = AoPS.View.extend({
template_id: "#blog-stats-tpl",
id: "stats-widget",
className: "block widget",
initialize: function () {
// TODO: Sort out location crap when we have it on the back end.
this.render();
this.listenTo(
this.model,
"change:num_topics change:num_posts",
this.render
);
},
render: function () {
this.$el.html(
this.getTemplate(this.template_id, {
lang_stats: Lang["blog-stats"],
lang_created: Lang["blog-created"],
lang_entries: Lang["blog-entries"],
lang_visits: Lang["blog-visits"],
lang_comments: Lang["blog-comments"],
created_date: this.model.get("created_at"),
num_entries: this.model.get("num_topics"),
num_visits: this.model.get("num_views"),
num_comments:
this.model.get("num_posts") - this.model.get("num_topics"),
})
);
},
});
ComViews.BlogSearch = AoPS.View.extend({
template_id: "#blog-search-tpl",
id: "search-widget",
className: "block widget",
initialize: function () {
this.$el.html(
this.getTemplate(this.template_id, {
lang_search_heading: Lang["blog-search-heading"],
lang_search_placeholder: Lang["blog-search-placeholder"],
lang_search_button: Lang["blog-search-button"],
lang_search_advanced: Lang["blog-search-advanced"],
lang_search_advanced_tooltip: Lang["blog-search-advanced-tooltip"],
blog_id: this.model.get("category_id"),
})
);
},
events: {
"click a": "onClickAdvancedSearch",
"submit #blog_searchform": "onSearch",
},
onClickAdvancedSearch: function (e) {
Modal.showMessage("Made you look! This is not implemented yet.");
e.stopPropagation();
e.preventDefault();
},
onKeyDownSearch: function (e) {
var key_pressed = e.which || e.keyCode,
target;
if (key_pressed === 13) {
target = e.currentTarget.value;
if (target.length > 0) {
this.launchSearch(target.length);
// un-comment below to turn search input back on
e.currentTarget.value = "";
}
}
},
launchSearch: function (term) {
$("#blog_keywords").val("");
console.log(term);
console.log(
"/c" + this.model.get("category_id") + "/" + encodeURIComponent(term)
);
Backbone.history.navigate(
"/c" + this.model.get("category_id") + "/" + encodeURIComponent(term),
{trigger: true}
);
},
onSearch: function (e) {
var term = $("#blog_keywords").val();
if (term.length > 0) {
this.launchSearch(term);
}
e.stopPropagation();
e.preventDefault();
},
});
/**
* Archive for a blog.
*
* Model : Community.Models.Topic of the topic that is the shoutbox for this
* blog
*
* All topics for a blog are sorted into expandable menus by month, for easy
* navigation.
**/
ComViews.BlogArchive = AoPS.View.extend({
template_id: "#blog-archive-tpl",
id: "archive-widget",
className: "widget",
initialize: function () {
var archive_topics = this.model.get("blog_archive_topics");
archive_topics = this.convertNumericalMonthsToWords(archive_topics);
this.$el.html(
this.getTemplate(this.template_id, {
lang_archive: Lang["blog-archive-title"],
category_id: this.model.get("category_id"),
monthly_topic_data: archive_topics,
})
);
},
events: {
"click .archive-month-header": "toggleMonthMenu",
},
// Converts month "02" to February, for example. Uses community language file.
convertNumericalMonthsToWords: function (topic_data) {
for (var i = 0; i < topic_data.length; i++) {
topic_data[i].month_name = Lang["month-" + topic_data[i].month];
}
return topic_data;
},
// Expand/collapse month menus in archive.
toggleMonthMenu: function (e) {
var $element = $(e.currentTarget),
$month_menu = $element.siblings(".archive-month-topics"),
$menu_image = $element.find(".month-image");
if ($month_menu.hasClass("archive-month-collapsed")) {
$menu_image.attr("src", "/m/community/img/blogs/hyperion/minus.gif");
$menu_image.attr("alt", "-");
$month_menu.removeClass("archive-month-collapsed");
} else {
$menu_image.attr("src", "/m/community/img/blogs/hyperion/plus.gif");
$menu_image.attr("alt", "+");
$month_menu.addClass("archive-month-collapsed");
}
},
});
/**
* Shoutbox for a blog.
*
* Model : Community.Models.Topic of the topic that is the shoutbox for this
* blog
*
* The html structure for the shoutbox was set in AoPS2.0, and had to be matched
* to allow user CSS to work.
*
* The posts in a shout topic are displayed with the most recent on top, which
* is the opposite of what we typically do in topics. So, a lot of things
* are reversed. We filter by those posts which are "show_from_end" posts,
* and a lot of the signs/directions in the addPost function are the opposite
* of those in Views.TopicFull.addPost()
**/
ComViews.BlogShoutbox = AoPS.View.extend({
template_id: "#blog-shoutbox-tpl",
id: "shouts-widget",
className: "widget",
initialize: function () {
var self = this;
var can_shout = this.model.getPermission("c_can_shout");
this.$el.html(
this.getTemplate(this.template_id, {
lang_shoutbox: Lang["blog-shoutbox-title"],
can_shout: can_shout,
lang_Submit: Lang["Submit"],
})
);
this.$loader = ComViews.buildBlogLoader();
this.$shouts_outer = this.$el.find("fieldset");
this.$shout_list = $("<ul></ul>");
this.$num_shouts = this.$el.find(".blog-num-shouts");
this.setNumShouts();
this.listenTo(this.model, "change:num_posts", this.setNumShouts);
this.$shouts_outer.prepend(
AoPS.Community.Views.buildBlogScrollbar(this.$shout_list)
);
this.$scrollbar = this.$shouts_outer.find(".aops-scroll-bar");
this.posts = [];
this.topic = this.model.get("posts").each(function (post) {
if (post.get("post_number") > 1) {
self.addPost(post);
}
});
this.applyPostClasses();
this.$scrollbar.on("slider_at_end", function () {
self.fetchMorePosts();
});
this.listenTo(this.model.get("posts"), "add", this.addPost);
this.listenTo(
this.model.get("posts"),
"change:deleted",
this.onDeletePost
);
if (can_shout) {
this.$post_box = this.$el.find("#message");
this.$chars_left = this.$el.find("#charsleft");
this.$shout_helper = this.$el.find("#shout-helper");
this.$post_box.on("keydown", function (e) {
var key_pressed = e.which || e.keyCode;
if (key_pressed === 13 && (e.ctrlKey || e.metaKey)) {
self.onClickSubmit();
}
});
this.$post_box.on("keyup", _.bind(this.onKeyPressShout, this));
}
// Allows us to mix-in AoPS.Community.Views.NewReply
this.settings = {
category_id: this.model.get("category_id"),
topic_id: this.model.get("topic_id"),
topic: this.model,
master: this.model.get("master"),
onSubmit: function () {
self.listenToOnce(
self.model,
"new_post_processed",
self.onSubmittedPostProcessed
);
},
};
this.extra_options = {};
this.model.startRealtimePostMonitor({
hash: this.model.get("master").get("watcher_hash"),
source: "master",
});
},
events: {
"click #submit": "onClickSubmit",
},
/**
* We show the user how many characters they have left in their shout.
**/
onKeyPressShout: function (e) {
var input_length = this.$post_box.val().length,
characters_left = 0,
character_message;
this.$shout_helper.toggle(input_length > 0);
if (input_length > AoPS.Community.Constants.max_shout_length) {
this.$post_box.val(
this.$post_box
.val()
.substr(0, AoPS.Community.Constants.max_shout_length)
);
} else {
characters_left =
AoPS.Community.Constants.max_shout_length - input_length;
}
character_message =
characters_left === 1
? Lang["blog-one-char-left"]
: Lang["blog-characters-left"];
this.$chars_left.html(characters_left + " " + character_message);
},
no_permission_lang_property: "blog-shout-no-permission",
no_permission_limited_lang_property: "blog-shout-no-permission-limited",
onClickSubmit: function () {
AoPS.Community.Views.NewReply.prototype.submitPost.apply(this);
},
checkCommonErrors:
AoPS.Community.Views.PostingEnviron.prototype.checkCommonErrors,
throwLatexErrorMessage: function (code) {
var message = Lang["blog-shout-err-" + code];
AoPS.Community.Views.showError(message);
},
throwBlockingMessage: function () {
ComViews.throwLoaderBlockingMessage(Lang["blog-shout-blocker"]);
},
onSubmittedPostProcessed: function () {
AoPS.Ui.Modal.closeAllModals();
},
onFinishSubmit: function () {
this.$post_box.val("");
this.$shout_helper.hide();
this.listenToOnce(this.model.get("posts"), "add", this.applyPostClasses);
},
showError: AoPS.Community.Views.NewReply.prototype.showError,
checkPost: function () {
var category;
// Also, how do we want to organize constants like the 8 below.
if (
this.$post_box.val().length < AoPS.Community.Constants.min_post_length
) {
this.showError(Lang["new-topic-post-too-short"]);
return false;
}
if (AoPS.session.user_id === this.model.get("last_poster_id")) {
category = this.model
.get("master")
.fetchCategory(this.model.get("category_id"));
// A little lazy here :( Hijacking c_can_edit_core_data --
// Blog owners and site admins can multi-shout.
if (
_.isUndefined(category) ||
!category.getPermission("c_can_edit_core_data")
) {
this.showError(Lang["blog-no-shoutbox-spamming"]);
return false;
}
}
return true;
},
filterPost: function (post) {
return post.get("show_from_end");
},
applyPostFilter: function () {
var self = this;
_.each(this.posts, function (post) {
post.$el.toggle(self.filterPost(post.model));
});
this.applyPostClasses();
},
setNumShouts: function () {
// We subtract 1 for the placeholder.
var num_shouts = this.model.get("num_posts") - 1;
this.$num_shouts.html(
num_shouts +
" " +
(num_shouts === 1 ? Lang["blog-shout"] : Lang["blog-shouts"])
);
},
/**
* Add classes to the visible posts to give the zebra pattern
**/
applyPostClasses: function () {
var row_one = true,
n_posts = this.posts.length,
i;
for (i = 0; i < n_posts; i++) {
if (this.filterPost(this.posts[i].model)) {
this.posts[i].$el.removeClass(row_one ? "row2" : "row1");
this.posts[i].$el.addClass(row_one ? "row1" : "row2");
row_one = !row_one;
}
}
},
/**
* Add a post in the appropriate place
*
* @param ComModels.Post
**/
addPost: function (post) {
var new_post = new ComViews.BlogShout({
model: post,
}),
i,
post_number = post.get("post_number"),
current_post,
n_posts = this.posts.length,
post_placed = false;
// placing the post in the DOM to decide if we should show it.
new_post.$el.hide();
for (i = 0; i < n_posts; i++) {
current_post = this.posts[i];
if (current_post.model.get("post_number") < post_number) {
current_post.$el.before(new_post.$el);
this.posts.splice(i, 0, new_post);
if (this.filterPost(post)) {
new_post.$el.show();
}
post_placed = true;
i = n_posts;
}
}
if (!post_placed) {
// Put this post at the end.
this.posts.push(new_post);
this.$shout_list.append(new_post.$el);
if (this.filterPost(post)) {
new_post.$el.show();
}
}
},
fetchMorePosts: function () {
if (
this.model.get("all_posts_fetched") ||
this.model.get("is_fetching_posts")
) {
return;
}
this.$shout_list.append(this.$loader);
this.model.fetchMorePosts({
start_post_num: _.last(
this.model.get("posts").where({
show_from_end: true,
})
).get("post_number"),
onFinish: _.bind(function () {
this.applyPostFilter();
this.$loader.detach();
}, this),
direction: "backwards", // The shoutbox is in reverse order of a typical topic
show_from_setting: "show_from_end",
});
},
/**
* When a post is deleted from the shoutbox, we need to remove
* its view from the posts array and then reset the classes.
**/
onDeletePost: function (post) {
var post_view = _.findWhere(this.posts, {
model: post,
});
this.posts = _.without(this.posts, post_view);
this.applyPostClasses();
post_view.close();
},
});
/**
* A single shout in the shoutbox.
*
* model : AoPS.Community.Models.Post
**/
ComViews.BlogShout = AoPS.View.extend({
tagName: "li",
template_id: "#blog-shout-tpl",
className: "blog-shout",
initialize: function () {
var user_route =
AoPS.Community.Constants.user_path + this.model.get("poster_id");
this.$el.html(
this.getTemplate(this.template_id, {
lang_by: Lang["by"],
post_rendered: this.model.get("post_rendered"),
username: this.model.get("username"),
date: this.model.get("date_rendered"),
user_route: user_route,
can_delete: this.model.get("topic").getPermission("c_can_delete"),
})
);
},
events: {
"click .blog-delete-shout": "onClickDelete",
},
onClickDelete: AoPS.Community.Views.Post.prototype.onClickDelete,
});
/**
* Tagbox for a blog
*
* model : AoPS.Community.Models.CategoryBlog
*/
ComViews.BlogTagbox = AoPS.View.extend({
template_id: "#blog-tagbox-tpl",
initialize: function () {
this.$el.html(
this.getTemplate(this.template_id, {
lang_tags: Lang["blog-Tags"],
})
);
this.tagbox = new AoPS.Community.Views.BlogItembox({
model: this.model,
});
this.$el.find("fieldset").append(this.tagbox.$el);
},
onClose: function () {
this.tagbox.close();
},
});
/**
* View for a single comment on a Blog Post.
*
* model : AoPS.Community.Models.Post
*
* Many functions are mixed in from AoPS.Community.Views.Post.
* This View is much simpler than Views.Post, and has a few bits that
* are a very different structure, so rather than extend Post (or the reverse),
* we mix in the functions we need.
*/
ComViews.BlogComment = AoPS.View.extend({
template_id: "#blog-comment-tpl",
className: "comment",
// TODO : Permanent link to post
initialize: function (options) {
this.topic = options.topic;
this.render();
this.listenTo(this.model, "change:deleted", this.onChangeDeleted);
this.listenTo(this.model, "hard_delete", this.removePostFromTopic);
this.listenTo(this.model, "change:post_rendered", this.render);
},
events: {
"click .blog-edit-post": "onClickEdit",
"click .blog-delete-post": "onClickDelete",
"click .blog-report-post": "onClickReport",
"click .cmty-post-deleted-info": "onClickDeletedInfo",
"click .cmty-undelete-post": "onClickUndelete",
"click .blog-direct-post-link": "onClickDirectLink",
},
render: function () {
var report_data = this.constructReportTemplateData(),
can_edit = this.topic.model.getPermission("c_can_edit"),
can_delete = this.topic.model.getPermission("c_can_delete"),
has_mod_actions = report_data.can_report || can_edit || can_delete;
this.$el.html(
this.getTemplate(this.template_id, {
lang_by: Lang["by"],
is_reported: report_data.is_reported,
highlight_report_button: report_data.highlight_report_button,
date: AoPS.Community.Utils.makePrettyTimeStatic(
this.model.get("post_time")
),
username: this.model.get("username"),
route_user:
AoPS.Community.Constants.user_path + this.model.get("poster_id"),
has_mod_actions: has_mod_actions,
can_report: report_data.can_report,
can_delete: can_delete,
can_undelete: this.model.get("topic").getPermission("c_can_undelete"),
can_edit: can_edit,
report_title: report_data.title,
lang_edit: Lang["blog-edit-post"],
lang_delete: Lang["blog-delete-post"],
lang_report: Lang["blog-report-post"],
})
);
this.first_post = this.model;
this.setVisibility();
this.finishRenderingPost();
},
finishRenderingPost: ComViews.TopicCellBlog.prototype.finishRenderingPost,
parseEditData: ComViews.TopicCellBlog.prototype.parseEditData,
parseAttachments: ComViews.TopicCellBlog.prototype.parseAttachments,
highlight: ComViews.TopicCellBlog.prototype.highlight,
onClickEdit: AoPS.Community.Views.Post.prototype.onClickEdit,
onClickDelete: AoPS.Community.Views.Post.prototype.onClickDelete,
onClickReport: AoPS.Community.Views.Post.prototype.onClickReport,
onClickDirectLink: AoPS.Community.Views.Post.prototype.onClickDirectLink,
constructReportTemplateData:
AoPS.Community.Views.Post.prototype.constructReportTemplateData,
setVisibility: AoPS.Community.Views.Post.prototype.setVisibility,
onClickDeletedInfo: AoPS.Community.Views.Post.prototype.onClickDeletedInfo,
onClickUndelete: AoPS.Community.Views.Post.prototype.onClickUndelete,
onChangeDeleted: AoPS.Community.Views.Post.prototype.onChangeDeleted,
removePostFromTopic:
AoPS.Community.Views.Post.prototype.removePostFromTopic,
});
/**
* A full blog post and its comments.
*
* model : AoPS.Community.Topic
*/
ComViews.BlogTopicFull = AoPS.View.extend({
postView: AoPS.Community.Views.BlogComment,
id: "main",
url_cmty_path: "",
initialize: function (options) {
var blog_post,
i,
post,
posts_length = this.model.get("posts").length;
this.focus_post_id = options.post_id;
this.reply_open = false;
this.reply_box = null;
// Post new topic
this.$el.html(
this.getTemplate("#blog-topic-list-tpl", {
can_post: this.model
.get("category")
.getPermission("c_can_start_topic"),
lang_post_new: Lang["blog-post-new-entry"],
})
);
// Initial blog post
blog_post = new ComViews.TopicCellBlog({
model: this.model,
category: this.model.get("category"),
});
this.$el.append(blog_post.$el);
this.$el.append(
this.getTemplate("#blog-comments-header-tpl", {
lang_post_comment: Lang["blog-post-comment"],
can_comment:
!this.model.get("locked") &&
this.model.getPermission("c_can_reply"), //TODO : what if user turns off replies?
})
);
if (this.model.getPermission("c_can_reply")) {
this.$reply_holder = this.$el.find(".cmty-reply-window");
}
this.$num_comments = this.$el.find(".blog-num-comments");
this.renderNumComments();
this.$loader = ComViews.buildBlogLoader();
this.$posts_box = this.$el.find(".blog-posts-box");
this.posts = [];
for (i = 1; i < posts_length; i++) {
post = this.model.get("posts").models[i];
if (
!post.get("deleted") ||
this.model.getPermission("c_can_read_deleted")
) {
this.addPost(post);
}
}
this.applyPostClasses();
this.listenTo(
this.model,
"change:all_posts_fetched",
this.onAllPostsFetched
);
this.listenTo(this.model, "change:num_posts", this.renderNumComments);
this.listenTo(this.model, "change:locked", this.onChangeLocked);
this.listenTo(this.model, "change:deleted", this.onDelete);
this.listenTo(this.model.get("posts"), "add", this.addPost);
if (!this.model.get("all_posts_fetched")) {
this.fetchAllPosts();
}
this.listenTo(
this.model.get("posts"),
"change:deleted",
this.onDeletedPost
);
// Router can trigger the reply to open
this.$el.on(
"open_reply",
_.bind(function () {
this.openReplyWindow();
}, this)
);
},
events: {
"click .blog-post-comment": "onClickPostComment",
"click .post-comment": "onClickPostComment",
"click a#post-new-entry": "onClickNewTopic",
},
onChangeLocked: function () {
var can_reply =
!this.model.get("locked") && this.model.getPermission("c_can_reply");
this.$el.find(".blog-post-comment").toggle(can_reply);
this.$el.find(".blog-reply-window").toggle(can_reply);
},
/**
* If the full topic has already been loaded prior to initialization, or if we
* somehow can navigate within the blog to a particular post_id, then
* we need onAddToPage to tell the already-created view to navigate to the
* correct post.
**/
onAddToPage: function (options) {
if (options.hasOwnProperty("post_id")) {
this.focus_post_id = options.post_id;
}
if (this.model.get("all_posts_fetched")) {
this.goToFocusPost();
}
},
/**
* When user clicks comment, we open up the reply window.
**/
onClickPostComment: function (e) {
e.stopPropagation();
e.preventDefault();
this.openReplyWindow();
},
/**
* Open the window in which users post comments to the blog.
**/
openReplyWindow: function () {
if (this.reply_open) {
return;
}
this.$reply_holder.addClass("blog-reply-open");
this.$reply_holder.show();
this.reply_open = true;
this.reply_box = new ComViews.NewReply({
topic_id: this.model.get("topic_id"),
category_id: this.model.get("category_id"),
topic: this.model,
has_add_to_feed: false,
has_email_subscribe: true,
master: this.model.get("master"),
onSubmit: _.bind(function () {
// Will probably edit when we have to deal with
// possible errors.
this.closeNewReply();
// Here, we throw a modal and watch for new_post_processed, then
this.listenToOnce(
this.model,
"new_post_processed",
this.onSubmittedPostProcessed
);
}, this),
onCancel: _.bind(function () {
this.closeNewReply();
}, this),
});
this.$reply_holder.append(this.reply_box.$el);
this.reply_box.$post_box.focus();
},
/**
* Once the submitted post is processed, an event will trigger a jump to that post
* (or to the oldest non-fetched post, which will almost always be this one).
**/
onSubmittedPostProcessed: function (response) {
AoPS.Ui.Modal.closeAllModals();
this.focus_post_id = response.post_id;
this.goToFocusPost();
},
/**
* closeNewReply resets the page to what it should look like without the NewReply window open.
*/
closeNewReply: function () {
this.reply_open = false;
this.$reply_holder.removeClass("blog-reply-open");
this.reply_box.close();
},
/**
* Once we've loaded everything, we spike the loader, set the classes on the posts,
* and set up any future adds to reset the post classes. We don't set that listener
* at the start because we don't want it to fire over and over on the initial post load.
**/
onAllPostsFetched: function () {
this.$loader.detach();
this.applyPostClasses();
this.listenTo(this.model.get("posts"), "add", this.applyPostClasses);
if (this.focus_post_id > 0) {
this.goToFocusPost();
}
},
onClickNewTopic: function (e) {
this.openNewBlogPost(this.model.get("category"));
e.preventDefault();
e.stopPropagation();
},
openNewBlogPost: ComViews.TopicsListBlog.prototype.openNewBlogPost,
/**
* If we are trying to load to a particular post, here's where we move the window
* scroll to the appropriate place.
**/
goToFocusPost: function () {
var target_post_cell,
target_id = this.focus_post_id;
if (target_id === 0) {
return;
}
target_post_cell = _.find(this.posts, function (post_cell) {
return post_cell.model.get("post_id") === target_id;
});
if (_.isUndefined(target_post_cell)) {
return;
}
window.scrollTo(0, target_post_cell.$el.offset().top);
},
removePost: ComViews.TopicFull.prototype.removePost,
/**
* If a post is deleted, we need to remove its cell from our view, and
* reset the post classes to keep the zebra pattern.
**/
onDeletedPost: function (post) {
return;
// var deleted_post = _.findWhere(this.posts, {
// model: post,
// });
// if (!_.isUndefined(deleted_post)) {
// this.posts = _.without(this.posts, deleted_post);
// deleted_post.close();
// this.applyPostClasses();
// }
},
/**
* We apply the same zebra pattern here as in the shoutbox.
**/
applyPostClasses:
AoPS.Community.Views.BlogShoutbox.prototype.applyPostClasses,
/**
* Build the number of comments line in the initial post box.
**/
renderNumComments: function () {
var num_comments = this.model.get("num_posts") - 1;
this.$num_comments.html(
num_comments +
(num_comments === 1 ? Lang["blog-Comment"] : Lang["blog-Comments"])
);
},
addPost: AoPS.Community.Views.TopicFull.prototype.addPost,
/**
* No deleted posts should make it to the front end, but just in case...
*/
filterPost: function (post) {
return !post.get("deleted") || this.model.getPermission("c_can_undelete");
},
/**
* Put up a loader and then go get all the comments for this blog post.
**/
fetchAllPosts: function () {
this.$posts_box.append(this.$loader);
/*this.model.fetchInitialPosts({
fetch_all : true
});*/
},
onDelete: ComViews.TopicFull.prototype.onDelete,
navigateAfterDelete: ComViews.TopicFull.prototype.navigateAfterDelete,
});
/**
* The only difference between a new blog post and a new topic in a forum is the
* topic_type field, which we want when we submit this post for processing.
**/
ComViews.NewBlogPost = ComViews.NewTopic.extend({
topic_type: "blog_post",
has_add_to_feed: false,
sending_blocker: Lang["new-blog-post-send-blocker"],
has_email_subscribe: true,
});
return ComViews;
})(AoPS.Community.Views || {});