mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-11-06 14:05:46 +01:00
ACP search updated to support translations
This commit is contained in:
@@ -78,6 +78,7 @@
|
|||||||
"request": "^2.44.0",
|
"request": "^2.44.0",
|
||||||
"rimraf": "~2.5.0",
|
"rimraf": "~2.5.0",
|
||||||
"rss": "^1.0.0",
|
"rss": "^1.0.0",
|
||||||
|
"sanitize-html": "^1.13.0",
|
||||||
"semver": "^5.1.0",
|
"semver": "^5.1.0",
|
||||||
"serve-favicon": "^2.1.5",
|
"serve-favicon": "^2.1.5",
|
||||||
"sitemap": "^1.4.0",
|
"sitemap": "^1.4.0",
|
||||||
|
|||||||
6
public/language/en_GB/admin/appearance/themes.json
Normal file
6
public/language/en_GB/admin/appearance/themes.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"checking-for-installed": "Checking for installed themes...",
|
||||||
|
"homepage": "Homepage",
|
||||||
|
"select-skin": "Select Skin",
|
||||||
|
"select-theme": "Select Theme"
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@
|
|||||||
@import "./modules/selectable";
|
@import "./modules/selectable";
|
||||||
@import "./modules/snackbar";
|
@import "./modules/snackbar";
|
||||||
@import "./modules/nprogress";
|
@import "./modules/nprogress";
|
||||||
|
@import "./modules/search";
|
||||||
|
|
||||||
body {
|
body {
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
|
|||||||
36
public/less/admin/modules/search.less
Normal file
36
public/less/admin/modules/search.less
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
#acp-search {
|
||||||
|
.dropdown-menu {
|
||||||
|
max-height: 75vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-start-typing {
|
||||||
|
.keep-typing, .search-forum, .no-results {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-keep-typing {
|
||||||
|
.start-typing, .search-forum, .no-results {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-no-results {
|
||||||
|
.keep-typing, .start-typing {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-yes-results {
|
||||||
|
.keep-typing, .start-typing, .no-results {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-disabled {
|
||||||
|
.search-forum {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,113 +2,106 @@
|
|||||||
/*globals define, admin, ajaxify, RELATIVE_PATH*/
|
/*globals define, admin, ajaxify, RELATIVE_PATH*/
|
||||||
|
|
||||||
define(function () {
|
define(function () {
|
||||||
var search = {},
|
var search = {};
|
||||||
searchIndex;
|
|
||||||
|
function nsToTitle(namespace) {
|
||||||
|
return namespace.replace('admin/', '').split('/').map(function (str) {
|
||||||
|
return str[0].toUpperCase() + str.slice(1);
|
||||||
|
}).join(' > ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function find(dict, term) {
|
||||||
|
var html = dict.filter(function (elem) {
|
||||||
|
return elem.translations.toLowerCase().includes(term);
|
||||||
|
}).map(function (params) {
|
||||||
|
var namespace = params.namespace;
|
||||||
|
var translations = params.translations;
|
||||||
|
var title = params.title == null ? nsToTitle(namespace) : params.title;
|
||||||
|
|
||||||
|
var results = translations
|
||||||
|
.replace(new RegExp('^(?:(?!' + term + ').)*$', 'gmi'), '')
|
||||||
|
.replace(
|
||||||
|
new RegExp('^[\\s\\S]*?(.{0,25})(' + term + ')(.{0,25})[\\s\\S]*?$', 'gmi'),
|
||||||
|
'...$1<span class="search-match">$2</span>$3...<br>'
|
||||||
|
).replace(/(\n ?)+/g, '\n');
|
||||||
|
|
||||||
|
return '<li role="presentation" class="result">' +
|
||||||
|
'<a role= "menuitem" href= "' + RELATIVE_PATH + '/' + namespace + '" >' +
|
||||||
|
title +
|
||||||
|
'<br>' +
|
||||||
|
'<small><code>' +
|
||||||
|
results +
|
||||||
|
'</small></code>' +
|
||||||
|
'</a>' +
|
||||||
|
'</li>';
|
||||||
|
}).join('');
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
search.init = function () {
|
search.init = function () {
|
||||||
$.getJSON(RELATIVE_PATH + '/templates/indexed.json', function (data) {
|
socket.emit('admin.getSearchDict', {}, function (err, dict) {
|
||||||
searchIndex = data;
|
if (err) {
|
||||||
for (var file in searchIndex) {
|
app.alertError(err);
|
||||||
if (searchIndex.hasOwnProperty(file)) {
|
throw err;
|
||||||
searchIndex[file] = searchIndex[file].replace(/<img/g, '<none'); // can't think of a better solution, see #2153
|
|
||||||
searchIndex[file] = $('<div class="search-container">' + searchIndex[file] + '</div>');
|
|
||||||
searchIndex[file].find('script').remove();
|
|
||||||
|
|
||||||
searchIndex[file] = searchIndex[file].text().toLowerCase().replace(/[ |\r|\n]+/g, ' ');
|
|
||||||
}
|
}
|
||||||
}
|
setupACPSearch(dict);
|
||||||
|
|
||||||
delete searchIndex['/admin/header.tpl'];
|
|
||||||
delete searchIndex['/admin/footer.tpl'];
|
|
||||||
|
|
||||||
setupACPSearch();
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function setupACPSearch() {
|
function setupACPSearch(dict) {
|
||||||
var menu = $('#acp-search .dropdown-menu'),
|
var dropdown = $('#acp-search .dropdown');
|
||||||
routes = [],
|
var menu = $('#acp-search .dropdown-menu');
|
||||||
input = $('#acp-search input'),
|
var input = $('#acp-search input');
|
||||||
firstResult = null;
|
|
||||||
|
if (!config.searchEnabled) {
|
||||||
|
menu.addClass('search-disabled');
|
||||||
|
}
|
||||||
|
|
||||||
input.on('keyup', function () {
|
input.on('keyup', function () {
|
||||||
$('#acp-search .dropdown').addClass('open');
|
dropdown.addClass('open');
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#acp-search').parents('form').on('submit', function (ev) {
|
$('#acp-search').parents('form').on('submit', function (ev) {
|
||||||
var input = $(this).find('input'),
|
var firstResult = menu.find('li:first-child > a').attr('href');
|
||||||
href = firstResult ? firstResult : RELATIVE_PATH + '/search/' + input.val();
|
var href = firstResult ? firstResult : RELATIVE_PATH + '/search/' + input.val();
|
||||||
|
|
||||||
ajaxify.go(href.replace(/^\//, ''));
|
ajaxify.go(href.replace(/^\//, ''));
|
||||||
|
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
$('#acp-search .dropdown').removeClass('open');
|
dropdown.removeClass('open');
|
||||||
$(input).blur();
|
input.blur();
|
||||||
}, 150);
|
}, 150);
|
||||||
|
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#main-menu a').each(function (idx, link) {
|
|
||||||
routes.push($(link).attr('href'));
|
|
||||||
});
|
|
||||||
|
|
||||||
input.on('keyup focus', function () {
|
input.on('keyup focus', function () {
|
||||||
var $input = $(this),
|
var value = input.val().toLowerCase();
|
||||||
value = $input.val().toLowerCase(),
|
menu.children('.result').remove();
|
||||||
menuItems = $('#acp-search .dropdown-menu').html('');
|
|
||||||
|
|
||||||
function toUpperCase(txt) {
|
var len = value.length;
|
||||||
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
|
var results;
|
||||||
}
|
|
||||||
|
|
||||||
firstResult = null;
|
menu.toggleClass('state-start-typing', len === 0);
|
||||||
|
menu.toggleClass('state-keep-typing', len > 0 && len < 3);
|
||||||
|
|
||||||
if (value.length >= 3) {
|
if (len >= 3) {
|
||||||
for (var file in searchIndex) {
|
menu.prepend(find(dict, value));
|
||||||
if (searchIndex.hasOwnProperty(file)) {
|
|
||||||
var position = searchIndex[file].indexOf(value);
|
|
||||||
|
|
||||||
if (position !== -1) {
|
results = menu.children('.result').length;
|
||||||
var href = file.replace('.tpl', ''),
|
|
||||||
title = href.replace(/^\/admin\//, '').split('/'),
|
|
||||||
description = searchIndex[file].substring(Math.max(0, position - 25), Math.min(searchIndex[file].length - 1, position + 25))
|
|
||||||
.replace(value, '<span class="search-match">' + value + '</span>');
|
|
||||||
|
|
||||||
for (var t in title) {
|
menu.toggleClass('state-no-results', !results);
|
||||||
if (title.hasOwnProperty(t)) {
|
menu.toggleClass('state-yes-results', !!results);
|
||||||
title[t] = title[t]
|
|
||||||
.replace('-', ' ')
|
|
||||||
.replace(/\w\S*/g, toUpperCase);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
title = title.join(' > ');
|
menu.find('.search-forum')
|
||||||
href = RELATIVE_PATH + href;
|
.not('.divider')
|
||||||
firstResult = firstResult ? firstResult : href;
|
.find('a')
|
||||||
|
.attr('href', RELATIVE_PATH + '/search/' + value)
|
||||||
if ($.inArray(href, routes) !== -1) {
|
.find('strong')
|
||||||
menuItems.append('<li role="presentation"><a role="menuitem" href="' + href + '">' + title + '<br /><small><code>...' + description + '...</code></small></a></li>');
|
.html(value);
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (menuItems.html() === '') {
|
|
||||||
menuItems.append('<li role="presentation"><a role="menuitem" href="#">No results...</a></li>');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value.length > 0) {
|
|
||||||
if (config.searchEnabled) {
|
|
||||||
menuItems.append('<li role="presentation" class="divider"></li>');
|
|
||||||
menuItems.append('<li role="presentation"><a role="menuitem" target="_top" href="' + RELATIVE_PATH + '/search/' + value + '">Search the forum for <strong>' + value + '</strong></a></li>');
|
|
||||||
} else if (value.length < 3) {
|
|
||||||
menuItems.append('<li role="presentation"><a role="menuitem" href="#">Type more to see results...</a></li>');
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
menuItems.append('<li role="presentation"><a role="menuitem" href="#">Start typing to see results...</a></li>');
|
menu.removeClass('state-no-results');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
128
src/admin/search.js
Normal file
128
src/admin/search.js
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var fs = require('fs');
|
||||||
|
var path = require('path');
|
||||||
|
var nconf = require('nconf');
|
||||||
|
var sanitize = require('sanitize-html');
|
||||||
|
|
||||||
|
var languages = require('../languages');
|
||||||
|
var meta = require('../meta');
|
||||||
|
var utils = require('../../public/src/utils');
|
||||||
|
|
||||||
|
function walk(directory) {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
utils.walk(directory, function (err, data) {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function readFile(path) {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
fs.readFile(path, function (err, data) {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve(data.toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadLanguage(language, filename) {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
languages.get(language, filename + '.json', function (err, data) {
|
||||||
|
if (err || !data || !Object.keys(data).length) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAdminNamespaces() {
|
||||||
|
return walk(path.resolve('./public/templates/admin'))
|
||||||
|
.then(function (directories) {
|
||||||
|
return directories.map(function (dir) {
|
||||||
|
return dir.replace(/^.*(admin.*?).tpl$/, '$1');
|
||||||
|
}).filter(function (dir) {
|
||||||
|
return !dir.includes('/partials/');
|
||||||
|
}).filter(function (dir) {
|
||||||
|
return dir.match(/\/.*\//);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var fallbackCache = {};
|
||||||
|
|
||||||
|
function fallback(namespace) {
|
||||||
|
fallbackCache[namespace] = fallbackCache[namespace] ||
|
||||||
|
readFile(path.resolve('./public/templates/', namespace + '.tpl'))
|
||||||
|
.then(function (template) {
|
||||||
|
var translations = sanitize(template, {
|
||||||
|
transformTags: {
|
||||||
|
'*': function () {
|
||||||
|
return {
|
||||||
|
tagName: 'div'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.replace(/(<div>)|(<\/div>)/g, '')
|
||||||
|
.replace(/([\n\r]+ ?)+/g, '\n')
|
||||||
|
.replace(/[\t ]+/g, ' ');
|
||||||
|
|
||||||
|
return {
|
||||||
|
namespace: namespace,
|
||||||
|
translations: translations,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return fallbackCache[namespace];
|
||||||
|
}
|
||||||
|
|
||||||
|
function initDict(language) {
|
||||||
|
return getAdminNamespaces().then(function (namespaces) {
|
||||||
|
return Promise.all(namespaces.map(function (namespace) {
|
||||||
|
return loadLanguage(language, namespace).then(function (translations) {
|
||||||
|
return { namespace: namespace, translations: translations };
|
||||||
|
}).then(function (params) {
|
||||||
|
var namespace = params.namespace;
|
||||||
|
var translations = params.translations;
|
||||||
|
|
||||||
|
var str = Object.keys(translations).map(function (key) {
|
||||||
|
return translations[key];
|
||||||
|
}).join('\n');
|
||||||
|
|
||||||
|
return {
|
||||||
|
namespace: namespace,
|
||||||
|
translations: str
|
||||||
|
};
|
||||||
|
})
|
||||||
|
// TODO: Use translator to get title for admin route?
|
||||||
|
.catch(function () {
|
||||||
|
return fallback(namespace);
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
return { namespace: namespace, translations: '' };
|
||||||
|
})
|
||||||
|
.then(function (params) {
|
||||||
|
params.translations = params.translations.replace(/\{[^\{\}]*\}/g, '');
|
||||||
|
return params;
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var cache = {};
|
||||||
|
|
||||||
|
function getDict(language, term) {
|
||||||
|
cache[language] = cache[language] || initDict(language);
|
||||||
|
return cache[language];
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.getDict = getDict;
|
||||||
@@ -15,6 +15,7 @@ var emailer = require('../emailer');
|
|||||||
var db = require('../database');
|
var db = require('../database');
|
||||||
var analytics = require('../analytics');
|
var analytics = require('../analytics');
|
||||||
var index = require('./index');
|
var index = require('./index');
|
||||||
|
var getAdminSearchDict = require('../admin/search').getDict;
|
||||||
|
|
||||||
var SocketAdmin = {
|
var SocketAdmin = {
|
||||||
user: require('./admin/user'),
|
user: require('./admin/user'),
|
||||||
@@ -277,5 +278,18 @@ SocketAdmin.deleteAllEvents = function (socket, data, callback) {
|
|||||||
events.deleteAll(callback);
|
events.deleteAll(callback);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
SocketAdmin.getSearchDict = function (socket, data, callback) {
|
||||||
|
user.getSettings(socket.uid, function (err, settings) {
|
||||||
|
if (err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
var lang = settings.userLang || meta.config.defaultLang || 'en_GB';
|
||||||
|
getAdminSearchDict(lang)
|
||||||
|
.then(function (results) {
|
||||||
|
callback(null, results);
|
||||||
|
}, callback);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
module.exports = SocketAdmin;
|
module.exports = SocketAdmin;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<div id="themes" class="themes">
|
<div id="themes" class="themes">
|
||||||
<div class="directory row" id="installed_themes">
|
<div class="directory row" id="installed_themes">
|
||||||
<i class="fa fa-refresh fa-spin"></i> Checking for installed themes...
|
<i class="fa fa-refresh fa-spin"></i> [[admin/appearance/themes:checking-for-installed]]
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -149,7 +149,23 @@
|
|||||||
<div class="" id="acp-search" >
|
<div class="" id="acp-search" >
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<input type="text" data-toggle="dropdown" class="form-control" placeholder="Search...">
|
<input type="text" data-toggle="dropdown" class="form-control" placeholder="Search...">
|
||||||
<ul class="dropdown-menu dropdown-menu-right" role="menu"></ul>
|
<ul class="dropdown-menu dropdown-menu-right state-start-typing" role="menu">
|
||||||
|
<li role="presentation" class="no-results">
|
||||||
|
<a>No results...</a>
|
||||||
|
</li>
|
||||||
|
<li role="presentation" class="divider search-forum"></li>
|
||||||
|
<li role="presentation" class="search-forum">
|
||||||
|
<a role="menuitem" target="_top" href="#">
|
||||||
|
Search the forum for <strong></strong>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li role="presentation" class="keep-typing">
|
||||||
|
<a>Type more to see results...</a>
|
||||||
|
</li>
|
||||||
|
<li role="presentation" class="start-typing">
|
||||||
|
<a>Start typing to see results...</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -10,13 +10,13 @@
|
|||||||
|
|
||||||
<!-- IF themes.url -->
|
<!-- IF themes.url -->
|
||||||
<p>
|
<p>
|
||||||
<a href="{themes.url}" target="_blank">Homepage</a>
|
<a href="{themes.url}" target="_blank">[[admin/appearance/themes:homepage]]</a>
|
||||||
</p>
|
</p>
|
||||||
<!-- ENDIF themes.url -->
|
<!-- ENDIF themes.url -->
|
||||||
</div>
|
</div>
|
||||||
<div class="mdl-card__actions mdl-card--border">
|
<div class="mdl-card__actions mdl-card--border">
|
||||||
<a class="mdl-button mdl-button--colored mdl-js-button mdl-js-ripple-effect" data-action="use">
|
<a class="mdl-button mdl-button--colored mdl-js-button mdl-js-ripple-effect" data-action="use">
|
||||||
<!-- IF themes.skin -->Select Skin<!-- ELSE -->Select Theme<!-- ENDIF themes.skin -->
|
<!-- IF themes.skin -->[[admin/appearance/themes:select-skin]]<!-- ELSE -->[[admin/appearance/themes:select-theme]]<!-- ENDIF themes.skin -->
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user