-
Notifications
You must be signed in to change notification settings - Fork 6
/
hackemup.js
249 lines (221 loc) · 8.1 KB
/
hackemup.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
/*
Welcome to a Hacker News Bookmarklet...
"Hack'em Up" by Mr Speaker
v1.1
DOM wranglin' a go-go.
*/
var hackemup = {
selecta: {
body: "body table:first",
logo: "body table:first table:first tbody tr:first td:first a:first img",
firstColumn: "body > center > table tbody tr:eq(3) td > table > tbody > tr > td:first"
},
init: function() {
// Animate the first column
$(this.selecta.firstColumn)
.animate({ width: 35 }, 400);
},
fetch: function(isRetry) {
var _this = this,
logo = $(this.selecta.logo).addClass("hnu-spin");
// Fetch the new HTML
$("<div></div>").load("/ " + this.selecta.body, function(response, status) {
logo.removeClass("hnu-spin");
if(status == "success") {
_this.update($(response));
return;
}
// Retry once
!isRetry && setTimeout(function(){
_this.fetch(true);
}, 1500);
return;
});
},
update: function(fetched) {
// Remove added changes from last round
// (because we re-parse the doc. TODO: just store the last doc
// then we don't have to be careful about how we add new elements)
$(this.selecta.body).find(".hnu").remove();
// Extract some infoz
var lastDoc = new hndoc($(this.selecta.body)),
newDoc = new hndoc(fetched.children()),
_this = this;
// Replace the current page DOM with the latest DOM
lastDoc.$.replaceWith(newDoc.$);
// Stretch the first column
$(this.selecta.firstColumn).addClass("hnu-col");
// Check if articles have changed
newDoc
.articleList
.each(function() {
var newVersion = this,
oldVersion = lastDoc
.articleList
.filter(function(){
return newVersion.id === this.id;
});
if(oldVersion.length) {
_this.updateArticle(newVersion, oldVersion[0]);
}
else {
_this.newArticle(newVersion);
}
});
// Hide runs of rises (probably means another story tanked)
this.removeRuns(newDoc.$.find(".hnu-up"));
this.removeRuns(newDoc.$.find(".hnu-down"));
// Check if karma has changed
if(newDoc.karma !== lastDoc.karma) {
this.setKarma(newDoc, lastDoc.karma);
}
},
// Update the DOM to show last karma
setKarma: function(doc, oldKarma) {
var end = $(document.createTextNode(") | ")),
old = $("<del></del>")
.text(oldKarma)
.addClass("hnu hnu-karma");
doc.$karma().textContent = " (" + doc.karma;
old.insertAfter(doc.$karma());
end.insertAfter(old);
},
// Update the DOM to include the last lot of info
updateArticle: function(newDoc, oldDoc) {
// Article rose (or sank a lot)
if(newDoc.rank < oldDoc.rank || newDoc.rank - oldDoc.rank > 4) {
$("<span></span>")
.addClass("hnu")
.addClass(newDoc.rank > oldDoc.rank ? "hnu-down" : "hnu-up")
.text(Math.abs(oldDoc.rank - newDoc.rank))
.hide()
.prependTo(newDoc.$rank())
.fadeIn();
}
// Votes changed
if(newDoc.votes !== oldDoc.votes) {
$("<span></span>")
.addClass("hnu hnu-votes")
.text(oldDoc.votes)
.insertBefore(newDoc.$votes());
}
// Comment count change
if(newDoc.comments !== oldDoc.comments) {
$("<span></span>")
.addClass('hnu hnu-votes')
.text(oldDoc.comments)
.insertBefore(newDoc.$comments());
}
},
// Update the DOM to show the article is new
newArticle: function(newDoc) {
$("<span></span>")
.addClass("hnu hnu-new")
.text("+")
.hide()
.prependTo(newDoc.$rank())
.fadeIn();
},
// Remove runs of +1 rises (when another story nose-dives)
removeRuns: function(elements) {
var extractValuesFromElement = function(el) {
return {
rise: parseInt($(el).text(), 10),
rank: parseInt(el.nextSibling.textContent.replace(/[^0-9]/g,""), 10),
$: $(el)
};
},
sortByRank = function(a, b) { return a.rank - b.rank; },
groupIntoRuns = function(acc, el) {
var head = acc.length ? acc[acc.length - 1] : [],
isSeq = function(prev, el){
return el.rise === prev.rise &&
el.rank === prev.rank + 1;
};
if(head.length && isSeq(head[head.length - 1], el)) {
head.push(el);
}
else {
acc.push([el]);
}
return acc;
},
returnAnyLongRuns = function(el) { return el.length > 2; },
flattenRuns = function(acc, el) { return acc.concat(el); },
removeIndicator = function(el) {
el.$.fadeOut("fast", function() {
$(this).remove();
});
};
// Get all map/reduce-y on the green circles
elements
.get()
.map( extractValuesFromElement )
.sort( sortByRank )
.reduce( groupIntoRuns, [] )
.filter( returnAnyLongRuns )
.reduce( flattenRuns, [] )
.forEach( removeIndicator );
}
};
// Encapsulate an entire HN page
function hndoc($doc) {
this.$ = $doc;
var cache = {};
this.$header = function() {
return cache.header ||
(cache.header = this.$.find("tbody tr:eq(0) table"));
};
this.$articles = function() {
return cache.articles ||
(cache.articles = this.$.find("tbody tr:eq(3) table tr").slice(0, -2));
};
this.$userNode = function(){
return cache.userNode ||
(cache.userNode = this.$header().find("tr > td:last").children());
};
this.$karma = function(){
return cache.karma ||
(cache.karma = this.$userNode().contents()[1]);
};
this.isLoggedIn = this.$userNode().find("a:first").text().indexOf("login") === -1;
this.karma = ! this.isLoggedIn ? -1 : parseInt(this.$karma().textContent.replace(/[^0-9]/g,""), 10);
this.articleList = this.$articles()
.filter(function(ind) {
// Every third TR is the start of an article
return ind % 3 === 0;
})
.map(function() {
// Turn them into articles
return new hnarticle($(this));
});
}
// Encapsulate an individual article
function hnarticle($row) {
this.$ = $row;
var cache = {};
this.$rank = function(){
return cache.rank ||
(cache.rank = this.$.find("td:first"));
};
this.$votes = function() {
return cache.votes ||
(cache.votes = this.$.next().find("td:eq(1) span:first"));
};
this.$comments = function() {
return cache.comments ||
(cache.comments = this.$.next().find("td:eq(1) a:last"));
};
this.$posted = function() {
var contents = this.$.next().find("td:eq(1)").contents();
// If ! length > 3 then it's a "special" YC post
return cache.posted ||
(cache.posted = contents.length > 3 ? contents[3] : contents[0]);
};
this.id = parseInt((this.$.next().find("td:eq(1) span").attr("id") + "").slice(6), 10);
this.rank = parseInt(this.$rank().text().replace(/[^0-9]/g,""), 10);
this.votes = parseInt(this.$votes().text(), 10);
this.comments = (parseInt(this.$comments().text(), 10) || 0);
this.posted = this.$posted().textContent;
// Not grabbed: postedBy, article Name, URL
}