diff --git a/package.json b/package.json
index 7bcd0ce3da..78d0f582c9 100644
--- a/package.json
+++ b/package.json
@@ -78,6 +78,7 @@
"request": "^2.44.0",
"rimraf": "~2.5.0",
"rss": "^1.0.0",
+ "sanitize-html": "^1.13.0",
"semver": "^5.1.0",
"serve-favicon": "^2.1.5",
"sitemap": "^1.4.0",
diff --git a/public/language/en_GB/admin/appearance/themes.json b/public/language/en_GB/admin/appearance/themes.json
new file mode 100644
index 0000000000..3f1b10cf7f
--- /dev/null
+++ b/public/language/en_GB/admin/appearance/themes.json
@@ -0,0 +1,6 @@
+{
+ "checking-for-installed": "Checking for installed themes...",
+ "homepage": "Homepage",
+ "select-skin": "Select Skin",
+ "select-theme": "Select Theme"
+}
\ No newline at end of file
diff --git a/public/less/admin/admin.less b/public/less/admin/admin.less
index 0ceeb050e5..aa5efff256 100644
--- a/public/less/admin/admin.less
+++ b/public/less/admin/admin.less
@@ -29,6 +29,7 @@
@import "./modules/selectable";
@import "./modules/snackbar";
@import "./modules/nprogress";
+@import "./modules/search";
body {
overflow-y: scroll;
diff --git a/public/less/admin/modules/search.less b/public/less/admin/modules/search.less
new file mode 100644
index 0000000000..381b940646
--- /dev/null
+++ b/public/less/admin/modules/search.less
@@ -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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/public/src/admin/modules/search.js b/public/src/admin/modules/search.js
index f64a2490e7..0a5d3dde56 100644
--- a/public/src/admin/modules/search.js
+++ b/public/src/admin/modules/search.js
@@ -2,113 +2,106 @@
/*globals define, admin, ajaxify, RELATIVE_PATH*/
define(function () {
- var search = {},
- searchIndex;
+ var search = {};
- search.init = function () {
- $.getJSON(RELATIVE_PATH + '/templates/indexed.json', function (data) {
- searchIndex = data;
- for (var file in searchIndex) {
- if (searchIndex.hasOwnProperty(file)) {
- searchIndex[file] = searchIndex[file].replace(/
' + searchIndex[file] + '');
- searchIndex[file].find('script').remove();
-
- searchIndex[file] = searchIndex[file].text().toLowerCase().replace(/[ |\r|\n]+/g, ' ');
- }
- }
+ function nsToTitle(namespace) {
+ return namespace.replace('admin/', '').split('/').map(function (str) {
+ return str[0].toUpperCase() + str.slice(1);
+ }).join(' > ');
+ }
- delete searchIndex['/admin/header.tpl'];
- delete searchIndex['/admin/footer.tpl'];
+ 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$2$3...
'
+ ).replace(/(\n ?)+/g, '\n');
+
+ return '
' +
+ '' +
+ title +
+ '
' +
+ '' +
+ results +
+ '
' +
+ '' +
+ '';
+ }).join('');
+ return html;
+ }
- setupACPSearch();
+ search.init = function () {
+ socket.emit('admin.getSearchDict', {}, function (err, dict) {
+ if (err) {
+ app.alertError(err);
+ throw err;
+ }
+ setupACPSearch(dict);
});
};
- function setupACPSearch() {
- var menu = $('#acp-search .dropdown-menu'),
- routes = [],
- input = $('#acp-search input'),
- firstResult = null;
+ function setupACPSearch(dict) {
+ var dropdown = $('#acp-search .dropdown');
+ var menu = $('#acp-search .dropdown-menu');
+ var input = $('#acp-search input');
+
+ if (!config.searchEnabled) {
+ menu.addClass('search-disabled');
+ }
input.on('keyup', function () {
- $('#acp-search .dropdown').addClass('open');
+ dropdown.addClass('open');
});
$('#acp-search').parents('form').on('submit', function (ev) {
- var input = $(this).find('input'),
- href = firstResult ? firstResult : RELATIVE_PATH + '/search/' + input.val();
+ var firstResult = menu.find('li:first-child > a').attr('href');
+ var href = firstResult ? firstResult : RELATIVE_PATH + '/search/' + input.val();
ajaxify.go(href.replace(/^\//, ''));
setTimeout(function () {
- $('#acp-search .dropdown').removeClass('open');
- $(input).blur();
+ dropdown.removeClass('open');
+ input.blur();
}, 150);
ev.preventDefault();
return false;
});
- $('#main-menu a').each(function (idx, link) {
- routes.push($(link).attr('href'));
- });
-
input.on('keyup focus', function () {
- var $input = $(this),
- value = $input.val().toLowerCase(),
- menuItems = $('#acp-search .dropdown-menu').html('');
+ var value = input.val().toLowerCase();
+ menu.children('.result').remove();
- function toUpperCase(txt) {
- return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
- }
+ var len = value.length;
+ var results;
- firstResult = null;
-
- if (value.length >= 3) {
- for (var file in searchIndex) {
- if (searchIndex.hasOwnProperty(file)) {
- var position = searchIndex[file].indexOf(value);
-
- if (position !== -1) {
- 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, '' + value + '');
-
- for (var t in title) {
- if (title.hasOwnProperty(t)) {
- title[t] = title[t]
- .replace('-', ' ')
- .replace(/\w\S*/g, toUpperCase);
- }
- }
-
- title = title.join(' > ');
- href = RELATIVE_PATH + href;
- firstResult = firstResult ? firstResult : href;
-
- if ($.inArray(href, routes) !== -1) {
- menuItems.append('' + title + '
...' + description + '...
');
- }
- }
- }
- }
-
- if (menuItems.html() === '') {
- menuItems.append('No results...');
- }
- }
+ menu.toggleClass('state-start-typing', len === 0);
+ menu.toggleClass('state-keep-typing', len > 0 && len < 3);
+
+ if (len >= 3) {
+ menu.prepend(find(dict, value));
+
+ results = menu.children('.result').length;
+
+ menu.toggleClass('state-no-results', !results);
+ menu.toggleClass('state-yes-results', !!results);
- if (value.length > 0) {
- if (config.searchEnabled) {
- menuItems.append('');
- menuItems.append('Search the forum for ' + value + '');
- } else if (value.length < 3) {
- menuItems.append('Type more to see results...');
- }
+ menu.find('.search-forum')
+ .not('.divider')
+ .find('a')
+ .attr('href', RELATIVE_PATH + '/search/' + value)
+ .find('strong')
+ .html(value);
} else {
- menuItems.append('Start typing to see results...');
+ menu.removeClass('state-no-results');
}
});
}
diff --git a/src/admin/search.js b/src/admin/search.js
new file mode 100644
index 0000000000..985ea51a05
--- /dev/null
+++ b/src/admin/search.js
@@ -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>)/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;
diff --git a/src/socket.io/admin.js b/src/socket.io/admin.js
index c3220ed094..9817099ef5 100644
--- a/src/socket.io/admin.js
+++ b/src/socket.io/admin.js
@@ -15,6 +15,7 @@ var emailer = require('../emailer');
var db = require('../database');
var analytics = require('../analytics');
var index = require('./index');
+var getAdminSearchDict = require('../admin/search').getDict;
var SocketAdmin = {
user: require('./admin/user'),
@@ -277,5 +278,18 @@ SocketAdmin.deleteAllEvents = function (socket, data, 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;
diff --git a/src/views/admin/appearance/themes.tpl b/src/views/admin/appearance/themes.tpl
index e67c6a2755..8203c84e8b 100644
--- a/src/views/admin/appearance/themes.tpl
+++ b/src/views/admin/appearance/themes.tpl
@@ -1,6 +1,6 @@
- Checking for installed themes...
+ [[admin/appearance/themes:checking-for-installed]]
diff --git a/src/views/admin/partials/menu.tpl b/src/views/admin/partials/menu.tpl
index 0fea719e46..9bc0c48d7e 100644
--- a/src/views/admin/partials/menu.tpl
+++ b/src/views/admin/partials/menu.tpl
@@ -149,7 +149,23 @@
diff --git a/src/views/admin/partials/theme_list.tpl b/src/views/admin/partials/theme_list.tpl
index 1517eb5c9e..066f43cdd8 100644
--- a/src/views/admin/partials/theme_list.tpl
+++ b/src/views/admin/partials/theme_list.tpl
@@ -10,13 +10,13 @@
- Homepage
+ [[admin/appearance/themes:homepage]]