/* @licstart The following is the entire license notice for the JavaScript code in this page. Copyright (C) 2013-2014 neko259 The JavaScript code in this page is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License (GNU GPL) as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. The code is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU GPL for more details. As additional permission under GNU GPL version 3 section 7, you may distribute non-source (e.g., minimized or compacted) forms of that code without the copy of the GNU GPL normally required by section 4, provided you include this license notice and a URL through which recipients can access the Corresponding Source. @licend The above is the entire license notice for the JavaScript code in this page. */ var CLASS_POST = '.post'; var POST_ADDED = 0; var POST_UPDATED = 1; // TODO These need to be syncronized with board settings. var JS_AUTOUPDATE_PERIOD = 20000; // TODO This needs to be the same for attachment download time limit. var POST_AJAX_TIMEOUT = 30000; var BLINK_SPEED = 500; var ALLOWED_FOR_PARTIAL_UPDATE = [ 'refmap', 'post-info' ]; var ATTR_CLASS = 'class'; var ATTR_UID = 'data-uid'; var unreadPosts = 0; var documentOriginalTitle = ''; // Thread ID does not change, can be stored one time var threadId = $('div.thread').children(CLASS_POST).first().attr('id'); var blinkColor = $('
').css('background-color'); /** * Get diff of the posts from the current thread timestamp. * This is required if the browser was closed and some post updates were * missed. */ function getThreadDiff() { var all_posts = $('.post'); var uids = ''; var posts = all_posts; for (var i = 0; i < posts.length; i++) { uids += posts[i].getAttribute('data-uid') + ' '; } var data = { uids: uids, thread: threadId }; var diffUrl = '/api/diff_thread/'; $.post(diffUrl, data, function(data) { var updatedPosts = data.updated; var addedPostCount = 0; for (var i = 0; i < updatedPosts.length; i++) { var postText = updatedPosts[i]; var post = $(postText); if (updatePost(post) == POST_ADDED) { addedPostCount++; } } var hasMetaUpdates = updatedPosts.length > 0; if (hasMetaUpdates) { updateMetadataPanel(); } if (addedPostCount > 0) { updateBumplimitProgress(addedPostCount); } if (updatedPosts.length > 0) { showNewPostsTitle(addedPostCount); } // TODO Process removed posts if any $('.metapanel').attr('data-last-update', data.last_update); if (data.subscribed == 'True') { var favButton = $('#thread-fav-button .not_fav'); if (favButton.length > 0) { favButton.attr('value', 'unsubscribe'); favButton.removeClass('not_fav'); favButton.addClass('fav'); } } }, 'json' ) } /** * Add or update the post on html page. */ function updatePost(postHtml) { // This needs to be set on start because the page is scrolled after posts // are added or updated var bottom = isPageBottom(); var post = $(postHtml); var threadBlock = $('div.thread'); var postId = post.attr('id'); // If the post already exists, replace it. Otherwise add as a new one. var existingPosts = threadBlock.children('.post[id=' + postId + ']'); var type; if (existingPosts.size() > 0) { replacePartial(existingPosts.first(), post, false); post = existingPosts.first(); type = POST_UPDATED; } else { post.appendTo(threadBlock); if (bottom) { scrollToBottom(); } type = POST_ADDED; } processNewPost(post); return type; } /** * Initiate a blinking animation on a node to show it was updated. */ function blink(node) { node.effect('highlight', { color: blinkColor }, BLINK_SPEED); } function isPageBottom() { var scroll = $(window).scrollTop() / ($(document).height() - $(window).height()); return scroll == 1 } function enableJsUpdate() { setInterval(getThreadDiff, JS_AUTOUPDATE_PERIOD); return true; } function initAutoupdate() { return enableJsUpdate(); } function getReplyCount() { return $('.thread').children(CLASS_POST).length } function getImageCount() { return $('.thread').find('img').length } /** * Update post count, images count and last update time in the metadata * panel. */ function updateMetadataPanel() { var replyCountField = $('#reply-count'); var imageCountField = $('#image-count'); var replyCount = getReplyCount(); replyCountField.text(replyCount); var imageCount = getImageCount(); imageCountField.text(imageCount); var lastUpdate = $('.post:last').children('.post-info').first() .children('.pub_time').first().html(); if (lastUpdate !== '') { var lastUpdateField = $('#last-update'); lastUpdateField.html(lastUpdate); blink(lastUpdateField); } blink(replyCountField); blink(imageCountField); } /** * Update bumplimit progress bar */ function updateBumplimitProgress(postDelta) { var progressBar = $('#bumplimit_progress'); if (progressBar) { var postsToLimitElement = $('#left_to_limit'); var oldPostsToLimit = parseInt(postsToLimitElement.text()); var postCount = getReplyCount(); var bumplimit = postCount - postDelta + oldPostsToLimit; var newPostsToLimit = bumplimit - postCount; if (newPostsToLimit <= 0) { $('.bar-bg').remove(); } else { postsToLimitElement.text(newPostsToLimit); progressBar.width((100 - postCount / bumplimit * 100.0) + '%'); } } } /** * Show 'new posts' text in the title if the document is not visible to a user */ function showNewPostsTitle(newPostCount) { if (document.hidden) { if (documentOriginalTitle === '') { documentOriginalTitle = document.title; } unreadPosts = unreadPosts + newPostCount; var newTitle = null; if (unreadPosts > 0) { newTitle = '[' + unreadPosts + '] '; } else { newTitle = '* '; } newTitle += documentOriginalTitle; document.title = newTitle; document.addEventListener('visibilitychange', function() { if (documentOriginalTitle !== '') { document.title = documentOriginalTitle; documentOriginalTitle = ''; unreadPosts = 0; } document.removeEventListener('visibilitychange', null); }); } } /** * Clear all entered values in the form fields */ function resetForm(form) { form.find('input:text, input:password, input:file, select, textarea').val(''); form.find('input:radio, input:checkbox') .removeAttr('checked').removeAttr('selected'); $('.file_wrap').find('.file-thumb').remove(); $('#preview-text').hide(); } /** * When the form is posted, this method will be run as a callback */ function updateOnPost(response, statusText, xhr, form) { var json = $.parseJSON(response); var status = json.status; showAsErrors(form, ''); $('.post-form-w').unblock(); if (status === 'ok') { resetFormPosition(); resetForm(form); getThreadDiff(); scrollToBottom(); } else { var errors = json.errors; for (var i = 0; i < errors.length; i++) { var fieldErrors = errors[i]; var error = fieldErrors.errors; showAsErrors(form, error); } } } /** * Run js methods that are usually run on the document, on the new post */ function processNewPost(post) { addScriptsToPost(post); blink(post); } function replacePartial(oldNode, newNode, recursive) { if (!equalNodes(oldNode, newNode)) { // Update parent node attributes updateNodeAttr(oldNode, newNode, ATTR_CLASS); updateNodeAttr(oldNode, newNode, ATTR_UID); // Replace children var children = oldNode.children(); if (children.length == 0) { oldNode.replaceWith(newNode); } else { var newChildren = newNode.children(); newChildren.each(function(i) { var newChild = newChildren.eq(i); var newChildClass = newChild.attr(ATTR_CLASS); // Update only certain allowed blocks (e.g. not images) if (ALLOWED_FOR_PARTIAL_UPDATE.indexOf(newChildClass) > -1) { var oldChild = oldNode.children('.' + newChildClass); if (oldChild.length == 0) { oldNode.append(newChild); } else { if (!equalNodes(oldChild, newChild)) { if (recursive) { replacePartial(oldChild, newChild, false); } else { oldChild.replaceWith(newChild); } } } } }); } } } /** * Compare nodes by content */ function equalNodes(node1, node2) { return node1[0].outerHTML == node2[0].outerHTML; } /** * Update attribute of a node if it has changed */ function updateNodeAttr(oldNode, newNode, attrName) { var oldAttr = oldNode.attr(attrName); var newAttr = newNode.attr(attrName); if (oldAttr != newAttr) { oldNode.attr(attrName, newAttr); } } $(document).ready(function() { if (initAutoupdate()) { // Post form data over AJAX var threadId = $('div.thread').children('.post').first().attr('id'); var form = $('#form'); if (form.length > 0) { var options = { beforeSubmit: function(arr, form, options) { $('.post-form-w').block({ message: gettext('Sending message...') }); }, success: updateOnPost, error: function(xhr, textStatus, errorString) { var errorText = gettext('Server error: ') + textStatus; if (errorString) { errorText += ' / ' + errorString; } showAsErrors(form, errorText); $('.post-form-w').unblock(); }, url: '/api/add_post/' + threadId + '/', timeout: POST_AJAX_TIMEOUT }; form.ajaxForm(options); resetForm(form); } } });