mirror of
https://github.com/NodeBB/NodeBB.git
synced 2026-03-05 12:01:17 +01:00
Merge remote-tracking branch 'origin/master' into flagging-refactor
This commit is contained in:
@@ -83,5 +83,7 @@
|
||||
"white" : false, // true: Check against strict whitespace and indentation rules
|
||||
|
||||
// Custom Globals
|
||||
"globals" : {} // additional predefined global variables
|
||||
"globals" : {
|
||||
"Promise": true
|
||||
} // additional predefined global variables
|
||||
}
|
||||
@@ -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",
|
||||
@@ -86,7 +87,7 @@
|
||||
"socket.io-redis": "2.0.0",
|
||||
"socketio-wildcard": "~0.3.0",
|
||||
"string": "^3.0.0",
|
||||
"templates.js": "0.3.4",
|
||||
"templates.js": "0.3.5",
|
||||
"toobusy-js": "^0.5.1",
|
||||
"uglify-js": "^2.6.0",
|
||||
"underscore": "^1.8.3",
|
||||
|
||||
9
public/language/en-GB/admin/appearance/skins.json
Normal file
9
public/language/en-GB/admin/appearance/skins.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"loading": "Loading Skins...",
|
||||
"homepage": "Homepage",
|
||||
"select-skin": "Select Skin",
|
||||
"current-skin": "Current Skin",
|
||||
"skin-updated": "Skin Updated",
|
||||
"applied-success": "%1 skin was succesfully applied",
|
||||
"revert-success": "Skin reverted to base colours"
|
||||
}
|
||||
11
public/language/en-GB/admin/appearance/themes.json
Normal file
11
public/language/en-GB/admin/appearance/themes.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"checking-for-installed": "Checking for installed themes...",
|
||||
"homepage": "Homepage",
|
||||
"select-theme": "Select Theme",
|
||||
"current-theme": "Current Theme",
|
||||
"no-themes": "No installed themes found",
|
||||
"revert-confirm": "Are you sure you wish to restore the default NodeBB theme?",
|
||||
"theme-changed": "Theme Changed",
|
||||
"revert-success": "You have successfully reverted your NodeBB back to it's default theme.",
|
||||
"restart-to-activate": "Please restart your NodeBB to fully activate this theme"
|
||||
}
|
||||
@@ -29,6 +29,7 @@
|
||||
@import "./modules/selectable";
|
||||
@import "./modules/snackbar";
|
||||
@import "./modules/nprogress";
|
||||
@import "./modules/search";
|
||||
|
||||
body {
|
||||
overflow-y: scroll;
|
||||
|
||||
45
public/less/admin/modules/search.less
Normal file
45
public/less/admin/modules/search.less
Normal file
@@ -0,0 +1,45 @@
|
||||
#acp-search {
|
||||
.dropdown-menu {
|
||||
max-height: 75vh;
|
||||
overflow-y: auto;
|
||||
|
||||
> li > a {
|
||||
&.focus {
|
||||
&:extend(.dropdown-menu>li>a:focus);
|
||||
}
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
"use strict";
|
||||
/* global define, app, socket, templates */
|
||||
|
||||
define('admin/appearance/skins', function () {
|
||||
define('admin/appearance/skins', ['translator'], function (translator) {
|
||||
var Skins = {};
|
||||
|
||||
Skins.init = function () {
|
||||
@@ -40,8 +40,8 @@ define('admin/appearance/skins', function () {
|
||||
app.alert({
|
||||
alert_id: 'admin:theme',
|
||||
type: 'info',
|
||||
title: 'Skin Updated',
|
||||
message: themeId ? (themeId + ' skin was successfully applied') : 'Skin reverted to base colours',
|
||||
title: '[[admin/appearance/skins:skin-updated]]',
|
||||
message: themeId ? ('[[admin/appearance/skins:applied-success, ' + themeId + ']]') : '[[admin/appearance/skins:revert-success]]',
|
||||
timeout: 5000
|
||||
});
|
||||
});
|
||||
@@ -67,40 +67,48 @@ define('admin/appearance/skins', function () {
|
||||
}),
|
||||
showRevert: true
|
||||
}, function (html) {
|
||||
themeContainer.html(html);
|
||||
translator.translate(html, function (html) {
|
||||
themeContainer.html(html);
|
||||
|
||||
if (config['theme:src']) {
|
||||
var skin = config['theme:src']
|
||||
if (config['theme:src']) {
|
||||
var skin = config['theme:src']
|
||||
.match(/latest\/(\S+)\/bootstrap.min.css/)[1]
|
||||
.replace(/(^|\s)([a-z])/g , function (m,p1,p2) {return p1 + p2.toUpperCase();});
|
||||
|
||||
highlightSelectedTheme(skin);
|
||||
}
|
||||
highlightSelectedTheme(skin);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function highlightSelectedTheme(themeId) {
|
||||
$('[data-theme]')
|
||||
.removeClass('selected')
|
||||
.find('[data-action="use"]').each(function () {
|
||||
if ($(this).parents('[data-theme]').attr('data-theme')) {
|
||||
$(this)
|
||||
.html('Select Skin')
|
||||
.removeClass('btn-success')
|
||||
.addClass('btn-primary');
|
||||
}
|
||||
});
|
||||
translator.translate('[[admin/appearance/skins:select-skin]] || [[admin/appearance/skins:current-skin]]', function (text) {
|
||||
text = text.split(' || ');
|
||||
var select = text[0];
|
||||
var current = text[1];
|
||||
|
||||
if (!themeId) {
|
||||
return;
|
||||
}
|
||||
$('[data-theme]')
|
||||
.removeClass('selected')
|
||||
.find('[data-action="use"]').each(function () {
|
||||
if ($(this).parents('[data-theme]').attr('data-theme')) {
|
||||
$(this)
|
||||
.html(select)
|
||||
.removeClass('btn-success')
|
||||
.addClass('btn-primary');
|
||||
}
|
||||
});
|
||||
|
||||
$('[data-theme="' + themeId + '"]')
|
||||
.addClass('selected')
|
||||
.find('[data-action="use"]')
|
||||
.html('Current Skin')
|
||||
.removeClass('btn-primary')
|
||||
.addClass('btn-success');
|
||||
if (!themeId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$('[data-theme="' + themeId + '"]')
|
||||
.addClass('selected')
|
||||
.find('[data-action="use"]')
|
||||
.html(current)
|
||||
.removeClass('btn-primary')
|
||||
.addClass('btn-success');
|
||||
});
|
||||
}
|
||||
|
||||
return Skins;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use strict";
|
||||
/* global define, app, socket, bootbox, templates, config */
|
||||
|
||||
define('admin/appearance/themes', function () {
|
||||
define('admin/appearance/themes', ['translator'], function (translator) {
|
||||
var Themes = {};
|
||||
|
||||
Themes.init = function () {
|
||||
@@ -28,8 +28,8 @@ define('admin/appearance/themes', function () {
|
||||
app.alert({
|
||||
alert_id: 'admin:theme',
|
||||
type: 'info',
|
||||
title: 'Theme Changed',
|
||||
message: 'Please restart your NodeBB to fully activate this theme',
|
||||
title: '[[admin/appearance/themes:theme-changed]]',
|
||||
message: '[[admin/appearance/themes:restart-to-activate]]',
|
||||
timeout: 5000,
|
||||
clickfn: function () {
|
||||
socket.emit('admin.restart');
|
||||
@@ -38,27 +38,29 @@ define('admin/appearance/themes', function () {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$('#revert_theme').on('click', function () {
|
||||
bootbox.confirm('Are you sure you wish to restore the default NodeBB theme?', function (confirm) {
|
||||
if (confirm) {
|
||||
socket.emit('admin.themes.set', {
|
||||
type: 'local',
|
||||
id: 'nodebb-theme-persona'
|
||||
}, function (err) {
|
||||
if (err) {
|
||||
return app.alertError(err.message);
|
||||
}
|
||||
highlightSelectedTheme('nodebb-theme-persona');
|
||||
app.alert({
|
||||
alert_id: 'admin:theme',
|
||||
type: 'success',
|
||||
title: 'Theme Changed',
|
||||
message: 'You have successfully reverted your NodeBB back to it\'s default theme.',
|
||||
timeout: 3500
|
||||
|
||||
translator.translate('[[admin/appearance/themes:revert-confirm]]', function (revert) {
|
||||
$('#revert_theme').on('click', function () {
|
||||
bootbox.confirm(revert, function (confirm) {
|
||||
if (confirm) {
|
||||
socket.emit('admin.themes.set', {
|
||||
type: 'local',
|
||||
id: 'nodebb-theme-persona'
|
||||
}, function (err) {
|
||||
if (err) {
|
||||
return app.alertError(err.message);
|
||||
}
|
||||
highlightSelectedTheme('nodebb-theme-persona');
|
||||
app.alert({
|
||||
alert_id: 'admin:theme',
|
||||
type: 'success',
|
||||
title: '[[admin/appearance/themes:theme-changed]]',
|
||||
message: '[[admin/appearance/themes:revert-success]]',
|
||||
timeout: 3500
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -70,17 +72,17 @@ define('admin/appearance/themes', function () {
|
||||
var instListEl = $('#installed_themes');
|
||||
|
||||
if (!themes.length) {
|
||||
instListEl.append($('<li/ >').addClass('no-themes').html('No installed themes found'));
|
||||
translator.translate('[[admin/appearance/themes:no-themes]]', function (text) {
|
||||
instListEl.append($('<li/ >').addClass('no-themes').html(text));
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
templates.parse('admin/partials/theme_list', {
|
||||
themes: themes
|
||||
}, function (html) {
|
||||
require(['translator'], function (translator) {
|
||||
translator.translate(html, function (html) {
|
||||
instListEl.html(html);
|
||||
highlightSelectedTheme(config['theme:id']);
|
||||
});
|
||||
translator.translate(html, function (html) {
|
||||
instListEl.html(html);
|
||||
highlightSelectedTheme(config['theme:id']);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -88,19 +90,25 @@ define('admin/appearance/themes', function () {
|
||||
};
|
||||
|
||||
function highlightSelectedTheme(themeId) {
|
||||
$('[data-theme]')
|
||||
.removeClass('selected')
|
||||
.find('[data-action="use"]')
|
||||
.html('Select Theme')
|
||||
.removeClass('btn-success')
|
||||
.addClass('btn-primary');
|
||||
translator.translate('[[admin/appearance/themes:select-theme]] || [[admin/appearance/themes:current-theme]]', function (text) {
|
||||
text = text.split(' || ');
|
||||
var select = text[0];
|
||||
var current = text[1];
|
||||
|
||||
$('[data-theme="' + themeId + '"]')
|
||||
.addClass('selected')
|
||||
.find('[data-action="use"]')
|
||||
.html('Current Theme')
|
||||
.removeClass('btn-primary')
|
||||
.addClass('btn-success');
|
||||
$('[data-theme]')
|
||||
.removeClass('selected')
|
||||
.find('[data-action="use"]')
|
||||
.html(select)
|
||||
.removeClass('btn-success')
|
||||
.addClass('btn-primary');
|
||||
|
||||
$('[data-theme="' + themeId + '"]')
|
||||
.addClass('selected')
|
||||
.find('[data-action="use"]')
|
||||
.html(current)
|
||||
.removeClass('btn-primary')
|
||||
.addClass('btn-success');
|
||||
});
|
||||
}
|
||||
|
||||
return Themes;
|
||||
|
||||
@@ -1,114 +1,148 @@
|
||||
"use strict";
|
||||
/*globals define, admin, ajaxify, RELATIVE_PATH*/
|
||||
/* globals socket, app, define, ajaxify, config */
|
||||
|
||||
define(function () {
|
||||
var search = {},
|
||||
searchIndex;
|
||||
define('admin/modules/search', ['mousetrap'], function (mousetrap) {
|
||||
var search = {};
|
||||
|
||||
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
|
||||
// remove all lines without a match
|
||||
.replace(new RegExp('^(?:(?!' + term + ').)*$', 'gmi'), '')
|
||||
// get up to 25 characaters of context on both sides of the match
|
||||
// and wrap the match in a `.search-match` element
|
||||
.replace(
|
||||
new RegExp('^[\\s\\S]*?(.{0,25})(' + term + ')(.{0,25})[\\s\\S]*?$', 'gmi'),
|
||||
'...$1<span class="search-match">$2</span>$3...<br>'
|
||||
)
|
||||
// collapse whitespace
|
||||
.replace(/(?:\n ?)+/g, '\n');
|
||||
|
||||
return '<li role="presentation" class="result">' +
|
||||
'<a role= "menuitem" href= "' + config.relative_path + '/' + namespace + '" >' +
|
||||
title +
|
||||
'<br>' +
|
||||
'<small><code>' +
|
||||
results +
|
||||
'</small></code>' +
|
||||
'</a>' +
|
||||
'</li>';
|
||||
}).join('');
|
||||
return html;
|
||||
}
|
||||
|
||||
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(/<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, ' ');
|
||||
}
|
||||
socket.emit('admin.getSearchDict', {}, function (err, dict) {
|
||||
if (err) {
|
||||
app.alertError(err);
|
||||
throw err;
|
||||
}
|
||||
|
||||
delete searchIndex['/admin/header.tpl'];
|
||||
delete searchIndex['/admin/footer.tpl'];
|
||||
|
||||
setupACPSearch();
|
||||
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 selected = menu.find('li.result > a.focus').attr('href');
|
||||
if (!selected.length) {
|
||||
selected = menu.find('li.result > a').first().attr('href');
|
||||
}
|
||||
var href = selected ? selected : config.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'));
|
||||
mousetrap(input[0]).bind(['up', 'down'], function (ev, key) {
|
||||
var next;
|
||||
if (key === 'up') {
|
||||
next = menu.find('li.result > a.focus').removeClass('focus').parent().prev('.result').children();
|
||||
if (!next.length) {
|
||||
next = menu.find('li.result > a').last();
|
||||
}
|
||||
next.addClass('focus');
|
||||
if (menu[0].getBoundingClientRect().top > next[0].getBoundingClientRect().top) {
|
||||
next[0].scrollIntoView(true);
|
||||
}
|
||||
} else if (key === 'down') {
|
||||
next = menu.find('li.result > a.focus').removeClass('focus').parent().next('.result').children();
|
||||
if (!next.length) {
|
||||
next = menu.find('li.result > a').first();
|
||||
}
|
||||
next.addClass('focus');
|
||||
if (menu[0].getBoundingClientRect().bottom < next[0].getBoundingClientRect().bottom) {
|
||||
next[0].scrollIntoView(false);
|
||||
}
|
||||
}
|
||||
|
||||
ev.preventDefault();
|
||||
});
|
||||
|
||||
var prevValue;
|
||||
|
||||
input.on('keyup focus', function () {
|
||||
var $input = $(this),
|
||||
value = $input.val().toLowerCase(),
|
||||
menuItems = $('#acp-search .dropdown-menu').html('');
|
||||
var value = input.val().toLowerCase();
|
||||
|
||||
function toUpperCase(txt) {
|
||||
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
|
||||
if (value === prevValue) {
|
||||
return;
|
||||
}
|
||||
prevValue = value;
|
||||
|
||||
firstResult = null;
|
||||
menu.children('.result').remove();
|
||||
|
||||
if (value.length >= 3) {
|
||||
for (var file in searchIndex) {
|
||||
if (searchIndex.hasOwnProperty(file)) {
|
||||
var position = searchIndex[file].indexOf(value);
|
||||
var len = value.length;
|
||||
var results;
|
||||
|
||||
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, '<span class="search-match">' + value + '</span>');
|
||||
menu.toggleClass('state-start-typing', len === 0);
|
||||
menu.toggleClass('state-keep-typing', len > 0 && len < 3);
|
||||
|
||||
if (len >= 3) {
|
||||
menu.prepend(find(dict, value));
|
||||
|
||||
for (var t in title) {
|
||||
if (title.hasOwnProperty(t)) {
|
||||
title[t] = title[t]
|
||||
.replace('-', ' ')
|
||||
.replace(/\w\S*/g, toUpperCase);
|
||||
}
|
||||
}
|
||||
results = menu.children('.result').length;
|
||||
|
||||
title = title.join(' > ');
|
||||
href = RELATIVE_PATH + href;
|
||||
firstResult = firstResult ? firstResult : href;
|
||||
menu.toggleClass('state-no-results', !results);
|
||||
menu.toggleClass('state-yes-results', !!results);
|
||||
|
||||
if ($.inArray(href, routes) !== -1) {
|
||||
menuItems.append('<li role="presentation"><a role="menuitem" href="' + href + '">' + title + '<br /><small><code>...' + description + '...</code></small></a></li>');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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>');
|
||||
}
|
||||
menu.find('.search-forum')
|
||||
.not('.divider')
|
||||
.find('a')
|
||||
.attr('href', config.relative_path + '/search/' + value)
|
||||
.find('strong')
|
||||
.html(value);
|
||||
} else {
|
||||
menuItems.append('<li role="presentation"><a role="menuitem" href="#">Start typing to see results...</a></li>');
|
||||
menu.removeClass('state-no-results state-yes-results');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
(function (factory) {
|
||||
'use strict';
|
||||
function loadClient(language, namespace) {
|
||||
return Promise.resolve(jQuery.getJSON(config.relative_path + '/api/language/' + language + '/' + namespace));
|
||||
return Promise.resolve(jQuery.getJSON(config.relative_path + '/api/language/' + language + '/' + encodeURIComponent(namespace)));
|
||||
}
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
// AMD. Register as a named module
|
||||
@@ -339,6 +339,68 @@
|
||||
|
||||
Translator.moduleFactories = {};
|
||||
|
||||
/**
|
||||
* Remove the translator patterns from text
|
||||
* @param {string} text
|
||||
* @returns {string}
|
||||
*/
|
||||
Translator.removePatterns = function removePatterns(text) {
|
||||
var len = text.length;
|
||||
var cursor = 0;
|
||||
var lastBreak = 0;
|
||||
var level = 0;
|
||||
var out = '';
|
||||
var sub;
|
||||
|
||||
while (cursor < len) {
|
||||
sub = text.slice(cursor, cursor + 2);
|
||||
if (sub === '[[') {
|
||||
if (level === 0) {
|
||||
out += text.slice(lastBreak, cursor);
|
||||
}
|
||||
level += 1;
|
||||
cursor += 2;
|
||||
} else if (sub === ']]') {
|
||||
level -= 1;
|
||||
cursor += 2;
|
||||
if (level === 0) {
|
||||
lastBreak = cursor;
|
||||
}
|
||||
} else {
|
||||
cursor += 1;
|
||||
}
|
||||
}
|
||||
out += text.slice(lastBreak, cursor);
|
||||
return out;
|
||||
};
|
||||
|
||||
/**
|
||||
* Escape translator patterns in text
|
||||
* @param {string} text
|
||||
* @returns {string}
|
||||
*/
|
||||
Translator.escape = function escape(text) {
|
||||
return typeof text === 'string' ? text.replace(/\[\[([\S]*?)\]\]/g, '\\[\\[$1\\]\\]') : text;
|
||||
};
|
||||
|
||||
/**
|
||||
* Unescape escaped translator patterns in text
|
||||
* @param {string} text
|
||||
* @returns {string}
|
||||
*/
|
||||
Translator.unescape = function unescape(text) {
|
||||
return typeof text === 'string' ? text.replace(/\\\[\\\[([\S]*?)\\\]\\\]/g, '[[$1]]') : text;
|
||||
};
|
||||
|
||||
/**
|
||||
* Construct a translator pattern
|
||||
*/
|
||||
Translator.compile = function compile() {
|
||||
var args = Array.prototype.slice.call(arguments, 0);
|
||||
|
||||
return '[[' + args.join(', ') + ']]';
|
||||
};
|
||||
|
||||
return Translator;
|
||||
}());
|
||||
|
||||
@@ -348,12 +410,16 @@
|
||||
*/
|
||||
Translator: Translator,
|
||||
|
||||
compile: Translator.compile,
|
||||
escape: Translator.escape,
|
||||
unescape: Translator.unescape,
|
||||
getLanguage: Translator.getLanguage,
|
||||
|
||||
/**
|
||||
* Legacy translator function for backwards compatibility
|
||||
*/
|
||||
translate: function translate(text, language, callback) {
|
||||
// console.warn('[translator] `translator.translate(text, [lang, ]callback)` is deprecated. ' +
|
||||
// 'Use the `translator.Translator` class instead.');
|
||||
// TODO: deprecate?
|
||||
|
||||
var cb = callback;
|
||||
var lang = language;
|
||||
@@ -373,31 +439,6 @@
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Construct a translator pattern
|
||||
* @param {string} name - Translation name
|
||||
* @param {string[]} args - Optional arguments for the pattern
|
||||
*/
|
||||
compile: function compile() {
|
||||
var args = Array.prototype.slice.call(arguments, 0);
|
||||
|
||||
return '[[' + args.join(', ') + ']]';
|
||||
},
|
||||
|
||||
/**
|
||||
* Escape translation patterns from text
|
||||
*/
|
||||
escape: function escape(text) {
|
||||
return typeof text === 'string' ? text.replace(/\[\[([\S]*?)\]\]/g, '\\[\\[$1\\]\\]') : text;
|
||||
},
|
||||
|
||||
/**
|
||||
* Unescape translation patterns from text
|
||||
*/
|
||||
unescape: function unescape(text) {
|
||||
return typeof text === 'string' ? text.replace(/\\\[\\\[([\S]*?)\\\]\\\]/g, '[[$1]]') : text;
|
||||
},
|
||||
|
||||
/**
|
||||
* Add translations to the cache
|
||||
*/
|
||||
@@ -422,11 +463,6 @@
|
||||
adaptor.getTranslations(language, namespace, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the language of the current environment, falling back to defaults
|
||||
*/
|
||||
getLanguage: Translator.getLanguage,
|
||||
|
||||
toggleTimeagoShorthand: function toggleTimeagoShorthand() {
|
||||
var tmp = assign({}, jQuery.timeago.settings.strings);
|
||||
jQuery.timeago.settings.strings = assign({}, adaptor.timeagoShort);
|
||||
|
||||
183
src/admin/search.js
Normal file
183
src/admin/search.js
Normal file
@@ -0,0 +1,183 @@
|
||||
'use strict';
|
||||
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var async = require('async');
|
||||
var sanitizeHTML = require('sanitize-html');
|
||||
|
||||
var languages = require('../languages');
|
||||
var utils = require('../../public/src/utils');
|
||||
var Translator = require('../../public/src/modules/translator').Translator;
|
||||
|
||||
function filterDirectories(directories) {
|
||||
return directories.map(function (dir) {
|
||||
// get the relative path
|
||||
return dir.replace(/^.*(admin.*?).tpl$/, '$1');
|
||||
}).filter(function (dir) {
|
||||
// exclude partials
|
||||
// only include subpaths
|
||||
// exclude category.tpl, group.tpl, category-analytics.tpl
|
||||
return !dir.includes('/partials/') &&
|
||||
/\/.*\//.test(dir) &&
|
||||
!/category|group|category\-analytics$/.test(dir);
|
||||
});
|
||||
}
|
||||
|
||||
function getAdminNamespaces(callback) {
|
||||
utils.walk(path.resolve(__dirname, '../../public/templates/admin'), function (err, directories) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(null, filterDirectories(directories));
|
||||
});
|
||||
}
|
||||
|
||||
function sanitize(html) {
|
||||
// reduce the template to just meaningful text
|
||||
// remove all tags and strip out scripts, etc completely
|
||||
return sanitizeHTML(html, {
|
||||
allowedTags: [],
|
||||
allowedAttributes: [],
|
||||
});
|
||||
}
|
||||
|
||||
function simplify(translations) {
|
||||
return translations
|
||||
// remove all mustaches
|
||||
.replace(/(?:\{{1,2}[^\}]*?\}{1,2})/g, '')
|
||||
// collapse whitespace
|
||||
.replace(/(?:[ \t]*[\n\r]+[ \t]*)+/g, '\n')
|
||||
.replace(/[\t ]+/g, ' ');
|
||||
}
|
||||
|
||||
function nsToTitle(namespace) {
|
||||
return namespace.replace('admin/', '').split('/').map(function (str) {
|
||||
return str[0].toUpperCase() + str.slice(1);
|
||||
}).join(' > ');
|
||||
}
|
||||
|
||||
var fallbackCacheInProgress = {};
|
||||
var fallbackCache = {};
|
||||
|
||||
function initFallback(namespace, callback) {
|
||||
fs.readFile(path.resolve(__dirname, '../../public/templates/', namespace + '.tpl'), function (err, file) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var template = file.toString();
|
||||
|
||||
var translations = sanitize(template);
|
||||
translations = Translator.removePatterns(translations);
|
||||
translations = simplify(translations);
|
||||
translations += '\n' + nsToTitle(namespace);
|
||||
|
||||
callback(null, {
|
||||
namespace: namespace,
|
||||
translations: translations,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function fallback(namespace, callback) {
|
||||
if (fallbackCache[namespace]) {
|
||||
return callback(null, fallbackCache[namespace]);
|
||||
}
|
||||
if (fallbackCacheInProgress[namespace]) {
|
||||
return fallbackCacheInProgress[namespace].push(callback);
|
||||
}
|
||||
|
||||
fallbackCacheInProgress[namespace] = [function (err, params) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(null, params);
|
||||
}];
|
||||
initFallback(namespace, function (err, params) {
|
||||
fallbackCacheInProgress[namespace].forEach(function (fn) {
|
||||
fn(err, params);
|
||||
});
|
||||
fallbackCacheInProgress[namespace] = null;
|
||||
fallbackCache[namespace] = params;
|
||||
});
|
||||
}
|
||||
|
||||
function initDict(language, callback) {
|
||||
getAdminNamespaces(function (err, namespaces) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
async.map(namespaces, function (namespace, cb) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
languages.get(language, namespace, next);
|
||||
},
|
||||
function (translations, next) {
|
||||
if (!translations || !Object.keys(translations).length) {
|
||||
return next(Error('No translations for ' + language + '/' + namespace));
|
||||
}
|
||||
|
||||
// join all translations into one string separated by newlines
|
||||
var str = Object.keys(translations).map(function (key) {
|
||||
return translations[key];
|
||||
}).join('\n');
|
||||
|
||||
next(null, {
|
||||
namespace: namespace,
|
||||
translations: str,
|
||||
});
|
||||
}
|
||||
], function (err, params) {
|
||||
if (err) {
|
||||
return fallback(namespace, function (err, params) {
|
||||
if (err) {
|
||||
return cb({
|
||||
namespace: namespace,
|
||||
translations: '',
|
||||
});
|
||||
}
|
||||
|
||||
cb(null, params);
|
||||
});
|
||||
}
|
||||
|
||||
cb(null, params);
|
||||
});
|
||||
}, callback);
|
||||
});
|
||||
}
|
||||
|
||||
var cacheInProgress = {};
|
||||
var cache = {};
|
||||
|
||||
function getDictionary(language, callback) {
|
||||
if (cache[language]) {
|
||||
return callback(null, cache[language]);
|
||||
}
|
||||
if (cacheInProgress[language]) {
|
||||
return cacheInProgress[language].push(callback);
|
||||
}
|
||||
|
||||
cacheInProgress[language] = [function (err, params) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(null, params);
|
||||
}];
|
||||
initDict(language, function (err, params) {
|
||||
cacheInProgress[language].forEach(function (fn) {
|
||||
fn(err, params);
|
||||
});
|
||||
cacheInProgress[language] = null;
|
||||
cache[language] = params;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports.getDictionary = getDictionary;
|
||||
module.exports.filterDirectories = filterDirectories;
|
||||
module.exports.simplify = simplify;
|
||||
module.exports.sanitize = sanitize;
|
||||
@@ -57,30 +57,24 @@ var utils = require('../public/src/utils');
|
||||
};
|
||||
|
||||
Groups.getGroupsFromSet = function (set, uid, start, stop, callback) {
|
||||
var method;
|
||||
var args;
|
||||
if (set === 'groups:visible:name') {
|
||||
method = db.getSortedSetRangeByLex;
|
||||
args = [set, '-', '+', start, stop - start + 1, done];
|
||||
} else {
|
||||
method = db.getSortedSetRevRange;
|
||||
args = [set, start, stop, done];
|
||||
}
|
||||
method.apply(null, args);
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
if (set === 'groups:visible:name') {
|
||||
db.getSortedSetRangeByLex(set, '-', '+', start, stop - start + 1, next);
|
||||
} else {
|
||||
db.getSortedSetRevRange(set, start, stop, next);
|
||||
}
|
||||
},
|
||||
function (groupNames, next) {
|
||||
if (set === 'groups:visible:name') {
|
||||
groupNames = groupNames.map(function (name) {
|
||||
return name.split(':')[1];
|
||||
});
|
||||
}
|
||||
|
||||
function done(err, groupNames) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
Groups.getGroupsAndMembers(groupNames, next);
|
||||
}
|
||||
|
||||
if (set === 'groups:visible:name') {
|
||||
groupNames = groupNames.map(function (name) {
|
||||
return name.split(':')[1];
|
||||
});
|
||||
}
|
||||
|
||||
Groups.getGroupsAndMembers(groupNames, callback);
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
|
||||
Groups.getGroups = function (set, start, stop, callback) {
|
||||
|
||||
@@ -92,6 +92,10 @@ module.exports = function (Groups) {
|
||||
return callback(new Error('[[error:group-name-too-long]]'));
|
||||
}
|
||||
|
||||
if (!Groups.isPrivilegeGroup(name) && name.indexOf(':') !== -1) {
|
||||
return callback(new Error('[[error:invalid-group-name]]'));
|
||||
}
|
||||
|
||||
if (name.indexOf('/') !== -1 || !utils.slugify(name)) {
|
||||
return callback(new Error('[[error:invalid-group-name]]'));
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ Languages.list = function (callback) {
|
||||
|
||||
fs.readFile(configPath, function (err, stream) {
|
||||
if (err) {
|
||||
next();
|
||||
return next(err);
|
||||
}
|
||||
languages.push(JSON.parse(stream.toString()));
|
||||
next();
|
||||
|
||||
@@ -12,7 +12,6 @@ var plugins = require('../plugins');
|
||||
var utils = require('../../public/src/utils');
|
||||
|
||||
var Templates = {};
|
||||
var searchIndex = {};
|
||||
|
||||
Templates.compile = function (callback) {
|
||||
callback = callback || function () {};
|
||||
@@ -131,10 +130,6 @@ function compile(callback) {
|
||||
}
|
||||
}
|
||||
|
||||
if (relativePath.match(/^\/admin\/[\s\S]*?/)) {
|
||||
addIndex(relativePath, file);
|
||||
}
|
||||
|
||||
mkdirp.sync(path.join(viewsPath, relativePath.split('/').slice(0, -1).join('/')));
|
||||
fs.writeFile(path.join(viewsPath, relativePath), file, next);
|
||||
}, function (err) {
|
||||
@@ -143,25 +138,11 @@ function compile(callback) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
compileIndex(viewsPath, function (err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
winston.verbose('[meta/templates] Successfully compiled templates.');
|
||||
winston.verbose('[meta/templates] Successfully compiled templates.');
|
||||
|
||||
callback();
|
||||
});
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function addIndex(path, file) {
|
||||
searchIndex[path] = file;
|
||||
}
|
||||
|
||||
function compileIndex(viewsPath, callback) {
|
||||
fs.writeFile(path.join(viewsPath, '/indexed.json'), JSON.stringify(searchIndex), callback);
|
||||
}
|
||||
|
||||
module.exports = Templates;
|
||||
@@ -111,6 +111,7 @@ module.exports = function (middleware) {
|
||||
var clean = req.path.replace(/^\/api/, '').replace(/^\/|\/$/g, '');
|
||||
var parts = clean.split('/').slice(0, 3);
|
||||
parts.forEach(function (p, index) {
|
||||
p = decodeURIComponent(p);
|
||||
parts[index] = index ? parts[0] + '-' + p : 'page-' + (p || 'home');
|
||||
});
|
||||
return parts.join(' ');
|
||||
|
||||
@@ -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').getDictionary;
|
||||
|
||||
var SocketAdmin = {
|
||||
user: require('./admin/user'),
|
||||
@@ -277,5 +278,15 @@ 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, callback);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
module.exports = SocketAdmin;
|
||||
|
||||
@@ -90,12 +90,12 @@ module.exports = function (Topics) {
|
||||
|
||||
Topics.post = function (data, callback) {
|
||||
var uid = data.uid;
|
||||
var title = String(data.title).trim();
|
||||
data.title = String(data.title).trim();
|
||||
data.tags = data.tags || [];
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
check(title, meta.config.minimumTitleLength, meta.config.maximumTitleLength, 'title-too-short', 'title-too-long', next);
|
||||
check(data.title, meta.config.minimumTitleLength, meta.config.maximumTitleLength, 'title-too-short', 'title-too-long', next);
|
||||
},
|
||||
function (next) {
|
||||
check(data.tags, meta.config.minimumTagsPerTopic, meta.config.maximumTagsPerTopic, 'not-enough-tags', 'too-many-tags', next);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div id="skins" class="row skins">
|
||||
<div class="directory row" id="bootstrap_themes">
|
||||
<i class="fa fa-refresh fa-spin"></i> Loading Skins
|
||||
<i class="fa fa-refresh fa-spin"></i> [[admin/appearance/skins:loading]]
|
||||
</div>
|
||||
|
||||
<div data-type="bootswatch" data-theme="" data-css="">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div id="themes" class="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>
|
||||
|
||||
|
||||
@@ -149,7 +149,23 @@
|
||||
<div class="" id="acp-search" >
|
||||
<div class="dropdown">
|
||||
<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>
|
||||
</form>
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
|
||||
<!-- IF themes.url -->
|
||||
<p>
|
||||
<a href="{themes.url}" target="_blank">Homepage</a>
|
||||
<a href="{themes.url}" target="_blank">[[admin/appearance/themes:homepage]]</a>
|
||||
</p>
|
||||
<!-- ENDIF themes.url -->
|
||||
</div>
|
||||
<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">
|
||||
<!-- IF themes.skin -->Select Skin<!-- ELSE -->Select Theme<!-- ENDIF themes.skin -->
|
||||
<!-- IF themes.skin -->[[admin/appearance/skins:select-skin]]<!-- ELSE -->[[admin/appearance/themes:select-theme]]<!-- ENDIF themes.skin -->
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -255,6 +255,13 @@ describe('Groups', function () {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail if group name is invalid', function (done) {
|
||||
Groups.create({name: 'not:valid'}, function (err) {
|
||||
assert.equal(err.message, '[[error:invalid-group-name]]');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.hide()', function () {
|
||||
|
||||
82
test/search-admin.js
Normal file
82
test/search-admin.js
Normal file
@@ -0,0 +1,82 @@
|
||||
'use strict';
|
||||
/*global require*/
|
||||
|
||||
var assert = require('assert');
|
||||
var search = require('../src/admin/search');
|
||||
|
||||
describe('admin search', function () {
|
||||
describe('filterDirectories', function () {
|
||||
it('should resolve all paths to relative paths', function (done) {
|
||||
assert.deepEqual(search.filterDirectories([
|
||||
'hfjksfd/fdsgagag/admin/gdhgfsdg/sggag.tpl',
|
||||
]), [
|
||||
'admin/gdhgfsdg/sggag',
|
||||
]);
|
||||
done();
|
||||
});
|
||||
it('should exclude partials', function (done) {
|
||||
assert.deepEqual(search.filterDirectories([
|
||||
'hfjksfd/fdsgagag/admin/gdhgfsdg/sggag.tpl',
|
||||
'dfahdfsgf/admin/partials/hgkfds/fdhsdfh.tpl',
|
||||
]), [
|
||||
'admin/gdhgfsdg/sggag',
|
||||
]);
|
||||
done();
|
||||
});
|
||||
it('should exclude files in the admin directory', function (done) {
|
||||
assert.deepEqual(search.filterDirectories([
|
||||
'hfjksfd/fdsgagag/admin/gdhgfsdg/sggag.tpl',
|
||||
'dfdasg/admin/hjkdfsk.tpl',
|
||||
]), [
|
||||
'admin/gdhgfsdg/sggag',
|
||||
]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitize', function () {
|
||||
it('should strip out scripts', function (done) {
|
||||
assert.equal(
|
||||
search.sanitize('Pellentesque tristique senectus' +
|
||||
'<script>alert("nope");</script> habitant morbi'),
|
||||
'Pellentesque tristique senectus' +
|
||||
' habitant morbi'
|
||||
);
|
||||
done();
|
||||
});
|
||||
it('should remove all tags', function (done) {
|
||||
assert.equal(
|
||||
search.sanitize('<p>Pellentesque <b>habitant morbi</b> tristique senectus' +
|
||||
'Aenean <i>vitae</i> est.Mauris <a href="placerat">eleifend</a> leo.</p>'),
|
||||
'Pellentesque habitant morbi tristique senectus' +
|
||||
'Aenean vitae est.Mauris eleifend leo.'
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('simplify', function () {
|
||||
it('should remove all mustaches', function (done) {
|
||||
assert.equal(
|
||||
search.simplify(
|
||||
'Pellentesque tristique {{senectus}}habitant morbi' +
|
||||
'liquam tincidunt {mauris.eu}risus'
|
||||
),
|
||||
'Pellentesque tristique habitant morbi' +
|
||||
'liquam tincidunt risus'
|
||||
);
|
||||
done();
|
||||
});
|
||||
it('should collapse all whitespace', function (done) {
|
||||
assert.equal(
|
||||
search.simplify(
|
||||
'Pellentesque tristique habitant morbi' +
|
||||
' \n\n liquam tincidunt mauris eu risus.'
|
||||
),
|
||||
'Pellentesque tristique habitant morbi' +
|
||||
'\nliquam tincidunt mauris eu risus.'
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -233,3 +233,15 @@ describe('Translator modules', function () {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Translator static methods', function () {
|
||||
describe('.removePatterns', function () {
|
||||
it('should remove translator patterns from text', function (done) {
|
||||
assert.strictEqual(
|
||||
Translator.removePatterns('Lorem ipsum dolor [[sit:amet]], consectetur adipiscing elit. [[sed:vitae, [[semper:dolor]]]] lorem'),
|
||||
'Lorem ipsum dolor , consectetur adipiscing elit. lorem'
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user