mirror of
https://github.com/NodeBB/NodeBB.git
synced 2026-06-08 08:12:30 +02:00
feat: new search WIP
This commit is contained in:
@@ -1,21 +1,39 @@
|
||||
{
|
||||
"type-to-search": "Type to search",
|
||||
"results_matching": "%1 result(s) matching \"%2\", (%3 seconds)",
|
||||
"no-matches": "No matches found",
|
||||
"advanced-search": "Advanced Search",
|
||||
"in": "In",
|
||||
"titles": "Titles",
|
||||
"titles-posts": "Titles and Posts",
|
||||
"in-titles": "In titles",
|
||||
"in-titles-posts": "In titles and posts",
|
||||
"in-posts": "In post",
|
||||
"in-categories": "In categories",
|
||||
"in-users": "In users",
|
||||
"in-tags": "In tags",
|
||||
"categories": "Categories",
|
||||
"categories-x": "Categories: %!",
|
||||
"type-a-category": "Type a category",
|
||||
"tags": "Tags",
|
||||
"tags-x": "Tags: %1",
|
||||
"type-a-tag": "Type a tag",
|
||||
"match-words": "Match words",
|
||||
"match-all-words": "Match all words",
|
||||
"match-any-word": "Match any word",
|
||||
"all": "All",
|
||||
"any": "Any",
|
||||
"posted-by": "Posted by",
|
||||
"in-categories": "In Categories",
|
||||
"posted-by-usernames": "Posted by: %1",
|
||||
"type-a-username": "Type a username",
|
||||
"search-child-categories": "Search child categories",
|
||||
"has-tags": "Has tags",
|
||||
"reply-count": "Reply Count",
|
||||
"replies": "Replies",
|
||||
"replies-atleast-count": "Replies: At least %1",
|
||||
"replies-atmost-count": "Replies: At most %1",
|
||||
"at-least": "At least",
|
||||
"at-most": "At most",
|
||||
"relevance": "Relevance",
|
||||
"time": "Time",
|
||||
"post-time": "Post time",
|
||||
"votes": "Votes",
|
||||
"newer-than": "Newer than",
|
||||
@@ -28,7 +46,22 @@
|
||||
"three-months": "Three months",
|
||||
"six-months": "Six months",
|
||||
"one-year": "One year",
|
||||
"time-newer-than-86400": "Time: Newer than yesterday",
|
||||
"time-older-than-86400": "Time: Older than yesterday",
|
||||
"time-newer-than-604800": "Time: Newer than one week",
|
||||
"time-older-than-604800": "Time: Older than one week",
|
||||
"time-newer-than-1209600": "Time: Newer than two weeks",
|
||||
"time-older-than-1209600": "Time: Older than two weeks",
|
||||
"time-newer-than-2592000": "Time: Newer than one month",
|
||||
"time-older-than-2592000": "Time: Older than one month",
|
||||
"time-newer-than-7776000": "Time: Newer than three months",
|
||||
"time-older-than-7776000": "Time: Older than three months",
|
||||
"time-newer-than-15552000": "Time: Newer than six months",
|
||||
"time-older-than-15552000": "Time: Older than six months",
|
||||
"time-newer-than-31104000": "Time: Newer than one year",
|
||||
"time-older-than-31104000": "Time: Older than one year",
|
||||
"sort-by": "Sort by",
|
||||
"sort": "Sort",
|
||||
"last-reply-time": "Last reply time",
|
||||
"topic-title": "Topic title",
|
||||
"topic-votes": "Topic votes",
|
||||
@@ -39,11 +72,36 @@
|
||||
"category": "Category",
|
||||
"descending": "In descending order",
|
||||
"ascending": "In ascending order",
|
||||
"sort-by-relevance-desc": "Sort by: Relevance in descending order",
|
||||
"sort-by-relevance-asc": "Sort by: Relevance in ascending order ",
|
||||
"sort-by-timestamp-desc": "Sort by: Post time in descending order",
|
||||
"sort-by-timestamp-asc": "Sort by: Post time in ascending order ",
|
||||
"sort-by-votes-desc": "Sort by: Votes in descending order",
|
||||
"sort-by-votes-asc": "Sort by: Votes in ascending order ",
|
||||
"sort-by-topic.lastposttime-desc": "Sort by: Last reply time in descending order",
|
||||
"sort-by-topic.lastposttime-asc": "Sort by: Last reply time in ascending order ",
|
||||
"sort-by-topic.title-desc": "Sort by: Topic title in descending order",
|
||||
"sort-by-topic.title-asc": "Sort by: Topic title in ascending order ",
|
||||
"sort-by-topic.postcount-desc": "Sort by: Number of replies in descending order",
|
||||
"sort-by-topic.postcount-asc": "Sort by: Number of replies in ascending order ",
|
||||
"sort-by-topic.viewcount-desc": "Sort by: Number of views in descending order",
|
||||
"sort-by-topic.viewcount-asc": "Sort by: Number of views in ascending order ",
|
||||
"sort-by-topic.votes-desc": "Sort by: Topic votes in descending order",
|
||||
"sort-by-topic.votes-asc": "Sort by: Topic votes in ascending order ",
|
||||
"sort-by-topic.timestamp-desc": "Sort by: Topic start date in descending order",
|
||||
"sort-by-topic.timestamp-asc": "Sort by: Topic start date in ascending order ",
|
||||
"sort-by-user.username-desc": "Sort by: Username in descending order",
|
||||
"sort-by-user.username-asc": "Sort by: Username in ascending order ",
|
||||
"sort-by-category.name-desc": "Sort by: Category in descending order",
|
||||
"sort-by-category.name-asc": "Sort by: Category in ascending order ",
|
||||
"save": "Save",
|
||||
"save-preferences": "Save preferences",
|
||||
"clear-preferences": "Clear preferences",
|
||||
"search-preferences-saved": "Search preferences saved",
|
||||
"search-preferences-cleared": "Search preferences cleared",
|
||||
"show-results-as": "Show results as",
|
||||
"show-results-as-topics": "Show results as topics",
|
||||
"show-results-as-posts": "Show results as posts",
|
||||
"see-more-results": "See more results (%1)",
|
||||
"search-in-category": "Search in \"%1\""
|
||||
}
|
||||
|
||||
@@ -3,25 +3,29 @@
|
||||
|
||||
define('forum/search', [
|
||||
'search',
|
||||
'autocomplete',
|
||||
'storage',
|
||||
'hooks',
|
||||
'alerts',
|
||||
], function (searchModule, autocomplete, storage, hooks, alerts) {
|
||||
'api',
|
||||
'translator',
|
||||
'slugify',
|
||||
], function (searchModule, storage, hooks, alerts, api, translator, slugify) {
|
||||
const Search = {};
|
||||
let selectedUsers = [];
|
||||
|
||||
Search.init = function () {
|
||||
const searchQuery = $('#results').attr('data-search-query');
|
||||
|
||||
const searchIn = $('#search-in');
|
||||
|
||||
searchIn.on('change', function () {
|
||||
updateFormItemVisiblity(searchIn.val());
|
||||
});
|
||||
|
||||
searchModule.highlightMatches(searchQuery, $('.search-result-text p, .search-result-text.search-result-title a'));
|
||||
const searchQuery = $('#results').attr('data-search-query');
|
||||
searchModule.highlightMatches(
|
||||
searchQuery,
|
||||
$('.search-results .content p, .search-results .topic-title')
|
||||
);
|
||||
|
||||
$('#advanced-search').off('submit').on('submit', function (e) {
|
||||
$('#advanced-search form').off('submit').on('submit', function (e) {
|
||||
e.preventDefault();
|
||||
searchModule.query(getSearchDataFromDOM(), function () {
|
||||
$('#search-input').val('');
|
||||
@@ -33,9 +37,70 @@ define('forum/search', [
|
||||
|
||||
enableAutoComplete();
|
||||
|
||||
userFilterDropdown($('[component="user/filter"]'), ajaxify.data.userFilterSelected);
|
||||
|
||||
$('[component="search/filters"]').on('hidden.bs.dropdown', '.dropdown', function () {
|
||||
const updateFns = {
|
||||
replies: updateReplyCountFilter,
|
||||
time: updateTimeFilter,
|
||||
sort: updateSortFilter,
|
||||
user: updateUserFilter,
|
||||
};
|
||||
|
||||
if (updateFns[$(this).attr('data-filter-name')]) {
|
||||
updateFns[$(this).attr('data-filter-name')]();
|
||||
}
|
||||
});
|
||||
|
||||
fillOutForm();
|
||||
};
|
||||
|
||||
function updateUserFilter() {
|
||||
const isActive = selectedUsers.length > 0;
|
||||
let labelText = '[[search:posted-by]]';
|
||||
if (selectedUsers.length) {
|
||||
labelText = translator.compile(
|
||||
'search:posted-by-usernames', selectedUsers.map(u => u.username).join(', ')
|
||||
);
|
||||
}
|
||||
$('[component="user/filter/button"]').toggleClass(
|
||||
'active-filter', isActive
|
||||
).find('.filter-label').translateText(labelText);
|
||||
}
|
||||
|
||||
function updateTimeFilter() {
|
||||
const isActive = $('#post-time-range').val() > 0;
|
||||
$('#post-time-button').toggleClass(
|
||||
'active-filter', isActive
|
||||
).find('.filter-label').translateText(
|
||||
isActive ?
|
||||
`[[search:time-${$('#post-time-filter').val()}-than-${$('#post-time-range').val()}]]` :
|
||||
`[[search:time]]`
|
||||
);
|
||||
}
|
||||
|
||||
function updateSortFilter() {
|
||||
const isActive = $('#post-sort-by').val() !== 'relevance' || $('#post-sort-direction').val() !== 'desc';
|
||||
$('#sort-by-button').toggleClass(
|
||||
'active-filter', isActive
|
||||
).find('.filter-label').translateText(
|
||||
isActive ?
|
||||
`[[search:sort-by-${$('#post-sort-by').val()}-${$('#post-sort-direction').val()}]]` :
|
||||
`[[search:sort]]`
|
||||
);
|
||||
}
|
||||
|
||||
function updateReplyCountFilter() {
|
||||
const isActive = $('#reply-count').val() > 0;
|
||||
$('#reply-count-button').toggleClass(
|
||||
'active-filter', isActive
|
||||
).find('.filter-label').translateText(
|
||||
isActive ?
|
||||
`[[search:replies-${$('#reply-count-filter').val()}-count, ${$('#reply-count').val()}]]` :
|
||||
`[[search:replies]]`
|
||||
);
|
||||
}
|
||||
|
||||
function getSearchDataFromDOM() {
|
||||
const form = $('#advanced-search');
|
||||
const searchData = {
|
||||
@@ -44,7 +109,7 @@ define('forum/search', [
|
||||
searchData.term = $('#search-input').val();
|
||||
if (searchData.in === 'posts' || searchData.in === 'titlesposts' || searchData.in === 'titles') {
|
||||
searchData.matchWords = form.find('#match-words-filter').val();
|
||||
searchData.by = form.find('#posted-by-user').tagsinput('items');
|
||||
searchData.by = selectedUsers.length ? selectedUsers.map(u => u.username) : undefined;
|
||||
searchData.categories = form.find('#posted-in-categories').val();
|
||||
searchData.searchChildren = form.find('#search-children').is(':checked');
|
||||
searchData.hasTags = form.find('#has-tags').tagsinput('items');
|
||||
@@ -54,7 +119,7 @@ define('forum/search', [
|
||||
searchData.timeRange = form.find('#post-time-range').val();
|
||||
searchData.sortBy = form.find('#post-sort-by').val();
|
||||
searchData.sortDirection = form.find('#post-sort-direction').val();
|
||||
searchData.showAs = form.find('#show-as-topics').is(':checked') ? 'topics' : 'posts';
|
||||
searchData.showAs = form.find('#show-results-as').val();
|
||||
}
|
||||
|
||||
hooks.fire('action:search.getSearchDataFromDOM', {
|
||||
@@ -66,8 +131,8 @@ define('forum/search', [
|
||||
}
|
||||
|
||||
function updateFormItemVisiblity(searchIn) {
|
||||
const hide = searchIn.indexOf('posts') === -1 && searchIn.indexOf('titles') === -1;
|
||||
$('.post-search-item').toggleClass('hide', hide);
|
||||
const hideTitlePostFilters = !searchIn.includes('posts') && !searchIn.includes('titles');
|
||||
$('.post-search-item').toggleClass('hidden', hideTitlePostFilters);
|
||||
}
|
||||
|
||||
function fillOutForm() {
|
||||
@@ -90,6 +155,10 @@ define('forum/search', [
|
||||
$('#match-words-filter').val(formData.matchWords);
|
||||
}
|
||||
|
||||
if (formData.showAs) {
|
||||
$('#show-results-as').val(formData.showAs);
|
||||
}
|
||||
|
||||
if (formData.by) {
|
||||
formData.by = Array.isArray(formData.by) ? formData.by : [formData.by];
|
||||
formData.by.forEach(function (by) {
|
||||
@@ -127,13 +196,6 @@ define('forum/search', [
|
||||
}
|
||||
$('#post-sort-direction').val(formData.sortDirection || 'desc');
|
||||
|
||||
if (formData.showAs) {
|
||||
const isTopic = formData.showAs === 'topics';
|
||||
const isPost = formData.showAs === 'posts';
|
||||
$('#show-as-topics').prop('checked', isTopic).parent().toggleClass('active', isTopic);
|
||||
$('#show-as-posts').prop('checked', isPost).parent().toggleClass('active', isPost);
|
||||
}
|
||||
|
||||
hooks.fire('action:search.fillOutForm', {
|
||||
form: formData,
|
||||
});
|
||||
@@ -147,36 +209,103 @@ define('forum/search', [
|
||||
return false;
|
||||
});
|
||||
|
||||
$('#clear-preferences').on('click', function () {
|
||||
$('#clear-preferences').on('click', async function () {
|
||||
storage.removeItem('search-preferences');
|
||||
const query = $('#search-input').val();
|
||||
$('#advanced-search')[0].reset();
|
||||
$('#search-input').val(query);
|
||||
const html = await app.parseAndTranslate('partials/search-filters', {});
|
||||
$('[component="search/filters"]').replaceWith(html);
|
||||
// clearing dom removes all event handlers, reinitialize
|
||||
userFilterDropdown($('[component="user/filter"]'), []);
|
||||
alerts.success('[[search:search-preferences-cleared]]');
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function enableAutoComplete() {
|
||||
const userEl = $('#posted-by-user');
|
||||
userEl.tagsinput({
|
||||
tagClass: 'badge bg-info',
|
||||
confirmKeys: [13, 44],
|
||||
trimValue: true,
|
||||
});
|
||||
if (app.user.privileges['search:users']) {
|
||||
autocomplete.user(userEl.siblings('.bootstrap-tagsinput').find('input'));
|
||||
function userFilterDropdown(el, _selectedUsers) {
|
||||
selectedUsers = _selectedUsers;
|
||||
async function renderSelectedUsers() {
|
||||
const html = await app.parseAndTranslate('partials/search-filters', 'userFilterSelected', {
|
||||
userFilterSelected: selectedUsers,
|
||||
});
|
||||
el.find('[component="user/filter/selected"]').html(html);
|
||||
}
|
||||
|
||||
const tagEl = $('#has-tags');
|
||||
tagEl.tagsinput({
|
||||
tagClass: 'badge bg-info',
|
||||
confirmKeys: [13, 44],
|
||||
trimValue: true,
|
||||
});
|
||||
if (app.user.privileges['search:tags']) {
|
||||
autocomplete.tag(tagEl.siblings('.bootstrap-tagsinput').find('input'));
|
||||
async function doSearch() {
|
||||
let result = { users: [] };
|
||||
const query = el.find('[component="user/filter/search"]').val();
|
||||
if (query && query.length > 1) {
|
||||
if (app.user.privileges['search:users']) {
|
||||
result = await api.get('/api/users', { query: query });
|
||||
} else {
|
||||
try {
|
||||
const userData = await api.get(`/api/user/${slugify(query)}`);
|
||||
result.users.push(userData);
|
||||
} catch (err) {}
|
||||
}
|
||||
}
|
||||
if (!result.users.length) {
|
||||
el.find('[component="user/filter/results"]').translateHtml(
|
||||
'[[users:no-users-found]]'
|
||||
);
|
||||
return;
|
||||
}
|
||||
result.users = result.users.slice(0, 20);
|
||||
const html = await app.parseAndTranslate('partials/search-filters', 'userFilterResults', {
|
||||
userFilterResults: result.users,
|
||||
});
|
||||
const uidToUser = {};
|
||||
result.users.forEach((user) => {
|
||||
uidToUser[user.uid] = user;
|
||||
});
|
||||
el.find('[component="user/filter/results"]').html(html);
|
||||
el.find('[component="user/filter/results"] [data-uid]').on('click', async function () {
|
||||
selectedUsers.push(uidToUser[$(this).attr('data-uid')]);
|
||||
renderSelectedUsers();
|
||||
});
|
||||
}
|
||||
|
||||
el.find('[component="user/filter/search"]').on('keyup', utils.debounce(function () {
|
||||
if (app.user.privileges['search:users']) {
|
||||
doSearch();
|
||||
}
|
||||
}, 1000));
|
||||
|
||||
el.on('click', '[component="user/filter/delete"]', function () {
|
||||
const uid = $(this).attr('data-uid');
|
||||
selectedUsers = selectedUsers.filter(u => parseInt(u.uid, 10) !== parseInt(uid, 10));
|
||||
renderSelectedUsers();
|
||||
});
|
||||
|
||||
el.find('[component="user/filter/search"]').on('keyup', (e) => {
|
||||
if (e.key === 'Enter' && !app.user.privileges['search:users']) {
|
||||
doSearch();
|
||||
}
|
||||
});
|
||||
|
||||
el.on('shown.bs.dropdown', function () {
|
||||
el.find('[component="user/filter/search"]').trigger('focus');
|
||||
});
|
||||
}
|
||||
|
||||
function enableAutoComplete() {
|
||||
// const userEl = $('#posted-by-user');
|
||||
// userEl.tagsinput({
|
||||
// tagClass: 'badge bg-info',
|
||||
// confirmKeys: [13, 44],
|
||||
// trimValue: true,
|
||||
// });
|
||||
// if (app.user.privileges['search:users']) {
|
||||
// autocomplete.user(userEl.siblings('.bootstrap-tagsinput').find('input'));
|
||||
// }
|
||||
|
||||
// const tagEl = $('#has-tags');
|
||||
// tagEl.tagsinput({
|
||||
// tagClass: 'badge bg-info',
|
||||
// confirmKeys: [13, 44],
|
||||
// trimValue: true,
|
||||
// });
|
||||
// if (app.user.privileges['search:tags']) {
|
||||
// autocomplete.tag(tagEl.siblings('.bootstrap-tagsinput').find('input'));
|
||||
// }
|
||||
}
|
||||
|
||||
return Search;
|
||||
|
||||
@@ -8,8 +8,10 @@ const meta = require('../meta');
|
||||
const plugins = require('../plugins');
|
||||
const search = require('../search');
|
||||
const categories = require('../categories');
|
||||
const user = require('../user');
|
||||
const pagination = require('../pagination');
|
||||
const privileges = require('../privileges');
|
||||
const translator = require('../translator');
|
||||
const utils = require('../utils');
|
||||
const helpers = require('./helpers');
|
||||
|
||||
@@ -57,12 +59,12 @@ searchController.search = async function (req, res, next) {
|
||||
categories: req.query.categories,
|
||||
searchChildren: req.query.searchChildren,
|
||||
hasTags: req.query.hasTags,
|
||||
replies: req.query.replies,
|
||||
repliesFilter: req.query.repliesFilter,
|
||||
timeRange: req.query.timeRange,
|
||||
timeFilter: req.query.timeFilter,
|
||||
sortBy: req.query.sortBy || meta.config.searchDefaultSortBy || '',
|
||||
sortDirection: req.query.sortDirection,
|
||||
replies: validator.escape(String(req.query.replies || '')),
|
||||
repliesFilter: validator.escape(String(req.query.repliesFilter || '')),
|
||||
timeRange: validator.escape(String(req.query.timeRange || '')),
|
||||
timeFilter: validator.escape(String(req.query.timeFilter || '')),
|
||||
sortBy: validator.escape(String(req.query.sortBy || '')) || meta.config.searchDefaultSortBy || '',
|
||||
sortDirection: validator.escape(String(req.query.sortDirection || '')),
|
||||
page: page,
|
||||
itemsPerPage: req.query.itemsPerPage,
|
||||
uid: req.uid,
|
||||
@@ -94,6 +96,28 @@ searchController.search = async function (req, res, next) {
|
||||
searchData.showAsTopics = req.query.showAs === 'topics';
|
||||
searchData.title = '[[global:header.search]]';
|
||||
|
||||
searchData.filters = {
|
||||
replies: {
|
||||
active: !!data.repliesFilter,
|
||||
label: `[[search:replies-${data.repliesFilter}-count, ${data.replies}]]`,
|
||||
},
|
||||
time: {
|
||||
active: !!(data.timeFilter && data.timeRange),
|
||||
label: `[[search:time-${data.timeFilter}-than-${data.timeRange}]]`,
|
||||
},
|
||||
sort: {
|
||||
active: !!(data.sortBy && data.sortBy !== 'relevance'),
|
||||
label: `[[search:sort-by-${data.sortBy}-${data.sortDirection}]]`,
|
||||
},
|
||||
users: {
|
||||
active: !!(data.postedBy),
|
||||
label: translator.compile(
|
||||
'search:posted-by-usernames',
|
||||
(Array.isArray(data.postedBy) ? data.postedBy : []).map(u => validator.escape(String(u))).join(', ')
|
||||
),
|
||||
},
|
||||
};
|
||||
searchData.userFilterSelected = await getSelectedUsers(data.postedBy);
|
||||
searchData.searchDefaultSortBy = meta.config.searchDefaultSortBy || '';
|
||||
searchData.searchDefaultIn = meta.config.searchDefaultIn || 'titlesposts';
|
||||
searchData.privileges = userPrivileges;
|
||||
@@ -127,6 +151,14 @@ async function recordSearch(data) {
|
||||
}
|
||||
}
|
||||
|
||||
async function getSelectedUsers(postedBy) {
|
||||
if (!Array.isArray(postedBy)) {
|
||||
return [];
|
||||
}
|
||||
const uids = await user.getUidsByUsernames(postedBy);
|
||||
return await user.getUsersFields(uids, ['username', 'userslug', 'picture']);
|
||||
}
|
||||
|
||||
async function buildCategories(uid, searchOnly) {
|
||||
if (searchOnly) {
|
||||
return [];
|
||||
|
||||
Reference in New Issue
Block a user