From a431dc030587f47760e005d30576176d0e1f1163 Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Mon, 28 Nov 2016 12:52:26 -0700 Subject: [PATCH] ACP search updated to support translations --- package.json | 1 + .../en_GB/admin/appearance/themes.json | 6 + public/less/admin/admin.less | 1 + public/less/admin/modules/search.less | 36 ++++ public/src/admin/modules/search.js | 155 +++++++++--------- src/admin/search.js | 128 +++++++++++++++ src/socket.io/admin.js | 14 ++ src/views/admin/appearance/themes.tpl | 2 +- src/views/admin/partials/menu.tpl | 18 +- src/views/admin/partials/theme_list.tpl | 4 +- 10 files changed, 280 insertions(+), 85 deletions(-) create mode 100644 public/language/en_GB/admin/appearance/themes.json create mode 100644 public/less/admin/modules/search.less create mode 100644 src/admin/search.js 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 ''; + }).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]]

    - Select SkinSelect Theme + [[admin/appearance/themes:select-skin]][[admin/appearance/themes:select-theme]]