window.hot = new Backbone.Model({
readOnly: false,
});
var Post = Backbone.Model.extend({
idAttribute: 'num',
});
var Replies = Backbone.Collection.extend({model: Post});
var Thread = Backbone.Model.extend({
idAttribute: 'num',
initialize: function () {
if (!this.get('replies'))
this.set('replies', new Replies([]));
},
});
var ThreadCollection = Backbone.Collection.extend({
model: Thread,
lookup: function (num, op) {
var thread = this.get(op) || UnknownThread;
return (num == op) ? thread : thread.get('replies').get(num);
},
});
var Threads = new ThreadCollection();
var UnknownThread = new Thread();
function model_link(key) {
return function (event) {
this.model.set(key, $(event.target).val());
};
}
var Section = Backbone.View.extend({
tagName: 'section',
initialize: function () {
this.listenTo(this.model, {
'change:hide': this.renderHide,
'change:locked': this.renderLocked,
'change:spoiler': this.renderSpoiler,
destroy: this.remove,
});
this.listenTo(this.model.get('replies'), {
remove: this.removePost,
});
},
renderHide: function (model, hide) {
this.$el.next('hr').andSelf().toggle(!hide);
},
renderLocked: function (model, locked) {
this.$el.toggleClass('locked', !!locked);
},
renderSpoiler: function (model, spoiler) {
var $img = this.$el.children('figure').find('img');
var sp = oneeSama.spoiler_info(spoiler, true);
$img.replaceWith($('', {
src: sp.thumb, width: sp.dims[0], height: sp.dims[1],
}));
},
remove: function () {
var replies = this.model.get('replies');
replies.each(function (post) {
clear_post_links(post, replies);
});
replies.reset();
this.$el.next('hr').andSelf().remove();
this.stopListening();
},
removePost: function (model) {
model.trigger('removeSelf');
},
});
/* XXX: Move into own views module once more substantial */
var Article = Backbone.View.extend({
tagName: 'article',
initialize: function () {
this.listenTo(this.model, {
'change:backlinks': this.renderBacklinks,
'change:editing': this.renderEditing,
'change:hide': this.renderHide,
'change:image': this.renderImage,
'change:spoiler': this.renderSpoiler,
'removeSelf': this.remove,
});
},
render: function () {
var html = oneeSama.mono(this.model.attributes);
this.setElement($($.parseHTML(html)).filter('article')[0]);
return this;
},
renderBacklinks: function () {
if (options.get('nobacklinks'))
return this; /* ought to disconnect handler? */
var backlinks = this.model.get('backlinks');
var $list = this.$el.find('small');
if (!backlinks || !backlinks.length) {
$list.remove();
return this;
}
if (!$list.length)
$list = $('', {text: 'Replies:'}).appendTo(
this.$el);
// TODO: Sync up DOM gracefully instead of clobbering
$list.find('a').remove();
backlinks.forEach(function (num) {
var $a = $('', {href: '#'+num, text: '>>'+num});
$list.append(' ', $a);
});
return this;
},
renderEditing: function (model, editing) {
this.$el.toggleClass('editing', !!editing);
if (!editing)
this.$('blockquote')[0].normalize();
},
renderHide: function (model, hide) {
this.$el.toggle(!hide);
},
renderImage: function (model, image) {
var hd = this.$('header'), fig = this.$('figure');
if (!image)
fig.remove();
else if (hd.length && !fig.length) {
/* Is this focus business necessary here? */
var focus = get_focus();
insert_image(image, hd, false);
if (focus)
focus.focus();
}
},
renderSpoiler: function (model, spoiler) {
var $img = this.$('figure').find('img');
var sp = oneeSama.spoiler_info(spoiler, false);
$img.replaceWith($('', {
src: sp.thumb,
width: sp.dims[0], height: sp.dims[1],
}));
},
});
/* BATCH DOM UPDATE DEFER */
var deferredChanges = {links: {}, backlinks: {}};
var haveDeferredChanges = false;
/* this runs just before every _outermost_ wrap_dom completion */
Backbone.on('flushDomUpdates', function () {
if (!haveDeferredChanges)
return;
haveDeferredChanges = false;
for (var attr in deferredChanges) {
var deferred = deferredChanges[attr];
var empty = true;
for (var id in deferred) {
deferred[id].trigger('change:'+attr);
empty = false;
}
if (!empty)
deferredChanges[attr] = {};
}
});
/* LINKS */
function add_post_links(src, links, op) {
if (!src || !links)
return;
var thread = Threads.get(op);
var srcLinks = src.get('links') || [];
var repliedToMe = false;
for (var destId in links) {
var dest = thread && thread.get('replies').get(destId);
if (!dest) {
/* Dest doesn't exist yet; track it anyway */
dest = new Post({id: destId, shallow: true});
UnknownThread.get('replies').add(dest);
}
if (dest.get('mine'))
repliedToMe = true;
var destLinks = dest.get('backlinks') || [];
/* Update links and backlinks arrays in-order */
var i = _.sortedIndex(srcLinks, dest.id);
if (srcLinks[i] == dest.id)
continue;
srcLinks.splice(i, 0, dest.id);
destLinks.splice(_.sortedIndex(destLinks, src.id), 0, src.id);
force_post_change(src, 'links', srcLinks);
force_post_change(dest, 'backlinks', destLinks);
}
if (repliedToMe && !src.get('mine')) {
/* Should really be triggered only on the thread */
Backbone.trigger('repliedToMe');
}
}
function force_post_change(post, attr, val) {
if (val === undefined && post.has(attr))
post.unset(attr);
else if (post.get(attr) !== val)
post.set(attr, val);
else if (!(post.id in deferredChanges[attr])) {
/* We mutated the existing array, so `change` won't fire.
Dumb hack ensues. Should extend Backbone or something. */
/* Also, here we coalesce multiple changes just in case. */
/* XXX: holding a direct reference to post is gross */
deferredChanges[attr][post.id] = post;
haveDeferredChanges = true;
}
}
function clear_post_links(post, replies) {
if (!post)
return;
(post.get('links') || []).forEach(function (destId) {
var dest = replies.get(destId);
if (!dest)
return;
var backlinks = dest.get('backlinks') || [];
var i = backlinks.indexOf(post.id);
if (i < 0)
return;
backlinks.splice(i, 1);
if (!backlinks.length)
backlinks = undefined;
force_post_change(dest, 'backlinks', backlinks);
});
(post.get('backlinks') || []).forEach(function (srcId) {
var src = replies.get(srcId);
if (!src)
return;
var links = src.get('links') || [];
var i = links.indexOf(post.id);
if (i < 0)
return;
links.splice(i, 1);
if (!links.length)
links = undefined;
force_post_change(src, 'links', links);
});
post.unset('links', {silent: true});
post.unset('backlinks');
}