##// END OF EJS Templates
Further updates to post partial replacement
neko259 -
r1315:d6b6426d default
parent child Browse files
Show More
@@ -1,411 +1,447 b''
1 1 /*
2 2 @licstart The following is the entire license notice for the
3 3 JavaScript code in this page.
4 4
5 5
6 6 Copyright (C) 2013-2014 neko259
7 7
8 8 The JavaScript code in this page is free software: you can
9 9 redistribute it and/or modify it under the terms of the GNU
10 10 General Public License (GNU GPL) as published by the Free Software
11 11 Foundation, either version 3 of the License, or (at your option)
12 12 any later version. The code is distributed WITHOUT ANY WARRANTY;
13 13 without even the implied warranty of MERCHANTABILITY or FITNESS
14 14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
15 15
16 16 As additional permission under GNU GPL version 3 section 7, you
17 17 may distribute non-source (e.g., minimized or compacted) forms of
18 18 that code without the copy of the GNU GPL normally required by
19 19 section 4, provided you include this license notice and a URL
20 20 through which recipients can access the Corresponding Source.
21 21
22 22 @licend The above is the entire license notice
23 23 for the JavaScript code in this page.
24 24 */
25 25
26 26 var CLASS_POST = '.post'
27 27
28 28 var POST_ADDED = 0;
29 29 var POST_UPDATED = 1;
30 30
31 31 var JS_AUTOUPDATE_PERIOD = 20000;
32 32
33 var ALLOWED_FOR_PARTIAL_UPDATE = [
34 'refmap',
35 'post-info'
36 ];
37
38 var ATTR_CLASS = 'class';
39
33 40 var wsUser = '';
34 41
35 42 var unreadPosts = 0;
36 43 var documentOriginalTitle = '';
37 44
38 45 // Thread ID does not change, can be stored one time
39 46 var threadId = $('div.thread').children(CLASS_POST).first().attr('id');
40 47
41 48 /**
42 49 * Connect to websocket server and subscribe to thread updates. On any update we
43 50 * request a thread diff.
44 51 *
45 52 * @returns {boolean} true if connected, false otherwise
46 53 */
47 54 function connectWebsocket() {
48 55 var metapanel = $('.metapanel')[0];
49 56
50 57 var wsHost = metapanel.getAttribute('data-ws-host');
51 58 var wsPort = metapanel.getAttribute('data-ws-port');
52 59
53 60 if (wsHost.length > 0 && wsPort.length > 0) {
54 61 var centrifuge = new Centrifuge({
55 62 "url": 'ws://' + wsHost + ':' + wsPort + "/connection/websocket",
56 63 "project": metapanel.getAttribute('data-ws-project'),
57 64 "user": wsUser,
58 65 "timestamp": metapanel.getAttribute('data-ws-token-time'),
59 66 "token": metapanel.getAttribute('data-ws-token'),
60 67 "debug": false
61 68 });
62 69
63 70 centrifuge.on('error', function(error_message) {
64 71 console.log("Error connecting to websocket server.");
65 72 console.log(error_message);
66 73 console.log("Using javascript update instead.");
67 74
68 75 // If websockets don't work, enable JS update instead
69 76 enableJsUpdate()
70 77 });
71 78
72 79 centrifuge.on('connect', function() {
73 80 var channelName = 'thread:' + threadId;
74 81 centrifuge.subscribe(channelName, function(message) {
75 82 getThreadDiff();
76 83 });
77 84
78 85 // For the case we closed the browser and missed some updates
79 86 getThreadDiff();
80 87 $('#autoupdate').hide();
81 88 });
82 89
83 90 centrifuge.connect();
84 91
85 92 return true;
86 93 } else {
87 94 return false;
88 95 }
89 96 }
90 97
91 98 /**
92 99 * Get diff of the posts from the current thread timestamp.
93 100 * This is required if the browser was closed and some post updates were
94 101 * missed.
95 102 */
96 103 function getThreadDiff() {
97 104 var lastUpdateTime = $('.metapanel').attr('data-last-update');
98 105 var lastPostId = $('.post').last().attr('id');
99 106
100 107 var uids = '';
101 108 var posts = $('.post');
102 109 for (var i = 0; i < posts.length; i++) {
103 110 uids += posts[i].getAttribute('data-uid') + ' ';
104 111 }
105 112
106 113 var data = {
107 114 uids: uids,
108 115 thread: threadId
109 116 }
110 117
111 118 var diffUrl = '/api/diff_thread/';
112 119
113 120 $.post(diffUrl,
114 121 data,
115 122 function(data) {
116 123 var updatedPosts = data.updated;
117 124 var addedPostCount = 0;
118 125
119 126 for (var i = 0; i < updatedPosts.length; i++) {
120 127 var postText = updatedPosts[i];
121 128 var post = $(postText);
122 129
123 130 if (updatePost(post) == POST_ADDED) {
124 131 addedPostCount++;
125 132 }
126 133 }
127 134
128 135 var hasMetaUpdates = updatedPosts.length > 0;
129 136 if (hasMetaUpdates) {
130 137 updateMetadataPanel();
131 138 }
132 139
133 140 if (addedPostCount > 0) {
134 141 updateBumplimitProgress(addedPostCount);
135 142 }
136 143
137 144 if (updatedPosts.length > 0) {
138 145 showNewPostsTitle(addedPostCount);
139 146 }
140 147
141 148 // TODO Process removed posts if any
142 149 $('.metapanel').attr('data-last-update', data.last_update);
143 150 },
144 151 'json'
145 152 )
146 153 }
147 154
148 155 /**
149 156 * Add or update the post on html page.
150 157 */
151 158 function updatePost(postHtml) {
152 159 // This needs to be set on start because the page is scrolled after posts
153 160 // are added or updated
154 161 var bottom = isPageBottom();
155 162
156 163 var post = $(postHtml);
157 164
158 165 var threadBlock = $('div.thread');
159 166
160 167 var postId = post.attr('id');
161 168
162 169 // If the post already exists, replace it. Otherwise add as a new one.
163 170 var existingPosts = threadBlock.children('.post[id=' + postId + ']');
164 171
165 172 var type;
166 173
167 174 if (existingPosts.size() > 0) {
168 existingPosts.first().replaceWith(post);
175 replacePartial(existingPosts.first(), post, false);
176 post = existingPosts.first();
169 177
170 178 type = POST_UPDATED;
171 179 } else {
172 180 post.appendTo(threadBlock);
173 181
174 182 if (bottom) {
175 183 scrollToBottom();
176 184 }
177 185
178 186 type = POST_ADDED;
179 187 }
180 188
181 189 processNewPost(post);
182 190
183 191 return type;
184 192 }
185 193
186 194 /**
187 195 * Initiate a blinking animation on a node to show it was updated.
188 196 */
189 197 function blink(node) {
190 198 var blinkCount = 2;
191 199
192 200 var nodeToAnimate = node;
193 201 for (var i = 0; i < blinkCount; i++) {
194 202 nodeToAnimate = nodeToAnimate.fadeTo('fast', 0.5).fadeTo('fast', 1.0);
195 203 }
196 204 }
197 205
198 206 function isPageBottom() {
199 207 var scroll = $(window).scrollTop() / ($(document).height()
200 208 - $(window).height());
201 209
202 210 return scroll == 1
203 211 }
204 212
205 213 function enableJsUpdate() {
206 214 setInterval(getThreadDiff, JS_AUTOUPDATE_PERIOD);
207 215 return true;
208 216 }
209 217
210 218 function initAutoupdate() {
211 219 if (location.protocol === 'https:') {
212 220 return enableJsUpdate();
213 221 } else {
214 222 if (connectWebsocket()) {
215 223 return true;
216 224 } else {
217 225 return enableJsUpdate();
218 226 }
219 227 }
220 228 }
221 229
222 230 function getReplyCount() {
223 231 return $('.thread').children(CLASS_POST).length
224 232 }
225 233
226 234 function getImageCount() {
227 235 return $('.thread').find('img').length
228 236 }
229 237
230 238 /**
231 239 * Update post count, images count and last update time in the metadata
232 240 * panel.
233 241 */
234 242 function updateMetadataPanel() {
235 243 var replyCountField = $('#reply-count');
236 244 var imageCountField = $('#image-count');
237 245
238 246 replyCountField.text(getReplyCount());
239 247 imageCountField.text(getImageCount());
240 248
241 249 var lastUpdate = $('.post:last').children('.post-info').first()
242 250 .children('.pub_time').first().html();
243 251 if (lastUpdate !== '') {
244 252 var lastUpdateField = $('#last-update');
245 253 lastUpdateField.html(lastUpdate);
246 254 blink(lastUpdateField);
247 255 }
248 256
249 257 blink(replyCountField);
250 258 blink(imageCountField);
251 259 }
252 260
253 261 /**
254 262 * Update bumplimit progress bar
255 263 */
256 264 function updateBumplimitProgress(postDelta) {
257 265 var progressBar = $('#bumplimit_progress');
258 266 if (progressBar) {
259 267 var postsToLimitElement = $('#left_to_limit');
260 268
261 269 var oldPostsToLimit = parseInt(postsToLimitElement.text());
262 270 var postCount = getReplyCount();
263 271 var bumplimit = postCount - postDelta + oldPostsToLimit;
264 272
265 273 var newPostsToLimit = bumplimit - postCount;
266 274 if (newPostsToLimit <= 0) {
267 275 $('.bar-bg').remove();
268 276 } else {
269 277 postsToLimitElement.text(newPostsToLimit);
270 278 progressBar.width((100 - postCount / bumplimit * 100.0) + '%');
271 279 }
272 280 }
273 281 }
274 282
275 283 /**
276 284 * Show 'new posts' text in the title if the document is not visible to a user
277 285 */
278 286 function showNewPostsTitle(newPostCount) {
279 287 if (document.hidden) {
280 288 if (documentOriginalTitle === '') {
281 289 documentOriginalTitle = document.title;
282 290 }
283 291 unreadPosts = unreadPosts + newPostCount;
284 292
285 293 var newTitle = '* ';
286 294 if (unreadPosts > 0) {
287 295 newTitle += '[' + unreadPosts + '] ';
288 296 }
289 297 newTitle += documentOriginalTitle;
290 298
291 299 document.title = newTitle;
292 300
293 301 document.addEventListener('visibilitychange', function() {
294 302 if (documentOriginalTitle !== '') {
295 303 document.title = documentOriginalTitle;
296 304 documentOriginalTitle = '';
297 305 unreadPosts = 0;
298 306 }
299 307
300 308 document.removeEventListener('visibilitychange', null);
301 309 });
302 310 }
303 311 }
304 312
305 313 /**
306 314 * Clear all entered values in the form fields
307 315 */
308 316 function resetForm(form) {
309 317 form.find('input:text, input:password, input:file, select, textarea').val('');
310 318 form.find('input:radio, input:checkbox')
311 319 .removeAttr('checked').removeAttr('selected');
312 320 $('.file_wrap').find('.file-thumb').remove();
313 321 $('#preview-text').hide();
314 322 }
315 323
316 324 /**
317 325 * When the form is posted, this method will be run as a callback
318 326 */
319 327 function updateOnPost(response, statusText, xhr, form) {
320 328 var json = $.parseJSON(response);
321 329 var status = json.status;
322 330
323 331 showAsErrors(form, '');
324 332
325 333 if (status === 'ok') {
326 334 resetFormPosition();
327 335 resetForm(form);
328 336 getThreadDiff();
329 337 scrollToBottom();
330 338 } else {
331 339 var errors = json.errors;
332 340 for (var i = 0; i < errors.length; i++) {
333 341 var fieldErrors = errors[i];
334 342
335 343 var error = fieldErrors.errors;
336 344
337 345 showAsErrors(form, error);
338 346 }
339 347 }
340 348 }
341 349
342 350 /**
343 351 * Show text in the errors row of the form.
344 352 * @param form
345 353 * @param text
346 354 */
347 355 function showAsErrors(form, text) {
348 356 form.children('.form-errors').remove();
349 357
350 358 if (text.length > 0) {
351 359 var errorList = $('<div class="form-errors">' + text + '<div>');
352 360 errorList.appendTo(form);
353 361 }
354 362 }
355 363
356 364 /**
357 365 * Run js methods that are usually run on the document, on the new post
358 366 */
359 367 function processNewPost(post) {
360 368 addRefLinkPreview(post[0]);
361 369 highlightCode(post);
362 370 blink(post);
363 371 }
364 372
365 function replacePartial(oldNode, newNode) {
366 var oldContent = oldNode[0].outerHTML;
367 var newContent = newNode[0].outerHTML;
373 function replacePartial(oldNode, newNode, recursive) {
374 if (!equalNodes(oldNode, newNode)) {
375 // Update parent node class attribute
376 var oldClass = oldNode.attr(ATTR_CLASS);
377 var newClass = newNode.attr(ATTR_CLASS);
378 if (oldClass != newClass) {
379 oldNode.attr(ATTR_CLASS, newClass);
380 };
368 381
369 // TODO Handle different children sizes
370
371 if (oldContent != newContent) {
382 // Replace children
372 383 var children = oldNode.children();
373 384 if (children.length == 0) {
374 385 console.log(oldContent);
375 386 console.log(newContent)
376 387
377 388 oldNode.replaceWith(newNode);
378 389 } else {
379 390 var newChildren = newNode.children();
380 children.each(function(i) {
381 replacePartial(children.eq(i), newChildren.eq(i));
391 newChildren.each(function(i) {
392 var newChild = newChildren.eq(i);
393 var newChildClass = newChild.attr(ATTR_CLASS);
394
395 // Update only certain allowed blocks (e.g. not images)
396 if (ALLOWED_FOR_PARTIAL_UPDATE.indexOf(newChildClass) > -1) {
397 var oldChild = oldNode.children('.' + newChildClass);
398
399 if (oldChild.length == 0) {
400 oldNode.append(newChild);
401 } else {
402 if (!equalNodes(oldChild, newChild)) {
403 if (recursive) {
404 replacePartial(oldChild, newChild, false);
405 } else {
406 oldChild.replaceWith(newChild);
407 }
408 }
409 }
410 }
382 411 });
383 412 }
384 413 }
385 414 }
386 415
416 /**
417 * Compare nodes by content
418 */
419 function equalNodes(node1, node2) {
420 return node1[0].outerHTML == node2[0].outerHTML;
421 }
422
387 423 $(document).ready(function(){
388 424 if (initAutoupdate()) {
389 425 // Post form data over AJAX
390 426 var threadId = $('div.thread').children('.post').first().attr('id');
391 427
392 428 var form = $('#form');
393 429
394 430 if (form.length > 0) {
395 431 var options = {
396 432 beforeSubmit: function(arr, $form, options) {
397 433 showAsErrors($('form'), gettext('Sending message...'));
398 434 },
399 435 success: updateOnPost,
400 436 error: function() {
401 437 showAsErrors($('form'), gettext('Server error!'));
402 438 },
403 439 url: '/api/add_post/' + threadId + '/'
404 440 };
405 441
406 442 form.ajaxForm(options);
407 443
408 444 resetForm(form);
409 445 }
410 446 }
411 447 });
General Comments 0
You need to be logged in to leave comments. Login now