##// END OF EJS Templates
Block the form when message is being sent
neko259 -
r1637:db51ab04 default
parent child Browse files
Show More
@@ -1,460 +1,462 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 // TODO These need to be syncronized with board settings.
32 32 var JS_AUTOUPDATE_PERIOD = 20000;
33 33 // TODO This needs to be the same for attachment download time limit.
34 34 var POST_AJAX_TIMEOUT = 30000;
35 35 var BLINK_SPEED = 500;
36 36
37 37 var ALLOWED_FOR_PARTIAL_UPDATE = [
38 38 'refmap',
39 39 'post-info'
40 40 ];
41 41
42 42 var ATTR_CLASS = 'class';
43 43 var ATTR_UID = 'data-uid';
44 44
45 45 var wsUser = '';
46 46
47 47 var unreadPosts = 0;
48 48 var documentOriginalTitle = '';
49 49
50 50 // Thread ID does not change, can be stored one time
51 51 var threadId = $('div.thread').children(CLASS_POST).first().attr('id');
52 52 var blinkColor = $('<div class="post-blink"></div>').css('background-color');
53 53
54 54 /**
55 55 * Connect to websocket server and subscribe to thread updates. On any update we
56 56 * request a thread diff.
57 57 *
58 58 * @returns {boolean} true if connected, false otherwise
59 59 */
60 60 function connectWebsocket() {
61 61 var metapanel = $('.metapanel')[0];
62 62
63 63 var wsHost = metapanel.getAttribute('data-ws-host');
64 64 var wsPort = metapanel.getAttribute('data-ws-port');
65 65
66 66 if (wsHost.length > 0 && wsPort.length > 0) {
67 67 var centrifuge = new Centrifuge({
68 68 "url": 'ws://' + wsHost + ':' + wsPort + "/connection/websocket",
69 69 "project": metapanel.getAttribute('data-ws-project'),
70 70 "user": wsUser,
71 71 "timestamp": metapanel.getAttribute('data-ws-token-time'),
72 72 "token": metapanel.getAttribute('data-ws-token'),
73 73 "debug": false
74 74 });
75 75
76 76 centrifuge.on('error', function(error_message) {
77 77 console.log("Error connecting to websocket server.");
78 78 console.log(error_message);
79 79 console.log("Using javascript update instead.");
80 80
81 81 // If websockets don't work, enable JS update instead
82 82 enableJsUpdate()
83 83 });
84 84
85 85 centrifuge.on('connect', function() {
86 86 var channelName = 'thread:' + threadId;
87 87 centrifuge.subscribe(channelName, function(message) {
88 88 getThreadDiff();
89 89 });
90 90
91 91 // For the case we closed the browser and missed some updates
92 92 getThreadDiff();
93 93 $('#autoupdate').hide();
94 94 });
95 95
96 96 centrifuge.connect();
97 97
98 98 return true;
99 99 } else {
100 100 return false;
101 101 }
102 102 }
103 103
104 104 /**
105 105 * Get diff of the posts from the current thread timestamp.
106 106 * This is required if the browser was closed and some post updates were
107 107 * missed.
108 108 */
109 109 function getThreadDiff() {
110 110 var all_posts = $('.post');
111 111
112 112 var uids = '';
113 113 var posts = all_posts;
114 114 for (var i = 0; i < posts.length; i++) {
115 115 uids += posts[i].getAttribute('data-uid') + ' ';
116 116 }
117 117
118 118 var data = {
119 119 uids: uids,
120 120 thread: threadId
121 121 };
122 122
123 123 var diffUrl = '/api/diff_thread/';
124 124
125 125 $.post(diffUrl,
126 126 data,
127 127 function(data) {
128 128 var updatedPosts = data.updated;
129 129 var addedPostCount = 0;
130 130
131 131 for (var i = 0; i < updatedPosts.length; i++) {
132 132 var postText = updatedPosts[i];
133 133 var post = $(postText);
134 134
135 135 if (updatePost(post) == POST_ADDED) {
136 136 addedPostCount++;
137 137 }
138 138 }
139 139
140 140 var hasMetaUpdates = updatedPosts.length > 0;
141 141 if (hasMetaUpdates) {
142 142 updateMetadataPanel();
143 143 }
144 144
145 145 if (addedPostCount > 0) {
146 146 updateBumplimitProgress(addedPostCount);
147 147 }
148 148
149 149 if (updatedPosts.length > 0) {
150 150 showNewPostsTitle(addedPostCount);
151 151 }
152 152
153 153 // TODO Process removed posts if any
154 154 $('.metapanel').attr('data-last-update', data.last_update);
155 155
156 156 if (data.subscribed == 'True') {
157 157 var favButton = $('.not_fav');
158 158
159 159 if (favButton.length > 0) {
160 160 favButton.attr('value', 'unsubscribe');
161 161 favButton.removeClass('not_fav');
162 162 favButton.addClass('fav');
163 163 }
164 164 }
165 165 },
166 166 'json'
167 167 )
168 168 }
169 169
170 170 /**
171 171 * Add or update the post on html page.
172 172 */
173 173 function updatePost(postHtml) {
174 174 // This needs to be set on start because the page is scrolled after posts
175 175 // are added or updated
176 176 var bottom = isPageBottom();
177 177
178 178 var post = $(postHtml);
179 179
180 180 var threadBlock = $('div.thread');
181 181
182 182 var postId = post.attr('id');
183 183
184 184 // If the post already exists, replace it. Otherwise add as a new one.
185 185 var existingPosts = threadBlock.children('.post[id=' + postId + ']');
186 186
187 187 var type;
188 188
189 189 if (existingPosts.size() > 0) {
190 190 replacePartial(existingPosts.first(), post, false);
191 191 post = existingPosts.first();
192 192
193 193 type = POST_UPDATED;
194 194 } else {
195 195 post.appendTo(threadBlock);
196 196
197 197 if (bottom) {
198 198 scrollToBottom();
199 199 }
200 200
201 201 type = POST_ADDED;
202 202 }
203 203
204 204 processNewPost(post);
205 205
206 206 return type;
207 207 }
208 208
209 209 /**
210 210 * Initiate a blinking animation on a node to show it was updated.
211 211 */
212 212 function blink(node) {
213 213 node.effect('highlight', { color: blinkColor }, BLINK_SPEED);
214 214 }
215 215
216 216 function isPageBottom() {
217 217 var scroll = $(window).scrollTop() / ($(document).height()
218 218 - $(window).height());
219 219
220 220 return scroll == 1
221 221 }
222 222
223 223 function enableJsUpdate() {
224 224 setInterval(getThreadDiff, JS_AUTOUPDATE_PERIOD);
225 225 return true;
226 226 }
227 227
228 228 function initAutoupdate() {
229 229 if (location.protocol === 'https:') {
230 230 return enableJsUpdate();
231 231 } else {
232 232 if (connectWebsocket()) {
233 233 return true;
234 234 } else {
235 235 return enableJsUpdate();
236 236 }
237 237 }
238 238 }
239 239
240 240 function getReplyCount() {
241 241 return $('.thread').children(CLASS_POST).length
242 242 }
243 243
244 244 function getImageCount() {
245 245 return $('.thread').find('img').length
246 246 }
247 247
248 248 /**
249 249 * Update post count, images count and last update time in the metadata
250 250 * panel.
251 251 */
252 252 function updateMetadataPanel() {
253 253 var replyCountField = $('#reply-count');
254 254 var imageCountField = $('#image-count');
255 255
256 256 var replyCount = getReplyCount();
257 257 replyCountField.text(replyCount);
258 258 var imageCount = getImageCount();
259 259 imageCountField.text(imageCount);
260 260
261 261 var lastUpdate = $('.post:last').children('.post-info').first()
262 262 .children('.pub_time').first().html();
263 263 if (lastUpdate !== '') {
264 264 var lastUpdateField = $('#last-update');
265 265 lastUpdateField.html(lastUpdate);
266 266 blink(lastUpdateField);
267 267 }
268 268
269 269 blink(replyCountField);
270 270 blink(imageCountField);
271 271
272 272 $('#message-count-text').text(ngettext('message', 'messages', replyCount));
273 273 $('#image-count-text').text(ngettext('image', 'images', imageCount));
274 274 }
275 275
276 276 /**
277 277 * Update bumplimit progress bar
278 278 */
279 279 function updateBumplimitProgress(postDelta) {
280 280 var progressBar = $('#bumplimit_progress');
281 281 if (progressBar) {
282 282 var postsToLimitElement = $('#left_to_limit');
283 283
284 284 var oldPostsToLimit = parseInt(postsToLimitElement.text());
285 285 var postCount = getReplyCount();
286 286 var bumplimit = postCount - postDelta + oldPostsToLimit;
287 287
288 288 var newPostsToLimit = bumplimit - postCount;
289 289 if (newPostsToLimit <= 0) {
290 290 $('.bar-bg').remove();
291 291 } else {
292 292 postsToLimitElement.text(newPostsToLimit);
293 293 progressBar.width((100 - postCount / bumplimit * 100.0) + '%');
294 294 }
295 295 }
296 296 }
297 297
298 298 /**
299 299 * Show 'new posts' text in the title if the document is not visible to a user
300 300 */
301 301 function showNewPostsTitle(newPostCount) {
302 302 if (document.hidden) {
303 303 if (documentOriginalTitle === '') {
304 304 documentOriginalTitle = document.title;
305 305 }
306 306 unreadPosts = unreadPosts + newPostCount;
307 307
308 308 var newTitle = null;
309 309 if (unreadPosts > 0) {
310 310 newTitle = '[' + unreadPosts + '] ';
311 311 } else {
312 312 newTitle = '* ';
313 313 }
314 314 newTitle += documentOriginalTitle;
315 315
316 316 document.title = newTitle;
317 317
318 318 document.addEventListener('visibilitychange', function() {
319 319 if (documentOriginalTitle !== '') {
320 320 document.title = documentOriginalTitle;
321 321 documentOriginalTitle = '';
322 322 unreadPosts = 0;
323 323 }
324 324
325 325 document.removeEventListener('visibilitychange', null);
326 326 });
327 327 }
328 328 }
329 329
330 330 /**
331 331 * Clear all entered values in the form fields
332 332 */
333 333 function resetForm(form) {
334 334 form.find('input:text, input:password, input:file, select, textarea').val('');
335 335 form.find('input:radio, input:checkbox')
336 336 .removeAttr('checked').removeAttr('selected');
337 337 $('.file_wrap').find('.file-thumb').remove();
338 338 $('#preview-text').hide();
339 339 }
340 340
341 341 /**
342 342 * When the form is posted, this method will be run as a callback
343 343 */
344 344 function updateOnPost(response, statusText, xhr, form) {
345 345 var json = $.parseJSON(response);
346 346 var status = json.status;
347 347
348 348 showAsErrors(form, '');
349 $('.post-form-w').unblock();
349 350
350 351 if (status === 'ok') {
351 352 resetFormPosition();
352 353 resetForm(form);
353 354 getThreadDiff();
354 355 scrollToBottom();
355 356 } else {
356 357 var errors = json.errors;
357 358 for (var i = 0; i < errors.length; i++) {
358 359 var fieldErrors = errors[i];
359 360
360 361 var error = fieldErrors.errors;
361 362
362 363 showAsErrors(form, error);
363 364 }
364 365 }
365 366 }
366 367
367 368
368 369 /**
369 370 * Run js methods that are usually run on the document, on the new post
370 371 */
371 372 function processNewPost(post) {
372 373 addScriptsToPost(post);
373 374 blink(post);
374 375 }
375 376
376 377 function replacePartial(oldNode, newNode, recursive) {
377 378 if (!equalNodes(oldNode, newNode)) {
378 379 // Update parent node attributes
379 380 updateNodeAttr(oldNode, newNode, ATTR_CLASS);
380 381 updateNodeAttr(oldNode, newNode, ATTR_UID);
381 382
382 383 // Replace children
383 384 var children = oldNode.children();
384 385 if (children.length == 0) {
385 386 oldNode.replaceWith(newNode);
386 387 } else {
387 388 var newChildren = newNode.children();
388 389 newChildren.each(function(i) {
389 390 var newChild = newChildren.eq(i);
390 391 var newChildClass = newChild.attr(ATTR_CLASS);
391 392
392 393 // Update only certain allowed blocks (e.g. not images)
393 394 if (ALLOWED_FOR_PARTIAL_UPDATE.indexOf(newChildClass) > -1) {
394 395 var oldChild = oldNode.children('.' + newChildClass);
395 396
396 397 if (oldChild.length == 0) {
397 398 oldNode.append(newChild);
398 399 } else {
399 400 if (!equalNodes(oldChild, newChild)) {
400 401 if (recursive) {
401 402 replacePartial(oldChild, newChild, false);
402 403 } else {
403 404 oldChild.replaceWith(newChild);
404 405 }
405 406 }
406 407 }
407 408 }
408 409 });
409 410 }
410 411 }
411 412 }
412 413
413 414 /**
414 415 * Compare nodes by content
415 416 */
416 417 function equalNodes(node1, node2) {
417 418 return node1[0].outerHTML == node2[0].outerHTML;
418 419 }
419 420
420 421 /**
421 422 * Update attribute of a node if it has changed
422 423 */
423 424 function updateNodeAttr(oldNode, newNode, attrName) {
424 425 var oldAttr = oldNode.attr(attrName);
425 426 var newAttr = newNode.attr(attrName);
426 427 if (oldAttr != newAttr) {
427 428 oldNode.attr(attrName, newAttr);
428 429 }
429 430 }
430 431
431 432 $(document).ready(function() {
432 433 if (initAutoupdate()) {
433 434 // Post form data over AJAX
434 435 var threadId = $('div.thread').children('.post').first().attr('id');
435 436
436 437 var form = $('#form');
437 438
438 439 if (form.length > 0) {
439 440 var options = {
440 441 beforeSubmit: function(arr, form, options) {
441 showAsErrors(form, gettext('Sending message...'));
442 $('.post-form-w').block({ message: gettext('Sending message...') });
442 443 },
443 444 success: updateOnPost,
444 445 error: function(xhr, textStatus, errorString) {
445 446 var errorText = gettext('Server error: ') + textStatus;
446 447 if (errorString) {
447 448 errorText += ' / ' + errorString;
448 449 }
449 450 showAsErrors(form, errorText);
451 $('.post-form-w').unblock();
450 452 },
451 453 url: '/api/add_post/' + threadId + '/',
452 454 timeout: POST_AJAX_TIMEOUT
453 455 };
454 456
455 457 form.ajaxForm(options);
456 458
457 459 resetForm(form);
458 460 }
459 461 }
460 462 });
General Comments 0
You need to be logged in to leave comments. Login now