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