f2b1e3bedb
Post uploads no longer encapsulates file content in base64. This means dramatic speed up for sending on local networks.
646 lines
16 KiB
JavaScript
646 lines
16 KiB
JavaScript
var App = App || {};
|
|
App.Presenters = App.Presenters || {};
|
|
|
|
App.Presenters.PostUploadPresenter = function(
|
|
_,
|
|
jQuery,
|
|
keyboard,
|
|
promise,
|
|
util,
|
|
api,
|
|
auth,
|
|
router,
|
|
tagList,
|
|
topNavigationPresenter,
|
|
messagePresenter) {
|
|
|
|
var KEY_RETURN = 13;
|
|
|
|
var $el = jQuery('#content');
|
|
var $messages;
|
|
var templates = {};
|
|
var allPosts = [];
|
|
var tagInput;
|
|
var fileDropper;
|
|
var interactionEnabled = true;
|
|
var currentUploadId = null;
|
|
var currentUploadXhr = null;
|
|
var maxPostSize = 0;
|
|
|
|
function init(params, loaded) {
|
|
topNavigationPresenter.select('upload');
|
|
topNavigationPresenter.changeTitle('Upload');
|
|
maxPostSize = parseInt(jQuery('head').attr('data-max-post-size'));
|
|
|
|
promise.wait(util.promiseTemplate('post-upload'))
|
|
.then(function(template) {
|
|
templates.upload = template;
|
|
render();
|
|
loaded();
|
|
}).fail(function() {
|
|
console.log(arguments);
|
|
loaded();
|
|
});
|
|
}
|
|
|
|
function render() {
|
|
$el.html(templates.upload({
|
|
canUploadPostsAnonymously: auth.hasPrivilege(auth.privileges.uploadPostsAnonymously)
|
|
}));
|
|
$messages = $el.find('.messages');
|
|
|
|
tagInput = new App.Controls.TagInput($el.find('form [name=tags]'));
|
|
fileDropper = new App.Controls.FileDropper($el.find('[name=post-content]'));
|
|
fileDropper.onChange = fileHandlerChanged;
|
|
|
|
$el.find('.url-handler input').keydown(urlHandlerKeyPressed);
|
|
$el.find('.url-handler button').click(urlHandlerButtonClicked);
|
|
$el.find('thead th.checkbox').click(postTableSelectAllCheckboxClicked);
|
|
|
|
keyboard.keydown('ctrl+up', selectPrevPostTableRow);
|
|
keyboard.keydown('ctrl+down', selectNextPostTableRow);
|
|
keyboard.keyup('q', tagInput.focus);
|
|
|
|
$el.find('.remove').click(removeButtonClicked);
|
|
$el.find('.move-up').click(moveUpButtonClicked);
|
|
$el.find('.move-down').click(moveDownButtonClicked);
|
|
$el.find('.upload').click(uploadButtonClicked);
|
|
$el.find('.stop').click(stopButtonClicked);
|
|
}
|
|
|
|
function getDefaultPost() {
|
|
return {
|
|
safety: 'safe',
|
|
source: null,
|
|
anonymous: false,
|
|
tags: [],
|
|
|
|
fileName: null,
|
|
content: null,
|
|
url: null,
|
|
thumbnail: null,
|
|
$tableRow: null,
|
|
};
|
|
}
|
|
|
|
function urlHandlerKeyPressed(e) {
|
|
if (e.which !== KEY_RETURN) {
|
|
return;
|
|
}
|
|
|
|
e.preventDefault();
|
|
$el.find('.url-handler button').trigger('click');
|
|
}
|
|
|
|
function urlHandlerButtonClicked(e) {
|
|
var $input = $el.find('.url-handler input');
|
|
|
|
var url = $input.val().trim();
|
|
if (url === '') {
|
|
return;
|
|
}
|
|
|
|
var protocol = /^(\w+):\/\//.exec(url);
|
|
if (!protocol) {
|
|
url = 'http://' + url;
|
|
} else {
|
|
protocol = protocol[1].toLowerCase();
|
|
if (protocol !== 'http' && protocol !== 'https') {
|
|
window.alert('Unsupported protocol: ' + protocol);
|
|
return;
|
|
}
|
|
}
|
|
$input.val('');
|
|
var post = addPostFromUrl(url);
|
|
selectPostTableRow(post);
|
|
}
|
|
|
|
function fileHandlerChanged(files) {
|
|
if (files.length > 0) {
|
|
var posts = [];
|
|
for (var i = 0; i < files.length; i ++) {
|
|
if (files[i].size > maxPostSize) {
|
|
window.alert('File "' + files[i].name + '" is too big - ignoring.' +
|
|
String.fromCharCode(10) +
|
|
'(max allowed file size = ' + maxPostSize + ' bytes)');
|
|
continue;
|
|
}
|
|
var post = addPostFromFile(files[i]);
|
|
posts.push(post);
|
|
}
|
|
selectPostTableRow(_.first(posts));
|
|
}
|
|
}
|
|
|
|
function postAdded(post) {
|
|
var allPosts = getAllPosts();
|
|
allPosts.push(post);
|
|
setAllPosts(allPosts);
|
|
createPostTableRow(post);
|
|
}
|
|
|
|
function postChanged(post) {
|
|
updatePostTableRow(post);
|
|
}
|
|
|
|
function postThumbnailLoaded(post) {
|
|
var selectedPosts = getSelectedPosts();
|
|
if (selectedPosts.length === 1 && selectedPosts[0] === post && post.thumbnail !== null) {
|
|
updatePostThumbnailInForm(post);
|
|
}
|
|
updatePostThumbnailInTable(post);
|
|
}
|
|
|
|
function postTableRowClicked(e) {
|
|
e.preventDefault();
|
|
if (!interactionEnabled) {
|
|
return;
|
|
}
|
|
var $allCheckboxes = jQuery(this).parents('table').find('tbody input[type=checkbox]');
|
|
var $myCheckbox = jQuery(this).parents('tr').find('input[type=checkbox]');
|
|
$allCheckboxes.prop('checked', false);
|
|
$myCheckbox.prop('checked', true);
|
|
postTableCheckboxesChanged(e);
|
|
}
|
|
|
|
function postTableCheckboxClicked(e) {
|
|
if (!interactionEnabled) {
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
if (e.target.nodeName === 'TD') {
|
|
var checkbox = jQuery(this).find('input[type=checkbox]');
|
|
checkbox.prop('checked', !checkbox.prop('checked'));
|
|
}
|
|
postTableCheckboxesChanged(e);
|
|
}
|
|
|
|
function postTableSelectAllCheckboxClicked(e) {
|
|
if (!interactionEnabled) {
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
var $checkbox = jQuery(this).find('input[type=checkbox]');
|
|
if (e.target.nodeName === 'TH') {
|
|
$checkbox.prop('checked', !$checkbox.prop('checked'));
|
|
}
|
|
$el.find('tbody input[type=checkbox]').prop('checked', $checkbox.prop('checked'));
|
|
postTableCheckboxesChanged();
|
|
}
|
|
|
|
function postTableCheckboxesChanged(e) {
|
|
if (!interactionEnabled) {
|
|
if (typeof(e) !== 'undefined') {
|
|
e.preventDefault();
|
|
}
|
|
return;
|
|
}
|
|
|
|
var $table = $el.find('table');
|
|
$table.find('tbody tr').each(function(i, row) {
|
|
var $row = jQuery(row);
|
|
var checked = $row.find('input[type=checkbox]').prop('checked');
|
|
$row.toggleClass('selected', checked);
|
|
});
|
|
|
|
var allPosts = getAllPosts();
|
|
var selectedPosts = getSelectedPosts();
|
|
$table.find('[name=select-all]').prop('checked', allPosts.length === selectedPosts.length);
|
|
postTableSelectionChanged(selectedPosts);
|
|
}
|
|
|
|
function postTableRowImageHovered(e) {
|
|
var $img = jQuery(this);
|
|
if ($img.parents('tr').data('post').thumbnail) {
|
|
var $lightbox = jQuery('#lightbox');
|
|
$lightbox.find('img').attr('src', $img.attr('src'));
|
|
$lightbox
|
|
.show()
|
|
.css({
|
|
left: ($img.position().left + $img.outerWidth()) + 'px',
|
|
top: ($img.position().top + ($img.outerHeight() - $lightbox.outerHeight()) / 2) + 'px',
|
|
});
|
|
}
|
|
}
|
|
|
|
function postTableRowImageUnhovered(e) {
|
|
jQuery('#lightbox').hide();
|
|
}
|
|
|
|
function removeButtonClicked(e) {
|
|
e.preventDefault();
|
|
removePosts(getSelectedPosts());
|
|
}
|
|
|
|
function moveUpButtonClicked(e) {
|
|
e.preventDefault();
|
|
movePostsUp(getSelectedPosts());
|
|
}
|
|
|
|
function moveDownButtonClicked(e) {
|
|
e.preventDefault();
|
|
movePostsDown(getSelectedPosts());
|
|
}
|
|
|
|
function uploadButtonClicked(e) {
|
|
e.preventDefault();
|
|
if (!interactionEnabled) {
|
|
return;
|
|
}
|
|
startUpload();
|
|
}
|
|
|
|
function stopButtonClicked(e) {
|
|
e.preventDefault();
|
|
stopUpload();
|
|
}
|
|
|
|
function addPostFromFile(file) {
|
|
var post = _.extend({}, getDefaultPost(), {fileName: file.name, file: file});
|
|
|
|
fileDropper.readAsDataURL(file, function(content) {
|
|
if (file.type.match('image.*')) {
|
|
post.thumbnail = content;
|
|
postThumbnailLoaded(post);
|
|
}
|
|
});
|
|
|
|
postAdded(post);
|
|
return post;
|
|
}
|
|
|
|
function addPostFromUrl(url) {
|
|
var post = _.extend({}, getDefaultPost(), {url: url, fileName: url});
|
|
postAdded(post);
|
|
setPostsSource([post], url);
|
|
|
|
var matches = url.match(/watch.*?=([a-zA-Z0-9_-]+)/);
|
|
if (matches) {
|
|
var youtubeThumbnailUrl = 'http://img.youtube.com/vi/' + matches[1] + '/mqdefault.jpg';
|
|
post.thumbnail = youtubeThumbnailUrl;
|
|
postThumbnailLoaded(post);
|
|
} else if (url.match(/image|img|jpg|png|gif/i)) {
|
|
post.thumbnail = url;
|
|
postThumbnailLoaded(post);
|
|
}
|
|
return post;
|
|
}
|
|
|
|
function createPostTableRow(post) {
|
|
var $table = $el.find('table');
|
|
var $row = $table.find('.template').clone(true);
|
|
|
|
post.$tableRow = $row;
|
|
|
|
$row.removeClass('template');
|
|
$row.find('td:not(.checkbox)').click(postTableRowClicked);
|
|
$row.find('td.checkbox').click(postTableCheckboxClicked);
|
|
$row.find('img').mouseenter(postTableRowImageHovered);
|
|
$row.find('img').mouseleave(postTableRowImageUnhovered);
|
|
$row.data('post', post);
|
|
$table.find('tbody').append($row);
|
|
$row.find('td.checkbox input').attr('id', _.uniqueId());
|
|
$row.find('td.checkbox label').attr('for', $row.find('td.checkbox input').attr('id'));
|
|
|
|
postChanged(post);
|
|
|
|
showOrHidePostsTable();
|
|
}
|
|
|
|
function updatePostTableRow(post) {
|
|
var $row = post.$tableRow;
|
|
$row.find('.tags').text(post.tags.join(', ') || '-');
|
|
$row.find('.safety div').attr('class', 'safety-' + post.safety);
|
|
}
|
|
|
|
function updatePostThumbnailInForm(post) {
|
|
if (post.thumbnail === null) {
|
|
$el.find('.form-slider .thumbnail img').hide();
|
|
} else {
|
|
$el.find('.form-slider .thumbnail img').show()[0].setAttribute('src', post.thumbnail);
|
|
}
|
|
}
|
|
|
|
function updatePostThumbnailInTable(post) {
|
|
var $row = post.$tableRow;
|
|
if (post.thumbnail === null) {
|
|
$row.find('img')[0].setAttribute('src', util.transparentPixel());
|
|
//huge speedup thanks to this condition
|
|
} else if ($row.find('img').attr('src') !== post.thumbnail) {
|
|
$row.find('img')[0].setAttribute('src', post.thumbnail);
|
|
}
|
|
}
|
|
|
|
function getAllPosts() {
|
|
return allPosts;
|
|
}
|
|
|
|
function setAllPosts(newPosts) {
|
|
allPosts = newPosts;
|
|
}
|
|
|
|
function syncPostsWithTable() {
|
|
setAllPosts(_.map($el.find('tbody tr'), function(row) {
|
|
return jQuery(row).data('post');
|
|
}));
|
|
}
|
|
|
|
function getSelectedPosts() {
|
|
return _.map($el.find('tbody tr.selected'), function(row) {
|
|
return jQuery(row).data('post');
|
|
});
|
|
}
|
|
|
|
function postTableSelectionChanged(selectedPosts) {
|
|
messagePresenter.hideMessages($messages);
|
|
if (selectedPosts.length === 0) {
|
|
hidePostEditForm();
|
|
} else {
|
|
tagInput.hideSuggestions();
|
|
showPostEditForm(selectedPosts);
|
|
}
|
|
$el.find('.post-table-op').prop('disabled', selectedPosts.length === 0);
|
|
}
|
|
|
|
function hidePostEditForm() {
|
|
var $postEditForm = $el.find('form');
|
|
$postEditForm.parent('.form-slider').slideUp(function() {
|
|
$postEditForm.find('.thumbnail').hide();
|
|
});
|
|
}
|
|
|
|
function showPostEditForm(selectedPosts) {
|
|
var $postEditForm = $el.find('form');
|
|
$postEditForm.parent('.form-slider').slideDown();
|
|
if (selectedPosts.length !== 1) {
|
|
$postEditForm.parent('.form-slider').find('.thumbnail').slideUp();
|
|
$postEditForm.find('.file-name strong').text('Multiple posts selected');
|
|
} else {
|
|
var post = selectedPosts[0];
|
|
$postEditForm.parent('.form-slider').find('.thumbnail').slideDown();
|
|
$postEditForm.find('.file-name strong').text(post.fileName || post.url);
|
|
updatePostThumbnailInForm(post);
|
|
}
|
|
|
|
var combinedPost = getCombinedPost(selectedPosts);
|
|
|
|
$postEditForm.find('[name=source]').val(combinedPost.source);
|
|
$postEditForm.find('[name=anonymous]').prop('checked', combinedPost.anonymous);
|
|
$postEditForm.find('[name=safety]').prop('checked', false);
|
|
if (combinedPost.safety !== null) {
|
|
$postEditForm.find('[name=safety][value=' + combinedPost.safety + ']').prop('checked', true);
|
|
}
|
|
tagInput.setTags(combinedPost.tags);
|
|
|
|
$postEditForm.find('[name=source]').unbind('change').bind('change', function(e) {
|
|
setPostsSource(selectedPosts, jQuery(this).val());
|
|
});
|
|
$postEditForm.find('[name=safety]').unbind('change').bind('change', function(e) {
|
|
setPostsSafety(selectedPosts, jQuery(this).val());
|
|
});
|
|
$postEditForm.find('[name=anonymous]').unbind('change').bind('change', function(e) {
|
|
setPostsAnonymity(selectedPosts, jQuery(this).is(':checked'));
|
|
});
|
|
tagInput.beforeTagAdded = function(tag) {
|
|
addTagToPosts(selectedPosts, tag);
|
|
};
|
|
tagInput.beforeTagRemoved = function(tag) {
|
|
removeTagFromPosts(selectedPosts, tag);
|
|
};
|
|
}
|
|
|
|
function getCombinedPost(posts) {
|
|
var combinedPost = _.extend({}, getDefaultPost());
|
|
if (posts.length === 0) {
|
|
return combinedPost;
|
|
}
|
|
_.extend(combinedPost, posts[0]);
|
|
|
|
var tagFilter = function(post) {
|
|
return function(tag) {
|
|
return post.tags.indexOf(tag) !== -1;
|
|
};
|
|
};
|
|
|
|
for (var i = 1; i < posts.length; i ++) {
|
|
if (posts[i].safety !== posts[0].safety) {
|
|
combinedPost.safety = null;
|
|
}
|
|
if (posts[i].anonymous !== posts[0].anonymous) {
|
|
combinedPost.anonymous = null;
|
|
}
|
|
if (posts[i].source !== posts[0].source) {
|
|
combinedPost.source = null;
|
|
}
|
|
combinedPost.tags = combinedPost.tags.filter(tagFilter(posts[i]));
|
|
}
|
|
|
|
return combinedPost;
|
|
}
|
|
|
|
function setPostsSource(posts, newSource) {
|
|
_.each(posts, function(post) {
|
|
var maxSourceLength = 200;
|
|
if (newSource.length > maxSourceLength) {
|
|
newSource = newSource.substring(0, maxSourceLength - 5) + '(...)';
|
|
}
|
|
post.source = newSource;
|
|
postChanged(post);
|
|
});
|
|
}
|
|
|
|
function setPostsSafety(posts, newSafety) {
|
|
_.each(posts, function(post) {
|
|
post.safety = newSafety;
|
|
postChanged(post);
|
|
});
|
|
}
|
|
|
|
function setPostsAnonymity(posts, isAnonymous) {
|
|
_.each(posts, function(post) {
|
|
post.anonymous = isAnonymous;
|
|
postChanged(post);
|
|
});
|
|
}
|
|
|
|
function addTagToPosts(posts, tag) {
|
|
jQuery.each(posts, function(i, post) {
|
|
var index = post.tags.indexOf(tag);
|
|
if (index === -1) {
|
|
post.tags.push(tag);
|
|
}
|
|
postChanged(post);
|
|
});
|
|
}
|
|
|
|
function removeTagFromPosts(posts, tag) {
|
|
jQuery.each(posts, function(i, post) {
|
|
var index = post.tags.indexOf(tag);
|
|
if (index !== -1) {
|
|
post.tags.splice(index, 1);
|
|
}
|
|
postChanged(post);
|
|
});
|
|
}
|
|
|
|
function selectPostTableRow(post) {
|
|
if (post) {
|
|
var $table = $el.find('table');
|
|
$table.find('tbody input[type=checkbox]').prop('checked', false);
|
|
$table.find('tbody tr').each(function(i, row) {
|
|
var $row = jQuery(row);
|
|
if (post === $row.data('post')) {
|
|
$row.find('input[type=checkbox]').prop('checked', true);
|
|
return false;
|
|
}
|
|
});
|
|
postTableCheckboxesChanged();
|
|
}
|
|
}
|
|
|
|
function selectPrevPostTableRow() {
|
|
selectPostTableRow($el.find('tbody tr.selected:eq(0)').prev().data('post'));
|
|
}
|
|
|
|
function selectNextPostTableRow() {
|
|
selectPostTableRow($el.find('tbody tr.selected:eq(0)').next().data('post'));
|
|
}
|
|
|
|
function showOrHidePostsTable() {
|
|
if (getAllPosts().length === 0) {
|
|
util.disableExitConfirmation();
|
|
$el.find('#post-upload-step2').fadeOut();
|
|
} else {
|
|
util.enableExitConfirmation();
|
|
$el.find('#post-upload-step2').fadeIn();
|
|
}
|
|
}
|
|
|
|
function removePosts(posts) {
|
|
_.each(posts, function(post) {
|
|
post.$tableRow.remove();
|
|
});
|
|
syncPostsWithTable();
|
|
showOrHidePostsTable();
|
|
postTableCheckboxesChanged();
|
|
}
|
|
|
|
function movePostsUp(posts) {
|
|
_.each(posts, function(post) {
|
|
var $row = post.$tableRow;
|
|
$row.insertBefore($row.prev('tr:not(.selected)'));
|
|
});
|
|
syncPostsWithTable();
|
|
}
|
|
|
|
function movePostsDown(posts) {
|
|
_.each(posts.reverse(), function(post) {
|
|
var $row = post.$tableRow;
|
|
$row.insertAfter($row.next('tr:not(.selected)'));
|
|
});
|
|
syncPostsWithTable();
|
|
}
|
|
|
|
function startUpload() {
|
|
$el.find('tbody input[type=checkbox]').prop('checked', false);
|
|
postTableCheckboxesChanged();
|
|
|
|
$el.find('.upload').hide();
|
|
$el.find('.stop').show();
|
|
interactionEnabled = false;
|
|
currentUploadId = Math.random();
|
|
uploadNextPost();
|
|
}
|
|
|
|
function stopUpload() {
|
|
currentUploadId = null;
|
|
showUploadError('Upload stopped.');
|
|
if (currentUploadXhr && currentUploadXhr.readystate !== api.AJAX_DONE) {
|
|
currentUploadXhr.abort();
|
|
}
|
|
}
|
|
|
|
function uploadNextPost() {
|
|
var priorUploadId = currentUploadId;
|
|
messagePresenter.hideMessages($messages);
|
|
|
|
var posts = getAllPosts();
|
|
if (posts.length === 0) {
|
|
onUploadCompleted();
|
|
return;
|
|
}
|
|
|
|
messagePresenter.showInfo($messages, 'Uploading in progress…');
|
|
var post = posts[0];
|
|
var $row = post.$tableRow;
|
|
|
|
var formData = new FormData();
|
|
if (post.url) {
|
|
formData.append('url', post.url);
|
|
} else {
|
|
formData.append('content', post.file);
|
|
formData.append('contentFileName', post.fileName);
|
|
}
|
|
formData.append('source', post.source);
|
|
formData.append('safety', post.safety);
|
|
formData.append('anonymous', (post.anonymous | 0));
|
|
formData.append('tags', post.tags.join(' '));
|
|
|
|
if (post.tags.length === 0) {
|
|
showUploadError('No tags set.');
|
|
return;
|
|
}
|
|
|
|
var apiPromise = api.post('/posts', formData);
|
|
currentUploadXhr = apiPromise.xhr;
|
|
|
|
promise.wait(apiPromise)
|
|
.then(function(response) {
|
|
$row.slideUp(function(response) {
|
|
if (priorUploadId === currentUploadId) {
|
|
$row.remove();
|
|
posts.shift();
|
|
setAllPosts(posts);
|
|
uploadNextPost();
|
|
}
|
|
});
|
|
}).fail(function(response) {
|
|
if (priorUploadId === currentUploadId) {
|
|
showUploadError(response.json && response.json.error || response);
|
|
}
|
|
});
|
|
}
|
|
|
|
function onUploadCompleted() {
|
|
util.disableExitConfirmation();
|
|
tagList.refreshTags();
|
|
router.navigate('#/posts');
|
|
}
|
|
|
|
function showUploadError(message) {
|
|
$el.find('.upload').show();
|
|
$el.find('.stop').hide();
|
|
messagePresenter.hideMessages($messages);
|
|
messagePresenter.showError($messages, message);
|
|
interactionEnabled = true;
|
|
}
|
|
|
|
return {
|
|
init: init,
|
|
render: render,
|
|
};
|
|
|
|
};
|
|
|
|
App.DI.register('postUploadPresenter', [
|
|
'_',
|
|
'jQuery',
|
|
'keyboard',
|
|
'promise',
|
|
'util',
|
|
'api',
|
|
'auth',
|
|
'router',
|
|
'tagList',
|
|
'topNavigationPresenter',
|
|
'messagePresenter'], App.Presenters.PostUploadPresenter);
|