added tag input box to composer when creating a topic
added new routes for viewing tags 'tags' and 'tags/:tagname'
respectively
post_bar.tpl shows the tags of the topic
can edit the main post to remove or add new tags
added a new menu item to header to go to the tags page
v1.18.x
barisusakli 11 years ago
parent 1d7c293197
commit df73ceaeb7

@ -28,6 +28,7 @@
"header.admin": "Admin",
"header.recent": "Recent",
"header.unread": "Unread",
"header.tags": "Tags",
"header.popular": "Popular",
"header.users": "Users",
"header.chats": "Chats",

@ -0,0 +1,5 @@
{
"no_tag_topics": "There are no topics with this tag.",
"tags": "Tags",
"enter_tags_here": "Enter tags here..."
}

@ -0,0 +1,41 @@
'use strict';
/* globals define, app, socket */
define(['forum/recent', 'forum/infinitescroll'], function(recent, infinitescroll) {
var Tag = {};
Tag.init = function() {
app.enterRoom('tags');
if ($('body').height() <= $(window).height() && $('#topics-container').children().length >= 20) {
$('#load-more-btn').show();
}
$('#load-more-btn').on('click', function() {
loadMoreTopics();
});
infinitescroll.init(loadMoreTopics);
function loadMoreTopics(direction) {
if(direction < 0 || !$('#topics-container').length) {
return;
}
infinitescroll.loadMore('topics.loadMoreFromSet', {
set: 'tag:' + ajaxify.variables.get('tag') + ':topics',
after: $('#topics-container').attr('data-nextstart')
}, function(data) {
if (data.topics && data.topics.length) {
recent.onTopicsLoaded('tag', data.topics, false);
$('#topics-container').attr('data-nextstart', data.nextStart);
} else {
$('#load-more-btn').hide();
}
});
}
};
return Tag;
});

@ -3,8 +3,7 @@
/* globals define, app, socket */
define(['forum/recent', 'topicSelect', 'forum/infinitescroll'], function(recent, topicSelect, infinitescroll) {
var Unread = {},
loadingMoreTopics = false;
var Unread = {};
$(window).on('action:ajaxify.start', function(ev, data) {
if(data.url.indexOf('unread') !== 0) {

@ -2,7 +2,7 @@
/* globals define, socket, app, config, ajaxify, utils, translator, templates, bootbox */
define(['taskbar', 'composer/controls', 'composer/uploads', 'composer/formatting', 'composer/drafts'], function(taskbar, controls, uploads, formatting, drafts) {
define(['taskbar', 'composer/controls', 'composer/uploads', 'composer/formatting', 'composer/drafts', 'composer/tags'], function(taskbar, controls, uploads, formatting, drafts, tags) {
var composer = {
active: undefined,
posts: {}
@ -152,7 +152,8 @@ define(['taskbar', 'composer/controls', 'composer/uploads', 'composer/formatting
body: threadData.body,
modified: false,
isMain: !threadData.index,
topic_thumb: threadData.topic_thumb
topic_thumb: threadData.topic_thumb,
tags: threadData.tags
});
});
};
@ -176,8 +177,10 @@ define(['taskbar', 'composer/controls', 'composer/uploads', 'composer/formatting
function createNewComposer(post_uuid) {
var allowTopicsThumbnail = config.allowTopicsThumbnail && composer.posts[post_uuid].isMain && (config.hasImageUploadPlugin || config.allowFileUploads);
var isTopic = composer.posts[post_uuid] ? !!composer.posts[post_uuid].cid : false;
var isMain = composer.posts[post_uuid] ? !!composer.posts[post_uuid].isMain : false;
templates.parse('composer', {allowTopicsThumbnail: allowTopicsThumbnail}, function(composerTemplate) {
templates.parse('composer', {allowTopicsThumbnail: allowTopicsThumbnail, showTags: isTopic || isMain}, function(composerTemplate) {
translator.translate(composerTemplate, function(composerTemplate) {
composerTemplate = $(composerTemplate);
@ -185,10 +188,12 @@ define(['taskbar', 'composer/controls', 'composer/uploads', 'composer/formatting
$(document.body).append(composerTemplate);
activateReposition(post_uuid);
var postContainer = $(composerTemplate[0]);
tags.init(postContainer, composer.posts[post_uuid]);
activateReposition(post_uuid);
if(config.allowFileUploads || config.hasImageUploadPlugin) {
uploads.initialize(post_uuid);
}
@ -266,6 +271,8 @@ define(['taskbar', 'composer/controls', 'composer/uploads', 'composer/formatting
});
formatting.addComposerButtons();
});
});
}
@ -321,15 +328,16 @@ define(['taskbar', 'composer/controls', 'composer/uploads', 'composer/formatting
if (resizeActive) {
var position = (e.clientY + resizeOffset);
var newHeight = $(window).height() - position;
var paddingBottom = parseInt(postContainer.css('padding-bottom'), 10);
if(newHeight > $(window).height() - $('#header-menu').height() - 20) {
newHeight = $(window).height() - $('#header-menu').height() - 20;
} else if (newHeight < paddingBottom) {
newHeight = paddingBottom;
} else if (newHeight < 100) {
newHeight = 100;
}
postContainer.css('height', newHeight);
$('body').css({'margin-bottom': newHeight});
resizeTabContent(postContainer);
resizeSavePosition(newHeight);
}
e.preventDefault();
@ -395,6 +403,19 @@ define(['taskbar', 'composer/controls', 'composer/uploads', 'composer/formatting
$('body').css({'margin-bottom': postContainer.css('height')});
focusElements(post_uuid);
resizeTabContent(postContainer);
}
function resizeTabContent(postContainer) {
var h1 = postContainer.find('.title').outerHeight(true);
var h2 = postContainer.find('.tags-container').outerHeight(true);
var h3 = postContainer.find('.formatting-bar').outerHeight(true);
var h4 = postContainer.find('.nav-tabs').outerHeight(true);
var h5 = postContainer.find('.instructions').outerHeight(true);
var h6 = postContainer.find('.topic-thumb-container').outerHeight(true);
var h7 = $('.taskbar').height();
var total = h1 + h2 + h3 + h4 + h5 + h6 + h7;
postContainer.find('.tab-content').css('height', postContainer.height() - total);
}
function focusElements(post_uuid) {
@ -441,7 +462,8 @@ define(['taskbar', 'composer/controls', 'composer/uploads', 'composer/formatting
title: titleEl.val(),
content: bodyEl.val(),
topic_thumb: thumbEl.val() || '',
category_id: postData.cid
category_id: postData.cid,
tags: tags.getTags(post_uuid)
}, function(err, topic) {
done(err);
if (!err) {
@ -459,7 +481,8 @@ define(['taskbar', 'composer/controls', 'composer/uploads', 'composer/formatting
pid: postData.pid,
content: bodyEl.val(),
title: titleEl.val(),
topic_thumb: thumbEl.val() || ''
topic_thumb: thumbEl.val() || '',
tags: tags.getTags(post_uuid)
}, done);
}

@ -0,0 +1,60 @@
'use strict';
/*globals define*/
define(function() {
var tags = {};
tags.init = function(postContainer, postData) {
var tagEl = postContainer.find('.tags');
if (!tagEl.length) {
return;
}
tagEl.tagsinput();
addTags(postData.tags, tagEl);
var input = postContainer.find('.bootstrap-tagsinput input');
input.autocomplete({
delay: 100,
source: function(request, response) {
socket.emit('topics.searchTags', request.term, function(err, tags) {
if (err) {
return app.alertError(err.message)
}
if (tags) {
response(tags);
$('.ui-autocomplete a').attr('href', '#');
}
});
},
select: function(event, ui) {
// when autocomplete is selected from the dropdown simulate a enter key down to turn it into a tag
// http://stackoverflow.com/a/3276819/583363
var e = jQuery.Event('keydown');
e.which = 13;
e.keyCode = 13;
setTimeout(function() {
input.trigger(e);
}, 100);
}
});
input.attr('tabIndex', tagEl.attr('tabIndex'));
};
function addTags(tags, tagEl) {
if (tags && tags.length) {
for(var i=0; i<tags.length; ++i) {
tagEl.tagsinput('add', tags[i]);
}
}
}
tags.getTags = function(post_uuid) {
return $('#cmp-uuid-' + post_uuid + ' .tags').tagsinput('items');
};
return tags;
});

@ -0,0 +1,45 @@
.bootstrap-tagsinput {
background-color: #fff;
border: 1px solid #ccc;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
display: inline-block;
padding: 4px 6px;
margin-bottom: 10px;
color: #555;
vertical-align: middle;
border-radius: 4px;
max-width: 100%;
line-height: 22px;
}
.bootstrap-tagsinput input {
border: none;
box-shadow: none;
outline: none;
background-color: transparent;
padding: 0;
margin: 0;
width: auto !important;
max-width: inherit;
}
.bootstrap-tagsinput input:focus {
border: none;
box-shadow: none;
}
.bootstrap-tagsinput .tag {
margin-right: 2px;
color: white;
}
.bootstrap-tagsinput .tag [data-role="remove"] {
margin-left: 8px;
cursor: pointer;
}
.bootstrap-tagsinput .tag [data-role="remove"]:after {
content: "x";
padding: 0px 2px;
}
.bootstrap-tagsinput .tag [data-role="remove"]:hover {
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
}
.bootstrap-tagsinput .tag [data-role="remove"]:hover:active {
box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
}

File diff suppressed because one or more lines are too long

@ -10,7 +10,7 @@ var categoriesController = {},
topics = require('./../topics');
categoriesController.recent = function(req, res, next) {
var uid = (req.user) ? req.user.uid : 0;
var uid = req.user ? req.user.uid : 0;
topics.getLatestTopics(uid, 0, 19, req.params.term, function (err, data) {
if(err) {
return next(err);
@ -21,7 +21,7 @@ categoriesController.recent = function(req, res, next) {
};
categoriesController.popular = function(req, res, next) {
var uid = (req.user) ? req.user.uid : 0;
var uid = req.user ? req.user.uid : 0;
var set = 'topics:' + req.params.set;
if(!req.params.set) {
set = 'topics:posts';
@ -37,7 +37,7 @@ categoriesController.popular = function(req, res, next) {
};
categoriesController.unread = function(req, res, next) {
var uid = req.user.uid;
var uid = req.user ? req.user.uid : 0;
topics.getUnreadTopics(uid, 0, 20, function (err, data) {
if(err) {
@ -49,7 +49,7 @@ categoriesController.unread = function(req, res, next) {
};
categoriesController.unreadTotal = function(req, res, next) {
var uid = req.user.uid;
var uid = req.user ? req.user.uid : 0;
topics.getTotalUnread(uid, function (err, data) {
if(err) {

@ -2,6 +2,7 @@
var topicsController = require('./topics'),
categoriesController = require('./categories'),
tagsController = require('./tags'),
usersController = require('./users'),
accountsController = require('./accounts'),
staticController = require('./static'),
@ -24,6 +25,7 @@ var topicsController = require('./topics'),
var Controllers = {
topics: topicsController,
categories: categoriesController,
tags: tagsController,
users: usersController,
accounts: accountsController,
static: staticController,

@ -0,0 +1,53 @@
"use strict";
var tagsController = {},
async = require('async'),
topics = require('./../topics');
tagsController.getTag = function(req, res, next) {
var tag = req.params.tag;
var uid = req.user ? req.user.uid : 0;
topics.getTagTids(tag, 0, 19, function(err, tids) {
if (err) {
return next(err);
}
topics.getTopics('tag:' + tag + ':topics', uid, tids, function(err, data) {
if (err) {
return next(err);
}
data.tag = tag;
res.render('tag', data);
});
});
};
tagsController.getTags = function(req, res, next) {
topics.getTagsObjects(function(err, tags) {
if (err) {
return next(err);
}
async.map(tags, function(tag, next) {
topics.getTagTopicCount(tag.name, function(err, count) {
if (err) {
return next(err);
}
tag.topicCount = count;
next(null, tag);
});
}, function(err, tags) {
if (err) {
return next(err);
}
tags = tags.sort(function(a, b) {
return parseInt(b.topicCount, 10) - parseInt(a.topicCount, 10);
});
res.render('tags', {tags: tags});
});
});
};
module.exports = tagsController;

@ -235,6 +235,7 @@ var fs = require('fs'),
'vendor/jquery/js/jquery.form.min.js',
'vendor/jquery/serializeObject/jquery.ba-serializeobject.min.js',
'vendor/bootstrap/js/bootstrap.min.js',
'vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.min.js',
'vendor/requirejs/require.js',
'vendor/bootbox/bootbox.min.js',
'vendor/tinycon/tinycon.js',
@ -354,6 +355,7 @@ var fs = require('fs'),
}
source += '\n@import (inline) "..' + path.sep + '..' + path.sep + 'public/vendor/jquery/css/smoothness/jquery-ui-1.10.4.custom.min.css";';
source += '\n@import (inline) "..' + path.sep + '..' + path.sep + 'public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.css";';
var parser = new (less.Parser)({
paths: paths

@ -61,6 +61,8 @@ var winston = require('winston'),
topics.setTopicField(tid, 'thumb', options.topic_thumb);
topics.updateTags(tid, options.tags);
plugins.fireHook('action:topic.edit', tid);
}

@ -55,6 +55,15 @@ function topicRoutes(app, middleware, controllers) {
app.get('/api/topic/:topic_id/:slug?', controllers.topics.get);
}
function tagRoutes(app, middleware, controllers) {
app.get('/tags/:tag', middleware.buildHeader, controllers.tags.getTag);
app.get('/api/tags/:tag', controllers.tags.getTag);
app.get('/tags', middleware.buildHeader, controllers.tags.getTags);
app.get('/api/tags', controllers.tags.getTags);
}
function categoryRoutes(app, middleware, controllers) {
app.get('/popular/:set?', middleware.buildHeader, controllers.categories.popular);
app.get('/api/popular/:set?', controllers.categories.popular);
@ -152,6 +161,7 @@ module.exports = function(app, middleware) {
mainRoutes(app, middleware, controllers);
staticRoutes(app, middleware, controllers);
topicRoutes(app, middleware, controllers);
tagRoutes(app, middleware, controllers);
categoryRoutes(app, middleware, controllers);
accountRoutes(app, middleware, controllers);
userRoutes(app, middleware, controllers);

@ -49,7 +49,7 @@ var stopTracking = function(replyObj) {
};
SocketModules.composer.push = function(socket, pid, callback) {
posts.getPostFields(pid, ['content'], function(err, postData) {
posts.getPostFields(pid, ['content', 'tid'], function(err, postData) {
if(err || (!postData && !postData.content)) {
return callback(err || new Error('[[error:invalid-pid]]'));
}
@ -58,6 +58,9 @@ SocketModules.composer.push = function(socket, pid, callback) {
topic: function(next) {
topics.getTopicDataByPid(pid, next);
},
tags: function(next) {
topics.getTopicTags(postData.tid, next);
},
index: function(next) {
posts.getPidIndex(pid, next);
}
@ -71,6 +74,7 @@ SocketModules.composer.push = function(socket, pid, callback) {
body: postData.content,
title: results.topic.title,
topic_thumb: results.topic.thumb,
tags: results.tags,
index: results.index
});
});

@ -161,7 +161,7 @@ SocketPosts.edit = function(socket, data, callback) {
return callback(new Error('[[error:content-too-short, ' + meta.config.minimumPostLength + ']]'));
}
postTools.edit(socket.uid, data.pid, data.title, data.content, {topic_thumb: data.topic_thumb}, function(err, results) {
postTools.edit(socket.uid, data.pid, data.title, data.content, {topic_thumb: data.topic_thumb, tags: data.tags}, function(err, results) {
if(err) {
return callback(err);
}

@ -27,6 +27,7 @@ SocketTopics.post = function(socket, data, callback) {
content: data.content,
cid: data.category_id,
thumb: data.topic_thumb,
tags: data.tags,
req: websockets.reqFromSocket(socket)
}, function(err, result) {
if(err) {
@ -376,4 +377,8 @@ SocketTopics.getTidIndex = function(socket, tid, callback) {
categories.getTopicIndex(tid, callback);
};
SocketTopics.searchTags = function(socket, query, callback) {
topics.searchTags(query, callback);
};
module.exports = SocketTopics;

@ -20,6 +20,7 @@ var async = require('async'),
require('./topics/fork')(Topics);
require('./topics/posts')(Topics);
require('./topics/follow')(Topics);
require('./topics/tags')(Topics);
Topics.getTopicData = function(tid, callback) {
Topics.getTopicsData([tid], function(err, topics) {
@ -275,6 +276,9 @@ var async = require('async'),
},
threadTools: function(next) {
plugins.fireHook('filter:topic.thread_tools', [], next);
},
tags: function(next) {
Topics.getTopicTagsObjects(tid, next);
}
}, function(err, results) {
if (err) {
@ -283,6 +287,7 @@ var async = require('async'),
topicData.category = results.category;
topicData.posts = results.posts;
topicData.tags = results.tags;
topicData.thread_tools = results.threadTools;
topicData.pageCount = results.pageCount;
topicData.unreplied = parseInt(topicData.postcount, 10) === 1;

@ -60,6 +60,8 @@ module.exports = function(Topics) {
db.incrObjectField('category:' + cid, 'topic_count');
db.incrObjectField('global', 'topicCount');
Topics.createTags(data.tags, tid, timestamp);
callback(null, tid);
});
});
@ -111,7 +113,7 @@ module.exports = function(Topics) {
user.isReadyToPost(uid, next);
},
function(next) {
Topics.create({uid: uid, title: title, cid: cid, thumb: data.thumb}, next);
Topics.create({uid: uid, title: title, cid: cid, thumb: data.thumb, tags: data.tags}, next);
},
function(tid, next) {
Topics.reply({uid:uid, tid:tid, content:content, req: data.req}, next);

@ -0,0 +1,115 @@
'use strict';
var async = require('async'),
db = require('../database');
module.exports = function(Topics) {
Topics.createTags = function(tags, tid, timestamp) {
if(Array.isArray(tags)) {
for (var i=0; i<tags.length; ++i) {
tags[i] = tags[i].trim().toLowerCase();
db.sortedSetAdd('tag:' + tags[i] + ':topics', timestamp, tid);
db.setAdd('topic:' + tid + ':tags', tags[i]);
db.setAdd('tags', tags[i]);
}
}
};
Topics.getTagTids = function(tag, start, end, callback) {
db.getSortedSetRevRange('tag:' + tag + ':topics', start, end, callback);
};
Topics.getTagTopicCount = function(tag, callback) {
db.sortedSetCard('tag:' + tag + ':topics', callback);
};
Topics.getTags = function(callback) {
db.getSetMembers('tags', callback);
};
//returns tags as objects cuz templates.js cant do arrays yet >_>
Topics.getTagsObjects = function(callback) {
Topics.getTags(function(err, tags) {
callback(err, mapToObject(tags));
});
};
Topics.getTopicTags = function(tid, callback) {
db.getSetMembers('topic:' + tid + ':tags', callback);
};
//returns tags as objects cuz templates.js cant do arrays yet >_>
Topics.getTopicTagsObjects = function(tid, callback) {
Topics.getTopicTags(tid, function(err, tags) {
callback(err, mapToObject(tags));
});
};
function mapToObject(tags) {
if (!tags) {
return tags;
}
return tags.map(function(tag) {
return {name: tag};
});
}
Topics.updateTags = function(tid, tags) {
async.parallel({
timestamp: function(next) {
Topics.getTopicField(tid, 'timestamp', next);
},
currentTags: function(next) {
Topics.getTopicTags(tid, next);
}
}, function(err, results) {
removeTopicTags(tid, results.currentTags, function(err) {
if (!err) {
Topics.createTags(tags, tid, results.timestamp);
}
});
});
};
function removeTopicTags(tid, tags, callback) {
async.parallel([
function(next) {
db.delete('topic:' + tid + ':tags', next);
},
function(next) {
async.each(tags, function(tag, next) {
db.sortedSetRemove('tag:' + tag + ':topics', tid, next);
}, next);
}
], callback);
}
Topics.searchTags = function(query, callback) {
if (!query || query.length === 0) {
return callback(null, []);
}
db.getSetMembers('tags', function(err, tags) {
if (err) {
return callback(null, []);
}
query = query.toLowerCase();
var matches = [];
for(var i=0; i<tags.length; ++i) {
if (tags[i].toLowerCase().indexOf(query) === 0) {
matches.push(tags[i]);
}
}
matches = matches.slice(0, 10).sort(function(a, b) {
return a > b;
});
callback(null, matches);
});
};
};
Loading…
Cancel
Save