closes #1556
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 pagev1.18.x
parent
1d7c293197
commit
df73ceaeb7
@ -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;
|
||||||
|
});
|
@ -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
@ -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;
|
@ -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…
Reference in New Issue