mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-12-17 14:00:29 +01:00
Compare commits
56 Commits
fullname-i
...
v2.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
522663d599 | ||
|
|
a61bbc8427 | ||
|
|
315566db6f | ||
|
|
1523d0cc50 | ||
|
|
e190ff875a | ||
|
|
54091c818c | ||
|
|
57468fb690 | ||
|
|
b9cc5c0905 | ||
|
|
66651d42fb | ||
|
|
48ab453a39 | ||
|
|
6c29b2a71f | ||
|
|
4be5a723b5 | ||
|
|
c14f72bbad | ||
|
|
799acf83c4 | ||
|
|
1dceb2b223 | ||
|
|
a4c5dcce6c | ||
|
|
eafd187941 | ||
|
|
0faae7343f | ||
|
|
a7fc159353 | ||
|
|
3b8f35a51d | ||
|
|
ba792b2920 | ||
|
|
6e6515fc47 | ||
|
|
29aaa9ee2d | ||
|
|
45d5b5a586 | ||
|
|
6bfc53fc6f | ||
|
|
cb34728421 | ||
|
|
66c45996e1 | ||
|
|
2e55ddc07c | ||
|
|
fec28a7e9e | ||
|
|
1803c29ec9 | ||
|
|
9dccd439f9 | ||
|
|
a9df137965 | ||
|
|
d60cb6c6e5 | ||
|
|
862381008a | ||
|
|
d010a61854 | ||
|
|
fe610f0daf | ||
|
|
70f35e56ec | ||
|
|
13e3b9440b | ||
|
|
ab673e362b | ||
|
|
b5b6e56f3c | ||
|
|
00aadc047f | ||
|
|
306a8dd2cf | ||
|
|
4b35f654a3 | ||
|
|
d3969e5656 | ||
|
|
1a0b82f434 | ||
|
|
3c129d7e8f | ||
|
|
2ea55c4311 | ||
|
|
1da1f21c79 | ||
|
|
3d4adaced4 | ||
|
|
68d88345e7 | ||
|
|
cc3298e688 | ||
|
|
668add8cab | ||
|
|
fd97f9ffcc | ||
|
|
36f172c1ac | ||
|
|
163936e2fd | ||
|
|
a1a1e8da18 |
@@ -18,3 +18,10 @@ logs/
|
|||||||
.eslintrc
|
.eslintrc
|
||||||
test/files
|
test/files
|
||||||
*.min.js
|
*.min.js
|
||||||
|
|
||||||
|
/public/src/app.js
|
||||||
|
/public/src/modules/translator.common.js
|
||||||
|
/public/src/modules/pictureCropper.js
|
||||||
|
/public/src/modules/ace-editor.js
|
||||||
|
/public/src/client/account/header.js
|
||||||
|
/public/src/client/test.js
|
||||||
11
Gruntfile.js
11
Gruntfile.js
@@ -105,9 +105,9 @@ module.exports = function (grunt) {
|
|||||||
'app.js',
|
'app.js',
|
||||||
'install/*.js',
|
'install/*.js',
|
||||||
'src/**/*.js',
|
'src/**/*.js',
|
||||||
'public/src/modules/translator.js',
|
'public/src/modules/translator.common.js',
|
||||||
'public/src/modules/helpers.js',
|
'public/src/modules/helpers.common.js',
|
||||||
'public/src/utils.js',
|
'public/src/utils.common.js',
|
||||||
serverUpdated,
|
serverUpdated,
|
||||||
'!src/upgrades/**',
|
'!src/upgrades/**',
|
||||||
],
|
],
|
||||||
@@ -137,9 +137,10 @@ module.exports = function (grunt) {
|
|||||||
});
|
});
|
||||||
const build = require('./src/meta/build');
|
const build = require('./src/meta/build');
|
||||||
if (!grunt.option('skip')) {
|
if (!grunt.option('skip')) {
|
||||||
await build.build(true);
|
await build.build(true, { webpack: false });
|
||||||
}
|
}
|
||||||
run();
|
run();
|
||||||
|
await build.webpack({ watch: true });
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -183,7 +184,7 @@ module.exports = function (grunt) {
|
|||||||
return run();
|
return run();
|
||||||
}
|
}
|
||||||
|
|
||||||
require('./src/meta/build').build([compiling], (err) => {
|
require('./src/meta/build').build([compiling], { webpack: false }, (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
winston.error(err.stack);
|
winston.error(err.stack);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "nodebb",
|
"name": "nodebb",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"description": "NodeBB Forum",
|
"description": "NodeBB Forum",
|
||||||
"version": "1.19.3",
|
"version": "2.0.0-beta.0",
|
||||||
"homepage": "http://www.nodebb.org",
|
"homepage": "http://www.nodebb.org",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -40,6 +40,7 @@
|
|||||||
"bootstrap": "3.4.1",
|
"bootstrap": "3.4.1",
|
||||||
"chalk": "4.1.2",
|
"chalk": "4.1.2",
|
||||||
"chart.js": "2.9.4",
|
"chart.js": "2.9.4",
|
||||||
|
"clean-webpack-plugin": "4.0.0",
|
||||||
"cli-graph": "3.2.2",
|
"cli-graph": "3.2.2",
|
||||||
"clipboard": "2.0.10",
|
"clipboard": "2.0.10",
|
||||||
"colors": "1.4.0",
|
"colors": "1.4.0",
|
||||||
@@ -60,6 +61,7 @@
|
|||||||
"express": "4.17.3",
|
"express": "4.17.3",
|
||||||
"express-session": "1.17.2",
|
"express-session": "1.17.2",
|
||||||
"express-useragent": "1.0.15",
|
"express-useragent": "1.0.15",
|
||||||
|
"file-loader": "6.2.0",
|
||||||
"graceful-fs": "4.2.9",
|
"graceful-fs": "4.2.9",
|
||||||
"helmet": "5.0.2",
|
"helmet": "5.0.2",
|
||||||
"html-to-text": "8.1.0",
|
"html-to-text": "8.1.0",
|
||||||
@@ -85,20 +87,20 @@
|
|||||||
"multiparty": "4.2.3",
|
"multiparty": "4.2.3",
|
||||||
"@nodebb/bootswatch": "3.4.2",
|
"@nodebb/bootswatch": "3.4.2",
|
||||||
"nconf": "0.11.3",
|
"nconf": "0.11.3",
|
||||||
"nodebb-plugin-2factor": "3.0.4",
|
"nodebb-plugin-2factor": "https://github.com/NodeBB/nodebb-plugin-2factor#webpack5",
|
||||||
"nodebb-plugin-composer-default": "7.0.20",
|
"nodebb-plugin-composer-default": "https://github.com/nodebb/nodebb-plugin-composer-default.git#webpack5",
|
||||||
"nodebb-plugin-dbsearch": "5.1.3",
|
"nodebb-plugin-dbsearch": "5.1.3",
|
||||||
"nodebb-plugin-emoji": "3.5.17",
|
"nodebb-plugin-emoji": "https://github.com/NodeBB/nodebb-plugin-emoji.git#webpack5",
|
||||||
"nodebb-plugin-emoji-android": "2.0.5",
|
"nodebb-plugin-emoji-android": "2.0.5",
|
||||||
"nodebb-plugin-markdown": "9.0.8",
|
"nodebb-plugin-markdown": "https://github.com/julianlam/nodebb-plugin-markdown.git#webpack5",
|
||||||
"nodebb-plugin-mentions": "3.0.6",
|
"nodebb-plugin-mentions": "3.0.6",
|
||||||
"nodebb-plugin-spam-be-gone": "0.7.13",
|
"nodebb-plugin-spam-be-gone": "https://github.com/akhoury/nodebb-plugin-spam-be-gone.git#webpack5",
|
||||||
"nodebb-rewards-essentials": "0.2.1",
|
"nodebb-rewards-essentials": "0.2.1",
|
||||||
"nodebb-theme-lavender": "5.3.2",
|
"nodebb-theme-lavender": "https://github.com/nodebb/nodebb-theme-lavender.git#webpack5",
|
||||||
"nodebb-theme-persona": "11.4.1",
|
"nodebb-theme-persona": "https://github.com/nodebb/nodebb-theme-persona.git#webpack5",
|
||||||
"nodebb-theme-slick": "1.4.23",
|
"nodebb-theme-slick": "https://github.com/pichalite/nodebb-theme-slick.git#webpack5",
|
||||||
"nodebb-theme-vanilla": "12.1.17",
|
"nodebb-theme-vanilla": "12.1.17",
|
||||||
"nodebb-widget-essentials": "5.0.9",
|
"nodebb-widget-essentials": "https://github.com/nodebb/nodebb-widget-essentials.git#webpack5",
|
||||||
"nodemailer": "6.7.2",
|
"nodemailer": "6.7.2",
|
||||||
"nprogress": "0.2.0",
|
"nprogress": "0.2.0",
|
||||||
"passport": "0.5.2",
|
"passport": "0.5.2",
|
||||||
@@ -112,7 +114,6 @@
|
|||||||
"ioredis": "4.28.5",
|
"ioredis": "4.28.5",
|
||||||
"request": "2.88.2",
|
"request": "2.88.2",
|
||||||
"request-promise-native": "1.0.9",
|
"request-promise-native": "1.0.9",
|
||||||
"requirejs": "2.3.6",
|
|
||||||
"rimraf": "3.0.2",
|
"rimraf": "3.0.2",
|
||||||
"rss": "1.2.2",
|
"rss": "1.2.2",
|
||||||
"sanitize-html": "2.7.0",
|
"sanitize-html": "2.7.0",
|
||||||
@@ -136,6 +137,8 @@
|
|||||||
"uglify-es": "3.3.9",
|
"uglify-es": "3.3.9",
|
||||||
"validator": "13.7.0",
|
"validator": "13.7.0",
|
||||||
"visibilityjs": "2.0.2",
|
"visibilityjs": "2.0.2",
|
||||||
|
"webpack": "^5.68.0",
|
||||||
|
"webpack-merge": "5.8.0",
|
||||||
"winston": "3.6.0",
|
"winston": "3.6.0",
|
||||||
"xml": "1.0.1",
|
"xml": "1.0.1",
|
||||||
"xregexp": "5.1.0",
|
"xregexp": "5.1.0",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const path = require('path');
|
|||||||
const childProcess = require('child_process');
|
const childProcess = require('child_process');
|
||||||
const less = require('less');
|
const less = require('less');
|
||||||
|
|
||||||
const uglify = require('uglify-es');
|
const webpack = require('webpack');
|
||||||
const nconf = require('nconf');
|
const nconf = require('nconf');
|
||||||
|
|
||||||
const Benchpress = require('benchpressjs');
|
const Benchpress = require('benchpressjs');
|
||||||
@@ -46,16 +46,6 @@ winston.configure({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const web = module.exports;
|
const web = module.exports;
|
||||||
|
|
||||||
const scripts = [
|
|
||||||
'node_modules/jquery/dist/jquery.js',
|
|
||||||
'node_modules/xregexp/xregexp-all.js',
|
|
||||||
'public/src/modules/slugify.js',
|
|
||||||
'public/src/utils.js',
|
|
||||||
'public/src/installer/install.js',
|
|
||||||
'node_modules/zxcvbn/dist/zxcvbn.js',
|
|
||||||
];
|
|
||||||
|
|
||||||
let installing = false;
|
let installing = false;
|
||||||
let success = false;
|
let success = false;
|
||||||
let error = false;
|
let error = false;
|
||||||
@@ -68,6 +58,8 @@ web.install = async function (port) {
|
|||||||
winston.info(`Launching web installer on port ${port}`);
|
winston.info(`Launching web installer on port ${port}`);
|
||||||
|
|
||||||
app.use(express.static('public', {}));
|
app.use(express.static('public', {}));
|
||||||
|
app.use('/assets', express.static(path.join(__dirname, '../build/webpack'), {}));
|
||||||
|
|
||||||
app.engine('tpl', (filepath, options, callback) => {
|
app.engine('tpl', (filepath, options, callback) => {
|
||||||
filepath = filepath.replace(/\.tpl$/, '.js');
|
filepath = filepath.replace(/\.tpl$/, '.js');
|
||||||
|
|
||||||
@@ -82,7 +74,7 @@ web.install = async function (port) {
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
compileTemplate(),
|
compileTemplate(),
|
||||||
compileLess(),
|
compileLess(),
|
||||||
compileJS(),
|
runWebpack(),
|
||||||
copyCSS(),
|
copyCSS(),
|
||||||
loadDefaults(),
|
loadDefaults(),
|
||||||
]);
|
]);
|
||||||
@@ -93,6 +85,13 @@ web.install = async function (port) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function runWebpack() {
|
||||||
|
const util = require('util');
|
||||||
|
const webpackCfg = require('../webpack.installer');
|
||||||
|
const compiler = webpack(webpackCfg);
|
||||||
|
const webpackRun = util.promisify(compiler.run).bind(compiler);
|
||||||
|
await webpackRun();
|
||||||
|
}
|
||||||
|
|
||||||
function launchExpress(port) {
|
function launchExpress(port) {
|
||||||
server = app.listen(port, () => {
|
server = app.listen(port, () => {
|
||||||
@@ -256,23 +255,6 @@ async function compileLess() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function compileJS() {
|
|
||||||
let code = '';
|
|
||||||
|
|
||||||
for (const srcPath of scripts) {
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
const buffer = await fs.promises.readFile(path.join(__dirname, '..', srcPath));
|
|
||||||
code += buffer.toString();
|
|
||||||
}
|
|
||||||
const minified = uglify.minify(code, {
|
|
||||||
compress: false,
|
|
||||||
});
|
|
||||||
if (!minified.code) {
|
|
||||||
throw new Error('[[error:failed-to-minify]]');
|
|
||||||
}
|
|
||||||
await fs.promises.writeFile(path.join(__dirname, '../public/installer.min.js'), minified.code);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function copyCSS() {
|
async function copyCSS() {
|
||||||
const src = await fs.promises.readFile(
|
const src = await fs.promises.readFile(
|
||||||
path.join(__dirname, '../node_modules/bootstrap/dist/css/bootstrap.min.css'), 'utf8'
|
path.join(__dirname, '../node_modules/bootstrap/dist/css/bootstrap.min.css'), 'utf8'
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
require('../app');
|
||||||
|
|
||||||
|
// scripts-admin.min contains javascript files
|
||||||
|
// from plugins that add files to "acpScripts" block in plugin.json
|
||||||
|
// eslint-disable-next-line import/no-unresolved
|
||||||
|
require('../../scripts-admin.min');
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
let logoutTimer = 0;
|
let logoutTimer = 0;
|
||||||
let logoutMessage;
|
let logoutMessage;
|
||||||
@@ -36,6 +43,12 @@
|
|||||||
hooks.on('action:ajaxify.end', () => {
|
hooks.on('action:ajaxify.end', () => {
|
||||||
showCorrectNavTab();
|
showCorrectNavTab();
|
||||||
startLogoutTimer();
|
startLogoutTimer();
|
||||||
|
if ($('.settings').length) {
|
||||||
|
require(['admin/settings'], function (Settings) {
|
||||||
|
Settings.prepare();
|
||||||
|
Settings.populateTOC();
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -67,7 +80,7 @@
|
|||||||
$(window).on('action:ajaxify.contentLoaded', function (ev, data) {
|
$(window).on('action:ajaxify.contentLoaded', function (ev, data) {
|
||||||
selectMenuItem(data.url);
|
selectMenuItem(data.url);
|
||||||
setupRestartLinks();
|
setupRestartLinks();
|
||||||
|
require('material-design-lite');
|
||||||
componentHandler.upgradeDom();
|
componentHandler.upgradeDom();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -227,10 +240,4 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// tell ace to use the right paths when requiring modules
|
|
||||||
require(['ace/ace'], function (ace) {
|
|
||||||
ace.config.set('packaged', true);
|
|
||||||
ace.config.set('basePath', config.relative_path + '/assets/src/modules/ace/');
|
|
||||||
});
|
|
||||||
}());
|
}());
|
||||||
|
|||||||
@@ -9,36 +9,9 @@ define('admin/appearance/customise', ['admin/settings', 'ace/ace'], function (Se
|
|||||||
$('#customJS').text($('#customJS-holder').val());
|
$('#customJS').text($('#customJS-holder').val());
|
||||||
$('#customHTML').text($('#customHTML-holder').val());
|
$('#customHTML').text($('#customHTML-holder').val());
|
||||||
|
|
||||||
const customCSS = ace.edit('customCSS');
|
initACE('customCSS', 'less', '#customCSS-holder');
|
||||||
const customJS = ace.edit('customJS');
|
initACE('customJS', 'javascript', '#customJS-holder');
|
||||||
const customHTML = ace.edit('customHTML');
|
initACE('customHTML', 'html', '#customHTML-holder');
|
||||||
|
|
||||||
customCSS.setTheme('ace/theme/twilight');
|
|
||||||
customCSS.getSession().setMode('ace/mode/less');
|
|
||||||
|
|
||||||
customCSS.on('change', function () {
|
|
||||||
app.flags = app.flags || {};
|
|
||||||
app.flags._unsaved = true;
|
|
||||||
$('#customCSS-holder').val(customCSS.getValue());
|
|
||||||
});
|
|
||||||
|
|
||||||
customJS.setTheme('ace/theme/twilight');
|
|
||||||
customJS.getSession().setMode('ace/mode/javascript');
|
|
||||||
|
|
||||||
customJS.on('change', function () {
|
|
||||||
app.flags = app.flags || {};
|
|
||||||
app.flags._unsaved = true;
|
|
||||||
$('#customJS-holder').val(customJS.getValue());
|
|
||||||
});
|
|
||||||
|
|
||||||
customHTML.setTheme('ace/theme/twilight');
|
|
||||||
customHTML.getSession().setMode('ace/mode/html');
|
|
||||||
|
|
||||||
customHTML.on('change', function () {
|
|
||||||
app.flags = app.flags || {};
|
|
||||||
app.flags._unsaved = true;
|
|
||||||
$('#customHTML-holder').val(customHTML.getValue());
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#save').on('click', function () {
|
$('#save').on('click', function () {
|
||||||
if ($('#enableLiveReload').is(':checked')) {
|
if ($('#enableLiveReload').is(':checked')) {
|
||||||
@@ -48,5 +21,20 @@ define('admin/appearance/customise', ['admin/settings', 'ace/ace'], function (Se
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function initACE(aceElementId, mode, holder) {
|
||||||
|
var editorEl = ace.edit(aceElementId, {
|
||||||
|
mode: 'ace/mode/' + mode,
|
||||||
|
theme: 'ace/theme/twilight',
|
||||||
|
maxLines: 30,
|
||||||
|
minLines: 30,
|
||||||
|
fontSize: 14,
|
||||||
|
});
|
||||||
|
editorEl.on('change', function () {
|
||||||
|
app.flags = app.flags || {};
|
||||||
|
app.flags._unsaved = true;
|
||||||
|
$(holder).val(editorEl.getValue());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return Customise;
|
return Customise;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ define('admin/manage/categories', [
|
|||||||
'bootbox',
|
'bootbox',
|
||||||
'alerts',
|
'alerts',
|
||||||
], function (translator, Benchpress, categorySelector, api, Sortable, bootbox, alerts) {
|
], function (translator, Benchpress, categorySelector, api, Sortable, bootbox, alerts) {
|
||||||
|
Sortable = Sortable.default;
|
||||||
const Categories = {};
|
const Categories = {};
|
||||||
let newCategoryId = -1;
|
let newCategoryId = -1;
|
||||||
let sortables;
|
let sortables;
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const hooks = require('./modules/hooks');
|
||||||
|
const { render } = require('./widgets');
|
||||||
|
|
||||||
ajaxify = window.ajaxify || {};
|
window.ajaxify = window.ajaxify || {};
|
||||||
|
ajaxify.widgets = { render: render };
|
||||||
(function () {
|
(function () {
|
||||||
let apiXHR = null;
|
let apiXHR = null;
|
||||||
let ajaxifyTimer;
|
let ajaxifyTimer;
|
||||||
@@ -13,11 +15,6 @@ ajaxify = window.ajaxify || {};
|
|||||||
ajaxify.count = 0;
|
ajaxify.count = 0;
|
||||||
ajaxify.currentPage = null;
|
ajaxify.currentPage = null;
|
||||||
|
|
||||||
let hooks;
|
|
||||||
require(['hooks'], function (_hooks) {
|
|
||||||
hooks = _hooks;
|
|
||||||
});
|
|
||||||
|
|
||||||
ajaxify.go = function (url, callback, quiet) {
|
ajaxify.go = function (url, callback, quiet) {
|
||||||
// Automatically reconnect to socket and re-ajaxify on success
|
// Automatically reconnect to socket and re-ajaxify on success
|
||||||
if (!socket.connected) {
|
if (!socket.connected) {
|
||||||
@@ -329,7 +326,6 @@ ajaxify = window.ajaxify || {};
|
|||||||
|
|
||||||
ajaxify.loadScript = function (tpl_url, callback) {
|
ajaxify.loadScript = function (tpl_url, callback) {
|
||||||
let location = !app.inAdmin ? 'forum/' : '';
|
let location = !app.inAdmin ? 'forum/' : '';
|
||||||
|
|
||||||
if (tpl_url.startsWith('admin')) {
|
if (tpl_url.startsWith('admin')) {
|
||||||
location = '';
|
location = '';
|
||||||
}
|
}
|
||||||
@@ -352,18 +348,14 @@ ajaxify = window.ajaxify || {};
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (typeof script === 'string') {
|
if (typeof script === 'string') {
|
||||||
return function (next) {
|
return async function (next) {
|
||||||
require([script], function (module) {
|
const module = await app.require(script);
|
||||||
// Hint: useful if you want to override a loaded library (e.g. replace core client-side logic),
|
// Hint: useful if you want to override a loaded library (e.g. replace core client-side logic),
|
||||||
// or call a method other than .init()
|
// or call a method other than .init()
|
||||||
hooks.fire('static:script.init', { tpl_url, name: script, module }).then(() => {
|
hooks.fire('static:script.init', { tpl_url, name: script, module }).then(() => {
|
||||||
if (module && module.init) {
|
if (module && module.init) {
|
||||||
module.init();
|
module.init();
|
||||||
}
|
}
|
||||||
next();
|
|
||||||
});
|
|
||||||
}, function () {
|
|
||||||
// ignore 404 error
|
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -434,9 +426,24 @@ ajaxify = window.ajaxify || {};
|
|||||||
};
|
};
|
||||||
|
|
||||||
ajaxify.loadTemplate = function (template, callback) {
|
ajaxify.loadTemplate = function (template, callback) {
|
||||||
require([config.asset_base_url + '/templates/' + template + '.js'], callback, function (err) {
|
$.ajax({
|
||||||
|
url: `${config.asset_base_url}/templates/${template}.js`,
|
||||||
|
dataType: 'text',
|
||||||
|
success: function (script) {
|
||||||
|
var context = {
|
||||||
|
module: {
|
||||||
|
exports: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-new-func
|
||||||
|
const renderFunction = new Function('module', script);
|
||||||
|
renderFunction(context.module);
|
||||||
|
callback(context.module.exports);
|
||||||
|
},
|
||||||
|
}).fail(function () {
|
||||||
console.error('Unable to load template: ' + template);
|
console.error('Unable to load template: ' + template);
|
||||||
throw err;
|
callback(new Error('[[error:unable-to-load-template]]'));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -446,15 +453,11 @@ ajaxify = window.ajaxify || {};
|
|||||||
translator.translate(`[[global:reconnecting-message, ${config.siteTitle}]]`);
|
translator.translate(`[[global:reconnecting-message, ${config.siteTitle}]]`);
|
||||||
Benchpress.registerLoader(ajaxify.loadTemplate);
|
Benchpress.registerLoader(ajaxify.loadTemplate);
|
||||||
Benchpress.setGlobal('config', config);
|
Benchpress.setGlobal('config', config);
|
||||||
|
Benchpress.render('500', {}); // loads and caches the 500.tpl
|
||||||
});
|
});
|
||||||
}());
|
}());
|
||||||
|
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
let hooks;
|
|
||||||
require(['hooks'], function (_hooks) {
|
|
||||||
hooks = _hooks;
|
|
||||||
});
|
|
||||||
|
|
||||||
$(window).on('popstate', function (ev) {
|
$(window).on('popstate', function (ev) {
|
||||||
ev = ev.originalEvent;
|
ev = ev.originalEvent;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,22 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
window.$ = require('jquery');
|
||||||
|
|
||||||
|
window.jQuery = window.$;
|
||||||
|
require('bootstrap');
|
||||||
|
window.bootbox = require('bootbox');
|
||||||
|
require('jquery-form');
|
||||||
|
window.utils = require('./utils');
|
||||||
|
require('timeago');
|
||||||
|
|
||||||
|
const Visibility = require('visibilityjs');
|
||||||
|
const Benchpress = require('benchpressjs');
|
||||||
|
|
||||||
|
Benchpress.setGlobal('config', config);
|
||||||
|
|
||||||
|
require('./sockets');
|
||||||
|
require('./overrides');
|
||||||
|
require('./ajaxify');
|
||||||
|
|
||||||
app = window.app || {};
|
app = window.app || {};
|
||||||
|
|
||||||
@@ -8,6 +25,7 @@ app.currentRoom = null;
|
|||||||
app.widgets = {};
|
app.widgets = {};
|
||||||
app.flags = {};
|
app.flags = {};
|
||||||
|
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
let appLoaded = false;
|
let appLoaded = false;
|
||||||
const isTouchDevice = utils.isTouchDevice();
|
const isTouchDevice = utils.isTouchDevice();
|
||||||
@@ -114,18 +132,29 @@ app.flags = {};
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
app.require = async (modules) => { // allows you to await require.js modules
|
app.require = async function (modules) {
|
||||||
const single = !Array.isArray(modules);
|
const single = !Array.isArray(modules);
|
||||||
if (single) {
|
if (single) {
|
||||||
modules = [modules];
|
modules = [modules];
|
||||||
}
|
}
|
||||||
|
async function requireModule(moduleName) {
|
||||||
return new Promise((resolve, reject) => {
|
let _module;
|
||||||
require(modules, (...exports) => {
|
try {
|
||||||
resolve(single ? exports.pop() : exports);
|
if (moduleName.startsWith('admin')) {
|
||||||
}, reject);
|
_module = await import(/* webpackChunkName: "admin/[request]" */ 'admin/' + moduleName.replace(/^admin\//, ''));
|
||||||
});
|
} else if (moduleName.startsWith('forum')) {
|
||||||
};
|
_module = await import(/* webpackChunkName: "forum/[request]" */ 'forum/' + moduleName.replace(/^forum\//, ''));
|
||||||
|
} else {
|
||||||
|
_module = await import(/* webpackChunkName: "modules/[request]" */ 'modules/' + moduleName);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`error loading ${moduleName}\n${err.stack}`);
|
||||||
|
}
|
||||||
|
return _module && _module.default;
|
||||||
|
}
|
||||||
|
const result = await Promise.all(modules.map(requireModule));
|
||||||
|
return single ? result.pop() : result;
|
||||||
|
}
|
||||||
|
|
||||||
app.logout = function (redirect) {
|
app.logout = function (redirect) {
|
||||||
console.warn('[deprecated] app.logout is deprecated, please use logout module directly');
|
console.warn('[deprecated] app.logout is deprecated, please use logout module directly');
|
||||||
|
|||||||
8
public/src/client.js
Normal file
8
public/src/client.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
require('./app');
|
||||||
|
|
||||||
|
// scripts-client.min contains javascript files
|
||||||
|
// from plugins that add files to "scripts" block in plugin.json
|
||||||
|
// eslint-disable-next-line import/no-unresolved
|
||||||
|
require('../scripts-client.min');
|
||||||
103
public/src/client/test.js
Normal file
103
public/src/client/test.js
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import 'jquery-ui/ui/widgets/datepicker';
|
||||||
|
import Sortable from 'sortablejs';
|
||||||
|
import semver from 'semver';
|
||||||
|
import * as autocomplete from 'autocomplete';
|
||||||
|
// we are using browser colorpicker
|
||||||
|
// import { enable as colorpickerEnable } from '../admin/modules/colorpicker';
|
||||||
|
import * as api from 'api';
|
||||||
|
import * as alerts from 'alerts';
|
||||||
|
|
||||||
|
export function init() {
|
||||||
|
console.log('should be true semver.gt("1.1.1", "1.0.0")', semver.gt('1.1.1', '1.0.0'));
|
||||||
|
$('#change-skin').val(config.bootswatchSkin);
|
||||||
|
|
||||||
|
$('#inputTags').tagsinput({
|
||||||
|
confirmKeys: [13, 44],
|
||||||
|
trimValue: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#inputBirthday').datepicker({
|
||||||
|
changeMonth: true,
|
||||||
|
changeYear: true,
|
||||||
|
yearRange: '1900:-5y',
|
||||||
|
defaultDate: '-13y',
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#change-language').on('click', function () {
|
||||||
|
config.userLang = 'tr';
|
||||||
|
var languageCode = utils.userLangToTimeagoCode(config.userLang);
|
||||||
|
import(/* webpackChunkName: "timeago/[request]" */ 'timeago/locales/jquery.timeago.' + languageCode).then(function () {
|
||||||
|
overrides.overrideTimeago();
|
||||||
|
ajaxify.refresh();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// colorpickerEnable($('#colorpicker'));
|
||||||
|
|
||||||
|
autocomplete.user($('#autocomplete'));
|
||||||
|
|
||||||
|
Sortable.create($('#sortable-list')[0], {});
|
||||||
|
|
||||||
|
var data = $('#form-serialize').serializeObject();
|
||||||
|
$('#json-form-data').text(JSON.stringify(data, null, 2));
|
||||||
|
|
||||||
|
$('#form-deserialize').deserialize({
|
||||||
|
foo: [1, 2],
|
||||||
|
moo: 'it works',
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#change-skin').change(async function () {
|
||||||
|
var newSkin = $(this).val();
|
||||||
|
api.put(`/users/${app.user.uid}/settings`, {
|
||||||
|
settings: {
|
||||||
|
postsPerPage: 20,
|
||||||
|
topicsPerPage: 20,
|
||||||
|
bootswatchSkin: newSkin,
|
||||||
|
}
|
||||||
|
}).then((newSettings) => {
|
||||||
|
config.bootswatchSkin = newSkin;
|
||||||
|
reskin(newSkin);
|
||||||
|
}).catch(alerts.error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// copied from account/settings
|
||||||
|
async function reskin(skinName) {
|
||||||
|
const clientEl = Array.prototype.filter.call(document.querySelectorAll('link[rel="stylesheet"]'), function (el) {
|
||||||
|
return el.href.indexOf(config.relative_path + '/assets/client') !== -1;
|
||||||
|
})[0] || null;
|
||||||
|
if (!clientEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentSkinClassName = $('body').attr('class').split(/\s+/).filter(function (className) {
|
||||||
|
return className.startsWith('skin-');
|
||||||
|
});
|
||||||
|
if (!currentSkinClassName[0]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let currentSkin = currentSkinClassName[0].slice(5);
|
||||||
|
currentSkin = currentSkin !== 'noskin' ? currentSkin : '';
|
||||||
|
|
||||||
|
// Stop execution if skin didn't change
|
||||||
|
if (skinName === currentSkin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const linkEl = document.createElement('link');
|
||||||
|
linkEl.rel = 'stylesheet';
|
||||||
|
linkEl.type = 'text/css';
|
||||||
|
linkEl.href = config.relative_path + '/assets/client' + (skinName ? '-' + skinName : '') + '.css';
|
||||||
|
linkEl.onload = function () {
|
||||||
|
clientEl.parentNode.removeChild(clientEl);
|
||||||
|
|
||||||
|
// Update body class with proper skin name
|
||||||
|
$('body').removeClass(currentSkinClassName.join(' '));
|
||||||
|
$('body').addClass('skin-' + (skinName || 'noskin'));
|
||||||
|
};
|
||||||
|
|
||||||
|
document.head.appendChild(linkEl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const testPage = { init };
|
||||||
|
export default testPage;
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
|
/* eslint-disable no-redeclare */
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/* global zxcvbn, slugify */
|
const $ = require('jquery');
|
||||||
|
const zxcvbn = require('zxcvbn');
|
||||||
|
const utils = require('../utils');
|
||||||
|
const slugify = require('../modules/slugify');
|
||||||
|
|
||||||
$('document').ready(function () {
|
$('document').ready(function () {
|
||||||
setupInputs();
|
setupInputs();
|
||||||
|
|||||||
20
public/src/modules/ace-editor.js
Normal file
20
public/src/modules/ace-editor.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
export * from 'ace-builds';
|
||||||
|
|
||||||
|
// only import the modes and theme we use
|
||||||
|
import 'ace-builds/src-noconflict/mode-javascript';
|
||||||
|
import 'ace-builds/src-noconflict/mode-less';
|
||||||
|
import 'ace-builds/src-noconflict/mode-html';
|
||||||
|
import 'ace-builds/src-noconflict/ext-searchbox';
|
||||||
|
import 'ace-builds/src-noconflict/theme-twilight';
|
||||||
|
|
||||||
|
/* eslint-disable import/no-webpack-loader-syntax */
|
||||||
|
/* eslint-disable import/no-unresolved */
|
||||||
|
import htmlWorkerUrl from 'file-loader!ace-builds/src-noconflict/worker-html';
|
||||||
|
import javascriptWorkerUrl from 'file-loader!ace-builds/src-noconflict/worker-javascript';
|
||||||
|
import cssWorkerUrl from 'file-loader!ace-builds/src-noconflict/worker-css';
|
||||||
|
|
||||||
|
ace.config.setModuleUrl('ace/mode/html_worker', htmlWorkerUrl);
|
||||||
|
ace.config.setModuleUrl('ace/mode/javascript_worker', javascriptWorkerUrl);
|
||||||
|
ace.config.setModuleUrl('ace/mode/css_worker', cssWorkerUrl);
|
||||||
|
|
||||||
|
|
||||||
368
public/src/modules/helpers.common.js
Normal file
368
public/src/modules/helpers.common.js
Normal file
@@ -0,0 +1,368 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = function (utils, Benchpress, relative_path) {
|
||||||
|
Benchpress.setGlobal('true', true);
|
||||||
|
Benchpress.setGlobal('false', false);
|
||||||
|
|
||||||
|
const helpers = {
|
||||||
|
displayMenuItem,
|
||||||
|
buildMetaTag,
|
||||||
|
buildLinkTag,
|
||||||
|
stringify,
|
||||||
|
escape,
|
||||||
|
stripTags,
|
||||||
|
generateCategoryBackground,
|
||||||
|
generateChildrenCategories,
|
||||||
|
generateTopicClass,
|
||||||
|
membershipBtn,
|
||||||
|
spawnPrivilegeStates,
|
||||||
|
localeToHTML,
|
||||||
|
renderTopicImage,
|
||||||
|
renderTopicEvents,
|
||||||
|
renderEvents,
|
||||||
|
renderDigestAvatar,
|
||||||
|
userAgentIcons,
|
||||||
|
buildAvatar,
|
||||||
|
register,
|
||||||
|
__escape: identity,
|
||||||
|
};
|
||||||
|
|
||||||
|
function identity(str) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayMenuItem(data, index) {
|
||||||
|
const item = data.navigation[index];
|
||||||
|
if (!item) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.route.match('/users') && data.user && !data.user.privileges['view:users']) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.route.match('/tags') && data.user && !data.user.privileges['view:tags']) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.route.match('/groups') && data.user && !data.user.privileges['view:groups']) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMetaTag(tag) {
|
||||||
|
const name = tag.name ? 'name="' + tag.name + '" ' : '';
|
||||||
|
const property = tag.property ? 'property="' + tag.property + '" ' : '';
|
||||||
|
const content = tag.content ? 'content="' + tag.content.replace(/\n/g, ' ') + '" ' : '';
|
||||||
|
|
||||||
|
return '<meta ' + name + property + content + '/>\n\t';
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildLinkTag(tag) {
|
||||||
|
const attributes = ['link', 'rel', 'as', 'type', 'href', 'sizes', 'title', 'crossorigin'];
|
||||||
|
const [link, rel, as, type, href, sizes, title, crossorigin] = attributes.map(attr => (tag[attr] ? `${attr}="${tag[attr]}" ` : ''));
|
||||||
|
|
||||||
|
return '<link ' + link + rel + as + type + sizes + title + href + crossorigin + '/>\n\t';
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringify(obj) {
|
||||||
|
// Turns the incoming object into a JSON string
|
||||||
|
return JSON.stringify(obj).replace(/&/gm, '&').replace(/</gm, '<').replace(/>/gm, '>')
|
||||||
|
.replace(/"/g, '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
function escape(str) {
|
||||||
|
return utils.escapeHTML(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripTags(str) {
|
||||||
|
return utils.stripHTMLTags(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateCategoryBackground(category) {
|
||||||
|
if (!category) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const style = [];
|
||||||
|
|
||||||
|
if (category.bgColor) {
|
||||||
|
style.push('background-color: ' + category.bgColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (category.color) {
|
||||||
|
style.push('color: ' + category.color);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (category.backgroundImage) {
|
||||||
|
style.push('background-image: url(' + category.backgroundImage + ')');
|
||||||
|
if (category.imageClass) {
|
||||||
|
style.push('background-size: ' + category.imageClass);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return style.join('; ') + ';';
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateChildrenCategories(category) {
|
||||||
|
let html = '';
|
||||||
|
if (!category || !category.children || !category.children.length) {
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
category.children.forEach(function (child) {
|
||||||
|
if (child && !child.isSection) {
|
||||||
|
const link = child.link ? child.link : (relative_path + '/category/' + child.slug);
|
||||||
|
html += '<span class="category-children-item pull-left">' +
|
||||||
|
'<div role="presentation" class="icon pull-left" style="' + generateCategoryBackground(child) + '">' +
|
||||||
|
'<i class="fa fa-fw ' + child.icon + '"></i>' +
|
||||||
|
'</div>' +
|
||||||
|
'<a href="' + link + '"><small>' + child.name + '</small></a></span>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
html = html ? ('<span class="category-children">' + html + '</span>') : html;
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateTopicClass(topic) {
|
||||||
|
const style = [];
|
||||||
|
|
||||||
|
if (topic.locked) {
|
||||||
|
style.push('locked');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (topic.pinned) {
|
||||||
|
style.push('pinned');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (topic.deleted) {
|
||||||
|
style.push('deleted');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (topic.unread) {
|
||||||
|
style.push('unread');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (topic.scheduled) {
|
||||||
|
style.push('scheduled');
|
||||||
|
}
|
||||||
|
|
||||||
|
return style.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Groups helpers
|
||||||
|
function membershipBtn(groupObj) {
|
||||||
|
if (groupObj.isMember && groupObj.name !== 'administrators') {
|
||||||
|
return '<button class="btn btn-danger" data-action="leave" data-group="' + groupObj.displayName + '"' + (groupObj.disableLeave ? ' disabled' : '') + '><i class="fa fa-times"></i> [[groups:membership.leave-group]]</button>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groupObj.isPending && groupObj.name !== 'administrators') {
|
||||||
|
return '<button class="btn btn-warning disabled"><i class="fa fa-clock-o"></i> [[groups:membership.invitation-pending]]</button>';
|
||||||
|
} else if (groupObj.isInvited) {
|
||||||
|
return '<button class="btn btn-link" data-action="rejectInvite" data-group="' + groupObj.displayName + '">[[groups:membership.reject]]</button><button class="btn btn-success" data-action="acceptInvite" data-group="' + groupObj.name + '"><i class="fa fa-plus"></i> [[groups:membership.accept-invitation]]</button>';
|
||||||
|
} else if (!groupObj.disableJoinRequests && groupObj.name !== 'administrators') {
|
||||||
|
return '<button class="btn btn-success" data-action="join" data-group="' + groupObj.displayName + '"><i class="fa fa-plus"></i> [[groups:membership.join-group]]</button>';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function spawnPrivilegeStates(member, privileges) {
|
||||||
|
const states = [];
|
||||||
|
for (const priv in privileges) {
|
||||||
|
if (privileges.hasOwnProperty(priv)) {
|
||||||
|
states.push({
|
||||||
|
name: priv,
|
||||||
|
state: privileges[priv],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return states.map(function (priv) {
|
||||||
|
const guestDisabled = ['groups:moderate', 'groups:posts:upvote', 'groups:posts:downvote', 'groups:local:login', 'groups:group:create'];
|
||||||
|
const spidersEnabled = ['groups:find', 'groups:read', 'groups:topics:read', 'groups:view:users', 'groups:view:tags', 'groups:view:groups'];
|
||||||
|
const globalModDisabled = ['groups:moderate'];
|
||||||
|
const disabled =
|
||||||
|
(member === 'guests' && (guestDisabled.includes(priv.name) || priv.name.startsWith('groups:admin:'))) ||
|
||||||
|
(member === 'spiders' && !spidersEnabled.includes(priv.name)) ||
|
||||||
|
(member === 'Global Moderators' && globalModDisabled.includes(priv.name));
|
||||||
|
|
||||||
|
return '<td class="text-center" data-privilege="' + priv.name + '" data-value="' + priv.state + '"><input autocomplete="off" type="checkbox"' + (priv.state ? ' checked' : '') + (disabled ? ' disabled="disabled"' : '') + ' /></td>';
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function localeToHTML(locale, fallback) {
|
||||||
|
locale = locale || fallback || 'en-GB';
|
||||||
|
return locale.replace('_', '-');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTopicImage(topicObj) {
|
||||||
|
if (topicObj.thumb) {
|
||||||
|
return '<img src="' + topicObj.thumb + '" class="img-circle user-img" title="' + topicObj.user.username + '" />';
|
||||||
|
}
|
||||||
|
return '<img component="user/picture" data-uid="' + topicObj.user.uid + '" src="' + topicObj.user.picture + '" class="user-img" title="' + topicObj.user.username + '" />';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTopicEvents(index, sort) {
|
||||||
|
if (sort === 'most_votes') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const start = this.posts[index].eventStart;
|
||||||
|
const end = this.posts[index].eventEnd;
|
||||||
|
const events = this.events.filter(event => event.timestamp >= start && event.timestamp < end);
|
||||||
|
if (!events.length) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderEvents.call(this, events);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderEvents(events) {
|
||||||
|
return events.reduce((html, event) => {
|
||||||
|
html += `<li component="topic/event" class="timeline-event" data-topic-event-id="${event.id}">
|
||||||
|
<div class="timeline-badge">
|
||||||
|
<i class="fa ${event.icon || 'fa-circle'}"></i>
|
||||||
|
</div>
|
||||||
|
<span class="timeline-text">
|
||||||
|
${event.href ? `<a href="${relative_path}${event.href}">${event.text}</a>` : event.text}
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (event.user) {
|
||||||
|
if (!event.user.system) {
|
||||||
|
html += `<span><a href="${relative_path}/user/${event.user.userslug}">${buildAvatar(event.user, 'xs', true)} ${event.user.username}</a></span> `;
|
||||||
|
} else {
|
||||||
|
html += `<span class="timeline-text">[[global:system-user]]</span> `;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `<span class="timeago timeline-text" title="${event.timestampISO}"></span>`;
|
||||||
|
|
||||||
|
if (this.privileges.isAdminOrMod) {
|
||||||
|
html += ` <span component="topic/event/delete" data-topic-event-id="${event.id}" class="timeline-text pointer" title="[[topic:delete-event]]"><i class="fa fa-trash"></i></span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDigestAvatar(block) {
|
||||||
|
if (block.teaser) {
|
||||||
|
if (block.teaser.user.picture) {
|
||||||
|
return '<img style="vertical-align: middle; width: 32px; height: 32px; border-radius: 50%;" src="' + block.teaser.user.picture + '" title="' + block.teaser.user.username + '" />';
|
||||||
|
}
|
||||||
|
return '<div style="vertical-align: middle; width: 32px; height: 32px; line-height: 32px; font-size: 16px; background-color: ' + block.teaser.user['icon:bgColor'] + '; color: white; text-align: center; display: inline-block; border-radius: 50%;">' + block.teaser.user['icon:text'] + '</div>';
|
||||||
|
}
|
||||||
|
if (block.user.picture) {
|
||||||
|
return '<img style="vertical-align: middle; width: 32px; height: 32px; border-radius: 50%;" src="' + block.user.picture + '" title="' + block.user.username + '" />';
|
||||||
|
}
|
||||||
|
return '<div style="vertical-align: middle; width: 32px; height: 32px; line-height: 32px; font-size: 16px; background-color: ' + block.user['icon:bgColor'] + '; color: white; text-align: center; display: inline-block; border-radius: 50%;">' + block.user['icon:text'] + '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function userAgentIcons(data) {
|
||||||
|
let icons = '';
|
||||||
|
|
||||||
|
switch (data.platform) {
|
||||||
|
case 'Linux':
|
||||||
|
icons += '<i class="fa fa-fw fa-linux"></i>';
|
||||||
|
break;
|
||||||
|
case 'Microsoft Windows':
|
||||||
|
icons += '<i class="fa fa-fw fa-windows"></i>';
|
||||||
|
break;
|
||||||
|
case 'Apple Mac':
|
||||||
|
icons += '<i class="fa fa-fw fa-apple"></i>';
|
||||||
|
break;
|
||||||
|
case 'Android':
|
||||||
|
icons += '<i class="fa fa-fw fa-android"></i>';
|
||||||
|
break;
|
||||||
|
case 'iPad':
|
||||||
|
icons += '<i class="fa fa-fw fa-tablet"></i>';
|
||||||
|
break;
|
||||||
|
case 'iPod': // intentional fall-through
|
||||||
|
case 'iPhone':
|
||||||
|
icons += '<i class="fa fa-fw fa-mobile"></i>';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
icons += '<i class="fa fa-fw fa-question-circle"></i>';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (data.browser) {
|
||||||
|
case 'Chrome':
|
||||||
|
icons += '<i class="fa fa-fw fa-chrome"></i>';
|
||||||
|
break;
|
||||||
|
case 'Firefox':
|
||||||
|
icons += '<i class="fa fa-fw fa-firefox"></i>';
|
||||||
|
break;
|
||||||
|
case 'Safari':
|
||||||
|
icons += '<i class="fa fa-fw fa-safari"></i>';
|
||||||
|
break;
|
||||||
|
case 'IE':
|
||||||
|
icons += '<i class="fa fa-fw fa-internet-explorer"></i>';
|
||||||
|
break;
|
||||||
|
case 'Edge':
|
||||||
|
icons += '<i class="fa fa-fw fa-edge"></i>';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
icons += '<i class="fa fa-fw fa-question-circle"></i>';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return icons;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAvatar(userObj, size, rounded, classNames, component) {
|
||||||
|
/**
|
||||||
|
* userObj requires:
|
||||||
|
* - uid, picture, icon:bgColor, icon:text (getUserField w/ "picture" should return all 4), username
|
||||||
|
* size: one of "xs", "sm", "md", "lg", or "xl" (required), or an integer
|
||||||
|
* rounded: true or false (optional, default false)
|
||||||
|
* classNames: additional class names to prepend (optional, default none)
|
||||||
|
* component: overrides the default component (optional, default none)
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Try to use root context if passed-in userObj is undefined
|
||||||
|
if (!userObj) {
|
||||||
|
userObj = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
const attributes = [
|
||||||
|
'alt="' + userObj.username + '"',
|
||||||
|
'title="' + userObj.username + '"',
|
||||||
|
'data-uid="' + userObj.uid + '"',
|
||||||
|
'loading="lazy"',
|
||||||
|
];
|
||||||
|
const styles = [];
|
||||||
|
classNames = classNames || '';
|
||||||
|
|
||||||
|
// Validate sizes, handle integers, otherwise fall back to `avatar-sm`
|
||||||
|
if (['xs', 'sm', 'sm2x', 'md', 'lg', 'xl'].includes(size)) {
|
||||||
|
classNames += ' avatar-' + size;
|
||||||
|
} else if (!isNaN(parseInt(size, 10))) {
|
||||||
|
styles.push('width: ' + size + 'px;', 'height: ' + size + 'px;', 'line-height: ' + size + 'px;', 'font-size: ' + (parseInt(size, 10) / 16) + 'rem;');
|
||||||
|
} else {
|
||||||
|
classNames += ' avatar-sm';
|
||||||
|
}
|
||||||
|
attributes.unshift('class="avatar ' + classNames + (rounded ? ' avatar-rounded' : '') + '"');
|
||||||
|
|
||||||
|
// Component override
|
||||||
|
if (component) {
|
||||||
|
attributes.push('component="' + component + '"');
|
||||||
|
} else {
|
||||||
|
attributes.push('component="avatar/' + (userObj.picture ? 'picture' : 'icon') + '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userObj.picture) {
|
||||||
|
return '<img ' + attributes.join(' ') + ' src="' + userObj.picture + '" style="' + styles.join(' ') + '" />';
|
||||||
|
}
|
||||||
|
|
||||||
|
styles.push('background-color: ' + userObj['icon:bgColor'] + ';');
|
||||||
|
return '<span ' + attributes.join(' ') + ' style="' + styles.join(' ') + '">' + userObj['icon:text'] + '</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function register() {
|
||||||
|
Object.keys(helpers).forEach(function (helperName) {
|
||||||
|
Benchpress.registerHelper(helperName, helpers[helperName]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return helpers;
|
||||||
|
};
|
||||||
@@ -1,377 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
(function (factory) {
|
const factory = require('./helpers.common');
|
||||||
if (typeof module === 'object' && module.exports) {
|
|
||||||
const relative_path = require('nconf').get('relative_path');
|
|
||||||
module.exports = factory(require('../utils'), require('benchpressjs'), relative_path);
|
|
||||||
} else if (typeof define === 'function' && define.amd) {
|
|
||||||
define('helpers', ['benchpress'], function (Benchpress) {
|
|
||||||
return factory(utils, Benchpress, config.relative_path);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}(function (utils, Benchpress, relative_path) {
|
|
||||||
Benchpress.setGlobal('true', true);
|
|
||||||
Benchpress.setGlobal('false', false);
|
|
||||||
|
|
||||||
const helpers = {
|
define('helpers', ['utils', 'benchpressjs'], function (utils, Benchpressjs) {
|
||||||
displayMenuItem,
|
return factory(utils, Benchpressjs, config.relative_path);
|
||||||
buildMetaTag,
|
});
|
||||||
buildLinkTag,
|
|
||||||
stringify,
|
|
||||||
escape,
|
|
||||||
stripTags,
|
|
||||||
generateCategoryBackground,
|
|
||||||
generateChildrenCategories,
|
|
||||||
generateTopicClass,
|
|
||||||
membershipBtn,
|
|
||||||
spawnPrivilegeStates,
|
|
||||||
localeToHTML,
|
|
||||||
renderTopicImage,
|
|
||||||
renderTopicEvents,
|
|
||||||
renderEvents,
|
|
||||||
renderDigestAvatar,
|
|
||||||
userAgentIcons,
|
|
||||||
buildAvatar,
|
|
||||||
register,
|
|
||||||
__escape: identity,
|
|
||||||
};
|
|
||||||
|
|
||||||
function identity(str) {
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
function displayMenuItem(data, index) {
|
|
||||||
const item = data.navigation[index];
|
|
||||||
if (!item) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.route.match('/users') && data.user && !data.user.privileges['view:users']) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.route.match('/tags') && data.user && !data.user.privileges['view:tags']) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.route.match('/groups') && data.user && !data.user.privileges['view:groups']) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildMetaTag(tag) {
|
|
||||||
const name = tag.name ? 'name="' + tag.name + '" ' : '';
|
|
||||||
const property = tag.property ? 'property="' + tag.property + '" ' : '';
|
|
||||||
const content = tag.content ? 'content="' + tag.content.replace(/\n/g, ' ') + '" ' : '';
|
|
||||||
|
|
||||||
return '<meta ' + name + property + content + '/>\n\t';
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildLinkTag(tag) {
|
|
||||||
const attributes = ['link', 'rel', 'as', 'type', 'href', 'sizes', 'title', 'crossorigin'];
|
|
||||||
const [link, rel, as, type, href, sizes, title, crossorigin] = attributes.map(attr => (tag[attr] ? `${attr}="${tag[attr]}" ` : ''));
|
|
||||||
|
|
||||||
return '<link ' + link + rel + as + type + sizes + title + href + crossorigin + '/>\n\t';
|
|
||||||
}
|
|
||||||
|
|
||||||
function stringify(obj) {
|
|
||||||
// Turns the incoming object into a JSON string
|
|
||||||
return JSON.stringify(obj).replace(/&/gm, '&').replace(/</gm, '<').replace(/>/gm, '>')
|
|
||||||
.replace(/"/g, '"');
|
|
||||||
}
|
|
||||||
|
|
||||||
function escape(str) {
|
|
||||||
return utils.escapeHTML(str);
|
|
||||||
}
|
|
||||||
|
|
||||||
function stripTags(str) {
|
|
||||||
return utils.stripHTMLTags(str);
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateCategoryBackground(category) {
|
|
||||||
if (!category) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
const style = [];
|
|
||||||
|
|
||||||
if (category.bgColor) {
|
|
||||||
style.push('background-color: ' + category.bgColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (category.color) {
|
|
||||||
style.push('color: ' + category.color);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (category.backgroundImage) {
|
|
||||||
style.push('background-image: url(' + category.backgroundImage + ')');
|
|
||||||
if (category.imageClass) {
|
|
||||||
style.push('background-size: ' + category.imageClass);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return style.join('; ') + ';';
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateChildrenCategories(category) {
|
|
||||||
let html = '';
|
|
||||||
if (!category || !category.children || !category.children.length) {
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
category.children.forEach(function (child) {
|
|
||||||
if (child && !child.isSection) {
|
|
||||||
const link = child.link ? child.link : (relative_path + '/category/' + child.slug);
|
|
||||||
html += '<span class="category-children-item pull-left">' +
|
|
||||||
'<div role="presentation" class="icon pull-left" style="' + generateCategoryBackground(child) + '">' +
|
|
||||||
'<i class="fa fa-fw ' + child.icon + '"></i>' +
|
|
||||||
'</div>' +
|
|
||||||
'<a href="' + link + '"><small>' + child.name + '</small></a></span>';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
html = html ? ('<span class="category-children">' + html + '</span>') : html;
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateTopicClass(topic) {
|
|
||||||
const style = [];
|
|
||||||
|
|
||||||
if (topic.locked) {
|
|
||||||
style.push('locked');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (topic.pinned) {
|
|
||||||
style.push('pinned');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (topic.deleted) {
|
|
||||||
style.push('deleted');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (topic.unread) {
|
|
||||||
style.push('unread');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (topic.scheduled) {
|
|
||||||
style.push('scheduled');
|
|
||||||
}
|
|
||||||
|
|
||||||
return style.join(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Groups helpers
|
|
||||||
function membershipBtn(groupObj) {
|
|
||||||
if (groupObj.isMember && groupObj.name !== 'administrators') {
|
|
||||||
return '<button class="btn btn-danger" data-action="leave" data-group="' + groupObj.displayName + '"' + (groupObj.disableLeave ? ' disabled' : '') + '><i class="fa fa-times"></i> [[groups:membership.leave-group]]</button>';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (groupObj.isPending && groupObj.name !== 'administrators') {
|
|
||||||
return '<button class="btn btn-warning disabled"><i class="fa fa-clock-o"></i> [[groups:membership.invitation-pending]]</button>';
|
|
||||||
} else if (groupObj.isInvited) {
|
|
||||||
return '<button class="btn btn-link" data-action="rejectInvite" data-group="' + groupObj.displayName + '">[[groups:membership.reject]]</button><button class="btn btn-success" data-action="acceptInvite" data-group="' + groupObj.name + '"><i class="fa fa-plus"></i> [[groups:membership.accept-invitation]]</button>';
|
|
||||||
} else if (!groupObj.disableJoinRequests && groupObj.name !== 'administrators') {
|
|
||||||
return '<button class="btn btn-success" data-action="join" data-group="' + groupObj.displayName + '"><i class="fa fa-plus"></i> [[groups:membership.join-group]]</button>';
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function spawnPrivilegeStates(member, privileges) {
|
|
||||||
const states = [];
|
|
||||||
for (const priv in privileges) {
|
|
||||||
if (privileges.hasOwnProperty(priv)) {
|
|
||||||
states.push({
|
|
||||||
name: priv,
|
|
||||||
state: privileges[priv],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return states.map(function (priv) {
|
|
||||||
const guestDisabled = ['groups:moderate', 'groups:posts:upvote', 'groups:posts:downvote', 'groups:local:login', 'groups:group:create'];
|
|
||||||
const spidersEnabled = ['groups:find', 'groups:read', 'groups:topics:read', 'groups:view:users', 'groups:view:tags', 'groups:view:groups'];
|
|
||||||
const globalModDisabled = ['groups:moderate'];
|
|
||||||
const disabled =
|
|
||||||
(member === 'guests' && (guestDisabled.includes(priv.name) || priv.name.startsWith('groups:admin:'))) ||
|
|
||||||
(member === 'spiders' && !spidersEnabled.includes(priv.name)) ||
|
|
||||||
(member === 'Global Moderators' && globalModDisabled.includes(priv.name));
|
|
||||||
|
|
||||||
return '<td class="text-center" data-privilege="' + priv.name + '" data-value="' + priv.state + '"><input autocomplete="off" type="checkbox"' + (priv.state ? ' checked' : '') + (disabled ? ' disabled="disabled"' : '') + ' /></td>';
|
|
||||||
}).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
function localeToHTML(locale, fallback) {
|
|
||||||
locale = locale || fallback || 'en-GB';
|
|
||||||
return locale.replace('_', '-');
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderTopicImage(topicObj) {
|
|
||||||
if (topicObj.thumb) {
|
|
||||||
return '<img src="' + topicObj.thumb + '" class="img-circle user-img" title="' + topicObj.user.username + '" />';
|
|
||||||
}
|
|
||||||
return '<img component="user/picture" data-uid="' + topicObj.user.uid + '" src="' + topicObj.user.picture + '" class="user-img" title="' + topicObj.user.username + '" />';
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderTopicEvents(index, sort) {
|
|
||||||
if (sort === 'most_votes') {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
const start = this.posts[index].eventStart;
|
|
||||||
const end = this.posts[index].eventEnd;
|
|
||||||
const events = this.events.filter(event => event.timestamp >= start && event.timestamp < end);
|
|
||||||
if (!events.length) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return renderEvents.call(this, events);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderEvents(events) {
|
|
||||||
return events.reduce((html, event) => {
|
|
||||||
html += `<li component="topic/event" class="timeline-event" data-topic-event-id="${event.id}">
|
|
||||||
<div class="timeline-badge">
|
|
||||||
<i class="fa ${event.icon || 'fa-circle'}"></i>
|
|
||||||
</div>
|
|
||||||
<span class="timeline-text">
|
|
||||||
${event.href ? `<a href="${relative_path}${event.href}">${event.text}</a>` : event.text}
|
|
||||||
</span>
|
|
||||||
`;
|
|
||||||
|
|
||||||
if (event.user) {
|
|
||||||
if (!event.user.system) {
|
|
||||||
html += `<span><a href="${relative_path}/user/${event.user.userslug}">${buildAvatar(event.user, 'xs', true)} ${event.user.username}</a></span> `;
|
|
||||||
} else {
|
|
||||||
html += `<span class="timeline-text">[[global:system-user]]</span> `;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
html += `<span class="timeago timeline-text" title="${event.timestampISO}"></span>`;
|
|
||||||
|
|
||||||
if (this.privileges.isAdminOrMod) {
|
|
||||||
html += ` <span component="topic/event/delete" data-topic-event-id="${event.id}" class="timeline-text pointer" title="[[topic:delete-event]]"><i class="fa fa-trash"></i></span>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return html;
|
|
||||||
}, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderDigestAvatar(block) {
|
|
||||||
if (block.teaser) {
|
|
||||||
if (block.teaser.user.picture) {
|
|
||||||
return '<img style="vertical-align: middle; width: 32px; height: 32px; border-radius: 50%;" src="' + block.teaser.user.picture + '" title="' + block.teaser.user.username + '" />';
|
|
||||||
}
|
|
||||||
return '<div style="vertical-align: middle; width: 32px; height: 32px; line-height: 32px; font-size: 16px; background-color: ' + block.teaser.user['icon:bgColor'] + '; color: white; text-align: center; display: inline-block; border-radius: 50%;">' + block.teaser.user['icon:text'] + '</div>';
|
|
||||||
}
|
|
||||||
if (block.user.picture) {
|
|
||||||
return '<img style="vertical-align: middle; width: 32px; height: 32px; border-radius: 50%;" src="' + block.user.picture + '" title="' + block.user.username + '" />';
|
|
||||||
}
|
|
||||||
return '<div style="vertical-align: middle; width: 32px; height: 32px; line-height: 32px; font-size: 16px; background-color: ' + block.user['icon:bgColor'] + '; color: white; text-align: center; display: inline-block; border-radius: 50%;">' + block.user['icon:text'] + '</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
function userAgentIcons(data) {
|
|
||||||
let icons = '';
|
|
||||||
|
|
||||||
switch (data.platform) {
|
|
||||||
case 'Linux':
|
|
||||||
icons += '<i class="fa fa-fw fa-linux"></i>';
|
|
||||||
break;
|
|
||||||
case 'Microsoft Windows':
|
|
||||||
icons += '<i class="fa fa-fw fa-windows"></i>';
|
|
||||||
break;
|
|
||||||
case 'Apple Mac':
|
|
||||||
icons += '<i class="fa fa-fw fa-apple"></i>';
|
|
||||||
break;
|
|
||||||
case 'Android':
|
|
||||||
icons += '<i class="fa fa-fw fa-android"></i>';
|
|
||||||
break;
|
|
||||||
case 'iPad':
|
|
||||||
icons += '<i class="fa fa-fw fa-tablet"></i>';
|
|
||||||
break;
|
|
||||||
case 'iPod': // intentional fall-through
|
|
||||||
case 'iPhone':
|
|
||||||
icons += '<i class="fa fa-fw fa-mobile"></i>';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
icons += '<i class="fa fa-fw fa-question-circle"></i>';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (data.browser) {
|
|
||||||
case 'Chrome':
|
|
||||||
icons += '<i class="fa fa-fw fa-chrome"></i>';
|
|
||||||
break;
|
|
||||||
case 'Firefox':
|
|
||||||
icons += '<i class="fa fa-fw fa-firefox"></i>';
|
|
||||||
break;
|
|
||||||
case 'Safari':
|
|
||||||
icons += '<i class="fa fa-fw fa-safari"></i>';
|
|
||||||
break;
|
|
||||||
case 'IE':
|
|
||||||
icons += '<i class="fa fa-fw fa-internet-explorer"></i>';
|
|
||||||
break;
|
|
||||||
case 'Edge':
|
|
||||||
icons += '<i class="fa fa-fw fa-edge"></i>';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
icons += '<i class="fa fa-fw fa-question-circle"></i>';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return icons;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildAvatar(userObj, size, rounded, classNames, component) {
|
|
||||||
/**
|
|
||||||
* userObj requires:
|
|
||||||
* - uid, picture, icon:bgColor, icon:text (getUserField w/ "picture" should return all 4), username
|
|
||||||
* size: one of "xs", "sm", "md", "lg", or "xl" (required), or an integer
|
|
||||||
* rounded: true or false (optional, default false)
|
|
||||||
* classNames: additional class names to prepend (optional, default none)
|
|
||||||
* component: overrides the default component (optional, default none)
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Try to use root context if passed-in userObj is undefined
|
|
||||||
if (!userObj) {
|
|
||||||
userObj = this;
|
|
||||||
}
|
|
||||||
|
|
||||||
const attributes = [
|
|
||||||
'alt="' + userObj.username + '"',
|
|
||||||
'title="' + userObj.username + '"',
|
|
||||||
'data-uid="' + userObj.uid + '"',
|
|
||||||
'loading="lazy"',
|
|
||||||
];
|
|
||||||
const styles = [];
|
|
||||||
classNames = classNames || '';
|
|
||||||
|
|
||||||
// Validate sizes, handle integers, otherwise fall back to `avatar-sm`
|
|
||||||
if (['xs', 'sm', 'sm2x', 'md', 'lg', 'xl'].includes(size)) {
|
|
||||||
classNames += ' avatar-' + size;
|
|
||||||
} else if (!isNaN(parseInt(size, 10))) {
|
|
||||||
styles.push('width: ' + size + 'px;', 'height: ' + size + 'px;', 'line-height: ' + size + 'px;', 'font-size: ' + (parseInt(size, 10) / 16) + 'rem;');
|
|
||||||
} else {
|
|
||||||
classNames += ' avatar-sm';
|
|
||||||
}
|
|
||||||
attributes.unshift('class="avatar ' + classNames + (rounded ? ' avatar-rounded' : '') + '"');
|
|
||||||
|
|
||||||
// Component override
|
|
||||||
if (component) {
|
|
||||||
attributes.push('component="' + component + '"');
|
|
||||||
} else {
|
|
||||||
attributes.push('component="avatar/' + (userObj.picture ? 'picture' : 'icon') + '"');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userObj.picture) {
|
|
||||||
return '<img ' + attributes.join(' ') + ' src="' + userObj.picture + '" style="' + styles.join(' ') + '" />';
|
|
||||||
}
|
|
||||||
|
|
||||||
styles.push('background-color: ' + userObj['icon:bgColor'] + ';');
|
|
||||||
return '<span ' + attributes.join(' ') + ' style="' + styles.join(' ') + '">' + userObj['icon:text'] + '</span>';
|
|
||||||
}
|
|
||||||
|
|
||||||
function register() {
|
|
||||||
Object.keys(helpers).forEach(function (helperName) {
|
|
||||||
Benchpress.registerHelper(helperName, helpers[helperName]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return helpers;
|
|
||||||
}));
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
define('pictureCropper', ['cropper', 'alerts'], function (Cropper, alerts) {
|
define('pictureCropper', ['alerts'], function (alerts) {
|
||||||
const module = {};
|
const module = {};
|
||||||
|
|
||||||
module.show = function (data, callback) {
|
module.show = function (data, callback) {
|
||||||
@@ -36,7 +36,7 @@ define('pictureCropper', ['cropper', 'alerts'], function (Cropper, alerts) {
|
|||||||
$('#crop-picture-modal').remove();
|
$('#crop-picture-modal').remove();
|
||||||
app.parseAndTranslate('modals/crop_picture', {
|
app.parseAndTranslate('modals/crop_picture', {
|
||||||
url: utils.escapeHTML(data.url),
|
url: utils.escapeHTML(data.url),
|
||||||
}, function (cropperModal) {
|
}, async function (cropperModal) {
|
||||||
cropperModal.modal({
|
cropperModal.modal({
|
||||||
backdrop: 'static',
|
backdrop: 'static',
|
||||||
}).modal('show');
|
}).modal('show');
|
||||||
@@ -45,6 +45,7 @@ define('pictureCropper', ['cropper', 'alerts'], function (Cropper, alerts) {
|
|||||||
const cropBoxHeight = parseInt($(window).height() / 2, 10);
|
const cropBoxHeight = parseInt($(window).height() / 2, 10);
|
||||||
const img = document.getElementById('cropped-image');
|
const img = document.getElementById('cropped-image');
|
||||||
$(img).css('max-height', cropBoxHeight);
|
$(img).css('max-height', cropBoxHeight);
|
||||||
|
const Cropper = (await import(/* webpackChunkName: "cropperjs" */ 'cropperjs')).default;
|
||||||
|
|
||||||
let cropperTool = new Cropper(img, {
|
let cropperTool = new Cropper(img, {
|
||||||
aspectRatio: data.aspectRatio,
|
aspectRatio: data.aspectRatio,
|
||||||
@@ -126,10 +127,11 @@ define('pictureCropper', ['cropper', 'alerts'], function (Cropper, alerts) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
cropperModal.find('.upload-btn').on('click', function () {
|
|
||||||
|
cropperModal.find('.upload-btn').on('click', async function () {
|
||||||
$(this).addClass('disabled');
|
$(this).addClass('disabled');
|
||||||
cropperTool.destroy();
|
cropperTool.destroy();
|
||||||
|
const Cropper = (await import(/* webpackChunkName: "cropperjs" */ 'cropperjs')).default;
|
||||||
cropperTool = new Cropper(img, {
|
cropperTool = new Cropper(img, {
|
||||||
viewMode: 1,
|
viewMode: 1,
|
||||||
autoCropArea: 1,
|
autoCropArea: 1,
|
||||||
|
|||||||
@@ -2,17 +2,6 @@
|
|||||||
|
|
||||||
|
|
||||||
define('settings', ['hooks', 'alerts'], function (hooks, alerts) {
|
define('settings', ['hooks', 'alerts'], function (hooks, alerts) {
|
||||||
const DEFAULT_PLUGINS = [
|
|
||||||
'settings/checkbox',
|
|
||||||
'settings/number',
|
|
||||||
'settings/textarea',
|
|
||||||
'settings/select',
|
|
||||||
'settings/array',
|
|
||||||
'settings/key',
|
|
||||||
'settings/object',
|
|
||||||
'settings/sorted-list',
|
|
||||||
];
|
|
||||||
|
|
||||||
// eslint-disable-next-line prefer-const
|
// eslint-disable-next-line prefer-const
|
||||||
let Settings;
|
let Settings;
|
||||||
let onReady = [];
|
let onReady = [];
|
||||||
@@ -574,7 +563,16 @@ define('settings', ['hooks', 'alerts'], function (hooks, alerts) {
|
|||||||
|
|
||||||
|
|
||||||
helper.registerReadyJobs(1);
|
helper.registerReadyJobs(1);
|
||||||
require(DEFAULT_PLUGINS, function () {
|
require([
|
||||||
|
'settings/checkbox',
|
||||||
|
'settings/number',
|
||||||
|
'settings/textarea',
|
||||||
|
'settings/select',
|
||||||
|
'settings/array',
|
||||||
|
'settings/key',
|
||||||
|
'settings/object',
|
||||||
|
'settings/sorted-list',
|
||||||
|
], function () {
|
||||||
for (let i = 0; i < arguments.length; i += 1) {
|
for (let i = 0; i < arguments.length; i += 1) {
|
||||||
Settings.registerPlugin(arguments[i]);
|
Settings.registerPlugin(arguments[i]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,24 +12,22 @@ define('taskbar', ['benchpress', 'translator', 'hooks'], function (Benchpress, t
|
|||||||
self.tasklist = self.taskbar.find('ul');
|
self.tasklist = self.taskbar.find('ul');
|
||||||
$(document.body).append(self.taskbar);
|
$(document.body).append(self.taskbar);
|
||||||
|
|
||||||
self.taskbar.on('click', 'li', function () {
|
self.taskbar.on('click', 'li', async function () {
|
||||||
const $btn = $(this);
|
const $btn = $(this);
|
||||||
const module = $btn.attr('data-module');
|
const moduleName = $btn.attr('data-module');
|
||||||
const uuid = $btn.attr('data-uuid');
|
const uuid = $btn.attr('data-uuid');
|
||||||
|
|
||||||
require([module], function (module) {
|
const module = await app.require(moduleName);
|
||||||
if (!$btn.hasClass('active')) {
|
if (!$btn.hasClass('active')) {
|
||||||
minimizeAll();
|
minimizeAll();
|
||||||
module.load(uuid);
|
module.load(uuid);
|
||||||
taskbar.toggleNew(uuid, false);
|
taskbar.toggleNew(uuid, false);
|
||||||
|
|
||||||
taskbar.tasklist.removeClass('active');
|
|
||||||
$btn.addClass('active');
|
|
||||||
} else {
|
|
||||||
module.minimize(uuid);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
taskbar.tasklist.removeClass('active');
|
||||||
|
$btn.addClass('active');
|
||||||
|
} else {
|
||||||
|
module.minimize(uuid);
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -39,22 +37,21 @@ define('taskbar', ['benchpress', 'translator', 'hooks'], function (Benchpress, t
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
taskbar.close = function (module, uuid) {
|
taskbar.close = async function (moduleName, uuid) {
|
||||||
// Sends signal to the appropriate module's .close() fn (if present)
|
// Sends signal to the appropriate module's .close() fn (if present)
|
||||||
const btnEl = taskbar.tasklist.find('[data-module="' + module + '"][data-uuid="' + uuid + '"]');
|
const btnEl = taskbar.tasklist.find('[data-module="' + module + '"][data-uuid="' + uuid + '"]');
|
||||||
let fnName = 'close';
|
let fnName = 'close';
|
||||||
|
|
||||||
// TODO: Refactor chat module to not take uuid in close instead of by jQuery element
|
// TODO: Refactor chat module to not take uuid in close instead of by jQuery element
|
||||||
if (module === 'chat') {
|
if (moduleName === 'chat') {
|
||||||
fnName = 'closeByUUID';
|
fnName = 'closeByUUID';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (btnEl.length) {
|
if (btnEl.length) {
|
||||||
require([module], function (module) {
|
const module = await app.require(moduleName);
|
||||||
if (typeof module[fnName] === 'function') {
|
if (module && typeof module[fnName] === 'function') {
|
||||||
module[fnName](uuid);
|
module[fnName](uuid);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
629
public/src/modules/translator.common.js
Normal file
629
public/src/modules/translator.common.js
Normal file
@@ -0,0 +1,629 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = function (utils, load, warn) {
|
||||||
|
const assign = Object.assign || jQuery.extend;
|
||||||
|
|
||||||
|
function escapeHTML(str) {
|
||||||
|
return utils.escapeHTML(utils.decodeHTMLEntities(
|
||||||
|
String(str)
|
||||||
|
.replace(/[\s\xa0]+/g, ' ')
|
||||||
|
.replace(/^\s+|\s+$/g, '')
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
const Translator = (function () {
|
||||||
|
/**
|
||||||
|
* Construct a new Translator object
|
||||||
|
* @param {string} language - Language code for this translator instance
|
||||||
|
* @exports translator.Translator
|
||||||
|
*/
|
||||||
|
function Translator(language) {
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
if (!language) {
|
||||||
|
throw new TypeError('Parameter `language` must be a language string. Received ' + language + (language === '' ? '(empty string)' : ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.modules = Object.keys(Translator.moduleFactories).map(function (namespace) {
|
||||||
|
const factory = Translator.moduleFactories[namespace];
|
||||||
|
return [namespace, factory(language)];
|
||||||
|
}).reduce(function (prev, elem) {
|
||||||
|
const namespace = elem[0];
|
||||||
|
const module = elem[1];
|
||||||
|
prev[namespace] = module;
|
||||||
|
|
||||||
|
return prev;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
self.lang = language;
|
||||||
|
self.translations = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Translator.prototype.load = load;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the translation instructions into the language of the Translator instance
|
||||||
|
* @param {string} str - Source string
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
Translator.prototype.translate = function translate(str) {
|
||||||
|
// regex for valid text in namespace / key
|
||||||
|
const validText = 'a-zA-Z0-9\\-_.\\/';
|
||||||
|
const validTextRegex = new RegExp('[' + validText + ']');
|
||||||
|
const invalidTextRegex = new RegExp('[^' + validText + '\\]]');
|
||||||
|
|
||||||
|
// current cursor position
|
||||||
|
let cursor = 0;
|
||||||
|
// last break of the input string
|
||||||
|
let lastBreak = 0;
|
||||||
|
// length of the input string
|
||||||
|
const len = str.length;
|
||||||
|
// array to hold the promises for the translations
|
||||||
|
// and the strings of untranslated text in between
|
||||||
|
const toTranslate = [];
|
||||||
|
|
||||||
|
// to store the state of if we're currently in a top-level token for later
|
||||||
|
let inToken = false;
|
||||||
|
|
||||||
|
// split a translator string into an array of tokens
|
||||||
|
// but don't split by commas inside other translator strings
|
||||||
|
function split(text) {
|
||||||
|
const len = text.length;
|
||||||
|
const arr = [];
|
||||||
|
let i = 0;
|
||||||
|
let brk = 0;
|
||||||
|
let level = 0;
|
||||||
|
|
||||||
|
while (i + 2 <= len) {
|
||||||
|
if (text[i] === '[' && text[i + 1] === '[') {
|
||||||
|
level += 1;
|
||||||
|
i += 1;
|
||||||
|
} else if (text[i] === ']' && text[i + 1] === ']') {
|
||||||
|
level -= 1;
|
||||||
|
i += 1;
|
||||||
|
} else if (level === 0 && text[i] === ',' && text[i - 1] !== '\\') {
|
||||||
|
arr.push(text.slice(brk, i).trim());
|
||||||
|
i += 1;
|
||||||
|
brk = i;
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
arr.push(text.slice(brk, i + 1).trim());
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// move to the first [[
|
||||||
|
cursor = str.indexOf('[[', cursor);
|
||||||
|
|
||||||
|
// the loooop, we'll go to where the cursor
|
||||||
|
// is equal to the length of the string since
|
||||||
|
// slice doesn't include the ending index
|
||||||
|
while (cursor + 2 <= len && cursor !== -1) {
|
||||||
|
// split the string from the last break
|
||||||
|
// to the character before the cursor
|
||||||
|
// add that to the result array
|
||||||
|
toTranslate.push(str.slice(lastBreak, cursor));
|
||||||
|
// set the cursor position past the beginning
|
||||||
|
// brackets of the translation string
|
||||||
|
cursor += 2;
|
||||||
|
// set the last break to our current
|
||||||
|
// spot since we just broke the string
|
||||||
|
lastBreak = cursor;
|
||||||
|
// we're in a token now
|
||||||
|
inToken = true;
|
||||||
|
|
||||||
|
// the current level of nesting of the translation strings
|
||||||
|
let level = 0;
|
||||||
|
let char0;
|
||||||
|
let char1;
|
||||||
|
// validating the current string is actually a translation
|
||||||
|
let textBeforeColonFound = false;
|
||||||
|
let colonFound = false;
|
||||||
|
let textAfterColonFound = false;
|
||||||
|
let commaAfterNameFound = false;
|
||||||
|
|
||||||
|
while (cursor + 2 <= len) {
|
||||||
|
char0 = str[cursor];
|
||||||
|
char1 = str[cursor + 1];
|
||||||
|
// found some text after the double bracket,
|
||||||
|
// so this is probably a translation string
|
||||||
|
if (!textBeforeColonFound && validTextRegex.test(char0)) {
|
||||||
|
textBeforeColonFound = true;
|
||||||
|
cursor += 1;
|
||||||
|
// found a colon, so this is probably a translation string
|
||||||
|
} else if (textBeforeColonFound && !colonFound && char0 === ':') {
|
||||||
|
colonFound = true;
|
||||||
|
cursor += 1;
|
||||||
|
// found some text after the colon,
|
||||||
|
// so this is probably a translation string
|
||||||
|
} else if (colonFound && !textAfterColonFound && validTextRegex.test(char0)) {
|
||||||
|
textAfterColonFound = true;
|
||||||
|
cursor += 1;
|
||||||
|
} else if (textAfterColonFound && !commaAfterNameFound && char0 === ',') {
|
||||||
|
commaAfterNameFound = true;
|
||||||
|
cursor += 1;
|
||||||
|
// a space or comma was found before the name
|
||||||
|
// this isn't a translation string, so back out
|
||||||
|
} else if (!(textBeforeColonFound && colonFound && textAfterColonFound && commaAfterNameFound) &&
|
||||||
|
invalidTextRegex.test(char0)) {
|
||||||
|
cursor += 1;
|
||||||
|
lastBreak -= 2;
|
||||||
|
// no longer in a token
|
||||||
|
inToken = false;
|
||||||
|
if (level > 0) {
|
||||||
|
level -= 1;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// if we're at the beginning of another translation string,
|
||||||
|
// we're nested, so add to our level
|
||||||
|
} else if (char0 === '[' && char1 === '[') {
|
||||||
|
level += 1;
|
||||||
|
cursor += 2;
|
||||||
|
// if we're at the end of a translation string
|
||||||
|
} else if (char0 === ']' && char1 === ']') {
|
||||||
|
// if we're at the base level, then this is the end
|
||||||
|
if (level === 0) {
|
||||||
|
// so grab the name and args
|
||||||
|
const currentSlice = str.slice(lastBreak, cursor);
|
||||||
|
const result = split(currentSlice);
|
||||||
|
const name = result[0];
|
||||||
|
const args = result.slice(1);
|
||||||
|
|
||||||
|
// make a backup based on the raw string of the token
|
||||||
|
// if there are arguments to the token
|
||||||
|
let backup = '';
|
||||||
|
if (args && args.length) {
|
||||||
|
backup = this.translate(currentSlice);
|
||||||
|
}
|
||||||
|
// add the translation promise to the array
|
||||||
|
toTranslate.push(this.translateKey(name, args, backup));
|
||||||
|
// skip past the ending brackets
|
||||||
|
cursor += 2;
|
||||||
|
// set this as our last break
|
||||||
|
lastBreak = cursor;
|
||||||
|
// and we're no longer in a translation string,
|
||||||
|
// so continue with the main loop
|
||||||
|
inToken = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// otherwise we lower the level
|
||||||
|
level -= 1;
|
||||||
|
// and skip past the ending brackets
|
||||||
|
cursor += 2;
|
||||||
|
} else {
|
||||||
|
// otherwise just move to the next character
|
||||||
|
cursor += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip to the next [[
|
||||||
|
cursor = str.indexOf('[[', cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ending string of source
|
||||||
|
let last = str.slice(lastBreak);
|
||||||
|
|
||||||
|
// if we were mid-token, treat it as invalid
|
||||||
|
if (inToken) {
|
||||||
|
last = this.translate(last);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the remaining text after the last translation string
|
||||||
|
toTranslate.push(last);
|
||||||
|
|
||||||
|
// and return a promise for the concatenated translated string
|
||||||
|
return Promise.all(toTranslate).then(function (translated) {
|
||||||
|
return translated.join('');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translates a specific key and array of arguments
|
||||||
|
* @param {string} name - Translation key (ex. 'global:home')
|
||||||
|
* @param {string[]} args - Arguments for `%1`, `%2`, etc
|
||||||
|
* @param {string|Promise<string>} backup - Text to use in case the key can't be found
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
Translator.prototype.translateKey = function translateKey(name, args, backup) {
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
const result = name.split(':', 2);
|
||||||
|
const namespace = result[0];
|
||||||
|
const key = result[1];
|
||||||
|
|
||||||
|
if (self.modules[namespace]) {
|
||||||
|
return Promise.resolve(self.modules[namespace](key, args));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (namespace && result.length === 1) {
|
||||||
|
return Promise.resolve('[[' + namespace + ']]');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (namespace && !key) {
|
||||||
|
warn('Missing key in translation token "' + name + '" for language "' + self.lang + '"');
|
||||||
|
return Promise.resolve('[[' + namespace + ']]');
|
||||||
|
}
|
||||||
|
|
||||||
|
const translation = this.getTranslation(namespace, key);
|
||||||
|
return translation.then(function (translated) {
|
||||||
|
// check if the translation is missing first
|
||||||
|
if (!translated) {
|
||||||
|
warn('Missing translation "' + name + '" for language "' + self.lang + '"');
|
||||||
|
return backup || key;
|
||||||
|
}
|
||||||
|
|
||||||
|
const argsToTranslate = args.map(function (arg) {
|
||||||
|
return self.translate(escapeHTML(arg));
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(argsToTranslate).then(function (translatedArgs) {
|
||||||
|
let out = translated;
|
||||||
|
translatedArgs.forEach(function (arg, i) {
|
||||||
|
let escaped = arg.replace(/%(?=\d)/g, '%').replace(/\\,/g, ',');
|
||||||
|
// fix double escaped translation keys, see https://github.com/NodeBB/NodeBB/issues/9206
|
||||||
|
escaped = escaped.replace(/&lsqb;/g, '[')
|
||||||
|
.replace(/&rsqb;/g, ']');
|
||||||
|
out = out.replace(new RegExp('%' + (i + 1), 'g'), escaped);
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load translation file (or use a cached version), and optionally return the translation of a certain key
|
||||||
|
* @param {string} namespace - The file name of the translation namespace
|
||||||
|
* @param {string} [key] - The key of the specific translation to getJSON
|
||||||
|
* @returns {Promise<{ [key: string]: string } | string>}
|
||||||
|
*/
|
||||||
|
Translator.prototype.getTranslation = function getTranslation(namespace, key) {
|
||||||
|
let translation;
|
||||||
|
if (!namespace) {
|
||||||
|
warn('[translator] Parameter `namespace` is ' + namespace + (namespace === '' ? '(empty string)' : ''));
|
||||||
|
translation = Promise.resolve({});
|
||||||
|
} else {
|
||||||
|
this.translations[namespace] = this.translations[namespace] ||
|
||||||
|
this.load(this.lang, namespace).catch(function () { return {}; });
|
||||||
|
translation = this.translations[namespace];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key) {
|
||||||
|
return translation.then(function (x) {
|
||||||
|
if (typeof x[key] === 'string') return x[key];
|
||||||
|
const keyParts = key.split('.');
|
||||||
|
for (let i = 0; i <= keyParts.length; i++) {
|
||||||
|
if (i === keyParts.length) {
|
||||||
|
// default to trying to find key with the same name as parent or equal to empty string
|
||||||
|
return x[keyParts[i - 1]] !== undefined ? x[keyParts[i - 1]] : x[''];
|
||||||
|
}
|
||||||
|
switch (typeof x[keyParts[i]]) {
|
||||||
|
case 'object':
|
||||||
|
x = x[keyParts[i]];
|
||||||
|
break;
|
||||||
|
case 'string':
|
||||||
|
if (i === keyParts.length - 1) {
|
||||||
|
return x[keyParts[i]];
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return translation;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Node} node
|
||||||
|
* @returns {Node[]}
|
||||||
|
*/
|
||||||
|
function descendantTextNodes(node) {
|
||||||
|
const textNodes = [];
|
||||||
|
|
||||||
|
function helper(node) {
|
||||||
|
if (node.nodeType === 3) {
|
||||||
|
textNodes.push(node);
|
||||||
|
} else {
|
||||||
|
for (let i = 0, c = node.childNodes, l = c.length; i < l; i += 1) {
|
||||||
|
helper(c[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
helper(node);
|
||||||
|
return textNodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively translate a DOM element in place
|
||||||
|
* @param {Element} element - Root element to translate
|
||||||
|
* @param {string[]} [attributes] - Array of node attributes to translate
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
Translator.prototype.translateInPlace = function translateInPlace(element, attributes) {
|
||||||
|
attributes = attributes || ['placeholder', 'title'];
|
||||||
|
|
||||||
|
const nodes = descendantTextNodes(element);
|
||||||
|
const text = nodes.map(function (node) {
|
||||||
|
return utils.escapeHTML(node.nodeValue);
|
||||||
|
}).join(' || ');
|
||||||
|
|
||||||
|
const attrNodes = attributes.reduce(function (prev, attr) {
|
||||||
|
const tuples = Array.prototype.map.call(element.querySelectorAll('[' + attr + '*="[["]'), function (el) {
|
||||||
|
return [attr, el];
|
||||||
|
});
|
||||||
|
return prev.concat(tuples);
|
||||||
|
}, []);
|
||||||
|
const attrText = attrNodes.map(function (node) {
|
||||||
|
return node[1].getAttribute(node[0]);
|
||||||
|
}).join(' || ');
|
||||||
|
|
||||||
|
return Promise.all([
|
||||||
|
this.translate(text),
|
||||||
|
this.translate(attrText),
|
||||||
|
]).then(function (ref) {
|
||||||
|
const translated = ref[0];
|
||||||
|
const translatedAttrs = ref[1];
|
||||||
|
if (translated) {
|
||||||
|
translated.split(' || ').forEach(function (html, i) {
|
||||||
|
$(nodes[i]).replaceWith(html);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (translatedAttrs) {
|
||||||
|
translatedAttrs.split(' || ').forEach(function (text, i) {
|
||||||
|
attrNodes[i][1].setAttribute(attrNodes[i][0], text);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the language of the current environment, falling back to defaults
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
Translator.getLanguage = function getLanguage() {
|
||||||
|
return utils.getLanguage();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and cache a new Translator instance, or return a cached one
|
||||||
|
* @param {string} [language] - ('en-GB') Language string
|
||||||
|
* @returns {Translator}
|
||||||
|
*/
|
||||||
|
Translator.create = function create(language) {
|
||||||
|
if (!language) {
|
||||||
|
language = Translator.getLanguage();
|
||||||
|
}
|
||||||
|
|
||||||
|
Translator.cache[language] = Translator.cache[language] || new Translator(language);
|
||||||
|
|
||||||
|
return Translator.cache[language];
|
||||||
|
};
|
||||||
|
|
||||||
|
Translator.cache = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a custom module to handle translations
|
||||||
|
* @param {string} namespace - Namespace to handle translation for
|
||||||
|
* @param {Function} factory - Function to return the translation function for this namespace
|
||||||
|
*/
|
||||||
|
Translator.registerModule = function registerModule(namespace, factory) {
|
||||||
|
Translator.moduleFactories[namespace] = factory;
|
||||||
|
|
||||||
|
Object.keys(Translator.cache).forEach(function (key) {
|
||||||
|
const translator = Translator.cache[key];
|
||||||
|
translator.modules[namespace] = factory(translator.lang);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Translator.moduleFactories = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the translator patterns from text
|
||||||
|
* @param {string} text
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
Translator.removePatterns = function removePatterns(text) {
|
||||||
|
const len = text.length;
|
||||||
|
let cursor = 0;
|
||||||
|
let lastBreak = 0;
|
||||||
|
let level = 0;
|
||||||
|
let out = '';
|
||||||
|
let 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(/\[\[/g, '[[').replace(/\]\]/g, ']]') : text;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unescape escaped translator patterns in text
|
||||||
|
* @param {string} text
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
Translator.unescape = function unescape(text) {
|
||||||
|
return typeof text === 'string' ?
|
||||||
|
text.replace(/[/g, '[').replace(/\\\[/g, '[')
|
||||||
|
.replace(/]/g, ']').replace(/\\\]/g, ']') :
|
||||||
|
text;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a translator pattern
|
||||||
|
* @param {string} name - Translation name
|
||||||
|
* @param {...string} arg - Optional argument for the pattern
|
||||||
|
*/
|
||||||
|
Translator.compile = function compile() {
|
||||||
|
const args = Array.prototype.slice.call(arguments, 0).map(function (text) {
|
||||||
|
// escape commas and percent signs in arguments
|
||||||
|
return String(text).replace(/%/g, '%').replace(/,/g, ',');
|
||||||
|
});
|
||||||
|
|
||||||
|
return '[[' + args.join(', ') + ']]';
|
||||||
|
};
|
||||||
|
|
||||||
|
return Translator;
|
||||||
|
}());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @exports translator
|
||||||
|
*/
|
||||||
|
const adaptor = {
|
||||||
|
/**
|
||||||
|
* The Translator class
|
||||||
|
*/
|
||||||
|
Translator: Translator,
|
||||||
|
|
||||||
|
compile: Translator.compile,
|
||||||
|
escape: Translator.escape,
|
||||||
|
unescape: Translator.unescape,
|
||||||
|
getLanguage: Translator.getLanguage,
|
||||||
|
|
||||||
|
flush: function () {
|
||||||
|
Object.keys(Translator.cache).forEach(function (code) {
|
||||||
|
Translator.cache[code].translations = {};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
flushNamespace: function (namespace) {
|
||||||
|
Object.keys(Translator.cache).forEach(function (code) {
|
||||||
|
if (Translator.cache[code] &&
|
||||||
|
Translator.cache[code].translations &&
|
||||||
|
Translator.cache[code].translations[namespace]
|
||||||
|
) {
|
||||||
|
Translator.cache[code].translations[namespace] = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legacy translator function for backwards compatibility
|
||||||
|
*/
|
||||||
|
translate: function translate(text, language, callback) {
|
||||||
|
// TODO: deprecate?
|
||||||
|
|
||||||
|
let cb = callback;
|
||||||
|
let lang = language;
|
||||||
|
if (typeof language === 'function') {
|
||||||
|
cb = language;
|
||||||
|
lang = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(typeof text === 'string' || text instanceof String) || text === '') {
|
||||||
|
if (cb) {
|
||||||
|
return setTimeout(cb, 0, '');
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return Translator.create(lang).translate(text).then(function (output) {
|
||||||
|
if (cb) {
|
||||||
|
setTimeout(cb, 0, output);
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}, function (err) {
|
||||||
|
warn('Translation failed: ' + err.stack);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add translations to the cache
|
||||||
|
*/
|
||||||
|
addTranslation: function addTranslation(language, namespace, translation) {
|
||||||
|
Translator.create(language).getTranslation(namespace).then(function (translations) {
|
||||||
|
assign(translations, translation);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the translations object
|
||||||
|
*/
|
||||||
|
getTranslations: function getTranslations(language, namespace, callback) {
|
||||||
|
callback = callback || function () {};
|
||||||
|
Translator.create(language).getTranslation(namespace).then(callback);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alias of getTranslations
|
||||||
|
*/
|
||||||
|
load: function load(language, namespace, callback) {
|
||||||
|
adaptor.getTranslations(language, namespace, callback);
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleTimeagoShorthand: function toggleTimeagoShorthand(callback) {
|
||||||
|
/* eslint "prefer-object-spread": "off" */
|
||||||
|
function toggle() {
|
||||||
|
const tmp = assign({}, jQuery.timeago.settings.strings);
|
||||||
|
jQuery.timeago.settings.strings = assign({}, adaptor.timeagoShort);
|
||||||
|
adaptor.timeagoShort = assign({}, tmp);
|
||||||
|
if (typeof callback === 'function') {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!adaptor.timeagoShort) {
|
||||||
|
let languageCode = utils.userLangToTimeagoCode(config.userLang);
|
||||||
|
if (!config.timeagoCodes.includes(languageCode + '-short')) {
|
||||||
|
languageCode = 'en';
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalSettings = assign({}, jQuery.timeago.settings.strings);
|
||||||
|
adaptor.switchTimeagoLanguage(languageCode + '-short', function () {
|
||||||
|
adaptor.timeagoShort = assign({}, jQuery.timeago.settings.strings);
|
||||||
|
jQuery.timeago.settings.strings = assign({}, originalSettings);
|
||||||
|
toggle();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toggle();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
switchTimeagoLanguage: function switchTimeagoLanguage(langCode, callback) {
|
||||||
|
// Delete the cached shorthand strings if present
|
||||||
|
delete adaptor.timeagoShort;
|
||||||
|
import(/* webpackChunkName: "timeago/[request]" */ 'timeago/locales/jquery.timeago.' + langCode).then(callback);
|
||||||
|
},
|
||||||
|
|
||||||
|
prepareDOM: function prepareDOM() {
|
||||||
|
// Add directional code if necessary
|
||||||
|
adaptor.translate('[[language:dir]]', function (value) {
|
||||||
|
if (value && !$('html').attr('data-dir')) {
|
||||||
|
jQuery('html').css('direction', value).attr('data-dir', value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return adaptor;
|
||||||
|
};
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
(function (factory) {
|
const factory = require('./translator.common');
|
||||||
|
|
||||||
|
define('translator', ['jquery', 'utils'], function (jQuery, utils) {
|
||||||
function loadClient(language, namespace) {
|
function loadClient(language, namespace) {
|
||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function (resolve, reject) {
|
||||||
jQuery.getJSON([config.asset_base_url, 'language', language, namespace].join('/') + '.json?' + config['cache-buster'], function (data) {
|
jQuery.getJSON([config.asset_base_url, 'language', language, namespace].join('/') + '.json?' + config['cache-buster'], function (data) {
|
||||||
@@ -18,667 +20,6 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let warn = function () { console.warn.apply(console, arguments); };
|
const warn = function () { console.warn.apply(console, arguments); };
|
||||||
if (typeof define === 'function' && define.amd) {
|
return factory(utils, loadClient, warn);
|
||||||
// AMD. Register as a named module
|
});
|
||||||
define('translator', [], function () {
|
|
||||||
return factory(utils, loadClient, warn);
|
|
||||||
});
|
|
||||||
} else if (typeof module === 'object' && module.exports) {
|
|
||||||
// Node
|
|
||||||
(function () {
|
|
||||||
if (global.env === 'development') {
|
|
||||||
const winston = require('winston');
|
|
||||||
warn = function (a) {
|
|
||||||
winston.warn(a);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = factory(require('../utils'), function (lang, namespace) {
|
|
||||||
const languages = require('../../../src/languages');
|
|
||||||
return languages.get(lang, namespace);
|
|
||||||
}, warn);
|
|
||||||
}());
|
|
||||||
}
|
|
||||||
}(function (utils, load, warn) {
|
|
||||||
const assign = Object.assign || jQuery.extend;
|
|
||||||
|
|
||||||
function escapeHTML(str) {
|
|
||||||
return utils.escapeHTML(utils.decodeHTMLEntities(
|
|
||||||
String(str)
|
|
||||||
.replace(/[\s\xa0]+/g, ' ')
|
|
||||||
.replace(/^\s+|\s+$/g, '')
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
const Translator = (function () {
|
|
||||||
/**
|
|
||||||
* Construct a new Translator object
|
|
||||||
* @param {string} language - Language code for this translator instance
|
|
||||||
* @exports translator.Translator
|
|
||||||
*/
|
|
||||||
function Translator(language) {
|
|
||||||
const self = this;
|
|
||||||
|
|
||||||
if (!language) {
|
|
||||||
throw new TypeError('Parameter `language` must be a language string. Received ' + language + (language === '' ? '(empty string)' : ''));
|
|
||||||
}
|
|
||||||
|
|
||||||
self.modules = Object.keys(Translator.moduleFactories).map(function (namespace) {
|
|
||||||
const factory = Translator.moduleFactories[namespace];
|
|
||||||
return [namespace, factory(language)];
|
|
||||||
}).reduce(function (prev, elem) {
|
|
||||||
const namespace = elem[0];
|
|
||||||
const module = elem[1];
|
|
||||||
prev[namespace] = module;
|
|
||||||
|
|
||||||
return prev;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
self.lang = language;
|
|
||||||
self.translations = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
Translator.prototype.load = load;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse the translation instructions into the language of the Translator instance
|
|
||||||
* @param {string} str - Source string
|
|
||||||
* @returns {Promise<string>}
|
|
||||||
*/
|
|
||||||
Translator.prototype.translate = function translate(str) {
|
|
||||||
// regex for valid text in namespace / key
|
|
||||||
const validText = 'a-zA-Z0-9\\-_.\\/';
|
|
||||||
const validTextRegex = new RegExp('[' + validText + ']');
|
|
||||||
const invalidTextRegex = new RegExp('[^' + validText + '\\]]');
|
|
||||||
|
|
||||||
// current cursor position
|
|
||||||
let cursor = 0;
|
|
||||||
// last break of the input string
|
|
||||||
let lastBreak = 0;
|
|
||||||
// length of the input string
|
|
||||||
const len = str.length;
|
|
||||||
// array to hold the promises for the translations
|
|
||||||
// and the strings of untranslated text in between
|
|
||||||
const toTranslate = [];
|
|
||||||
|
|
||||||
// to store the state of if we're currently in a top-level token for later
|
|
||||||
let inToken = false;
|
|
||||||
|
|
||||||
// split a translator string into an array of tokens
|
|
||||||
// but don't split by commas inside other translator strings
|
|
||||||
function split(text) {
|
|
||||||
const len = text.length;
|
|
||||||
const arr = [];
|
|
||||||
let i = 0;
|
|
||||||
let brk = 0;
|
|
||||||
let level = 0;
|
|
||||||
|
|
||||||
while (i + 2 <= len) {
|
|
||||||
if (text[i] === '[' && text[i + 1] === '[') {
|
|
||||||
level += 1;
|
|
||||||
i += 1;
|
|
||||||
} else if (text[i] === ']' && text[i + 1] === ']') {
|
|
||||||
level -= 1;
|
|
||||||
i += 1;
|
|
||||||
} else if (level === 0 && text[i] === ',' && text[i - 1] !== '\\') {
|
|
||||||
arr.push(text.slice(brk, i).trim());
|
|
||||||
i += 1;
|
|
||||||
brk = i;
|
|
||||||
}
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
arr.push(text.slice(brk, i + 1).trim());
|
|
||||||
return arr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// move to the first [[
|
|
||||||
cursor = str.indexOf('[[', cursor);
|
|
||||||
|
|
||||||
// the loooop, we'll go to where the cursor
|
|
||||||
// is equal to the length of the string since
|
|
||||||
// slice doesn't include the ending index
|
|
||||||
while (cursor + 2 <= len && cursor !== -1) {
|
|
||||||
// split the string from the last break
|
|
||||||
// to the character before the cursor
|
|
||||||
// add that to the result array
|
|
||||||
toTranslate.push(str.slice(lastBreak, cursor));
|
|
||||||
// set the cursor position past the beginning
|
|
||||||
// brackets of the translation string
|
|
||||||
cursor += 2;
|
|
||||||
// set the last break to our current
|
|
||||||
// spot since we just broke the string
|
|
||||||
lastBreak = cursor;
|
|
||||||
// we're in a token now
|
|
||||||
inToken = true;
|
|
||||||
|
|
||||||
// the current level of nesting of the translation strings
|
|
||||||
let level = 0;
|
|
||||||
let char0;
|
|
||||||
let char1;
|
|
||||||
// validating the current string is actually a translation
|
|
||||||
let textBeforeColonFound = false;
|
|
||||||
let colonFound = false;
|
|
||||||
let textAfterColonFound = false;
|
|
||||||
let commaAfterNameFound = false;
|
|
||||||
|
|
||||||
while (cursor + 2 <= len) {
|
|
||||||
char0 = str[cursor];
|
|
||||||
char1 = str[cursor + 1];
|
|
||||||
// found some text after the double bracket,
|
|
||||||
// so this is probably a translation string
|
|
||||||
if (!textBeforeColonFound && validTextRegex.test(char0)) {
|
|
||||||
textBeforeColonFound = true;
|
|
||||||
cursor += 1;
|
|
||||||
// found a colon, so this is probably a translation string
|
|
||||||
} else if (textBeforeColonFound && !colonFound && char0 === ':') {
|
|
||||||
colonFound = true;
|
|
||||||
cursor += 1;
|
|
||||||
// found some text after the colon,
|
|
||||||
// so this is probably a translation string
|
|
||||||
} else if (colonFound && !textAfterColonFound && validTextRegex.test(char0)) {
|
|
||||||
textAfterColonFound = true;
|
|
||||||
cursor += 1;
|
|
||||||
} else if (textAfterColonFound && !commaAfterNameFound && char0 === ',') {
|
|
||||||
commaAfterNameFound = true;
|
|
||||||
cursor += 1;
|
|
||||||
// a space or comma was found before the name
|
|
||||||
// this isn't a translation string, so back out
|
|
||||||
} else if (!(textBeforeColonFound && colonFound && textAfterColonFound && commaAfterNameFound) &&
|
|
||||||
invalidTextRegex.test(char0)) {
|
|
||||||
cursor += 1;
|
|
||||||
lastBreak -= 2;
|
|
||||||
// no longer in a token
|
|
||||||
inToken = false;
|
|
||||||
if (level > 0) {
|
|
||||||
level -= 1;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// if we're at the beginning of another translation string,
|
|
||||||
// we're nested, so add to our level
|
|
||||||
} else if (char0 === '[' && char1 === '[') {
|
|
||||||
level += 1;
|
|
||||||
cursor += 2;
|
|
||||||
// if we're at the end of a translation string
|
|
||||||
} else if (char0 === ']' && char1 === ']') {
|
|
||||||
// if we're at the base level, then this is the end
|
|
||||||
if (level === 0) {
|
|
||||||
// so grab the name and args
|
|
||||||
const currentSlice = str.slice(lastBreak, cursor);
|
|
||||||
const result = split(currentSlice);
|
|
||||||
const name = result[0];
|
|
||||||
const args = result.slice(1);
|
|
||||||
|
|
||||||
// make a backup based on the raw string of the token
|
|
||||||
// if there are arguments to the token
|
|
||||||
let backup = '';
|
|
||||||
if (args && args.length) {
|
|
||||||
backup = this.translate(currentSlice);
|
|
||||||
}
|
|
||||||
// add the translation promise to the array
|
|
||||||
toTranslate.push(this.translateKey(name, args, backup));
|
|
||||||
// skip past the ending brackets
|
|
||||||
cursor += 2;
|
|
||||||
// set this as our last break
|
|
||||||
lastBreak = cursor;
|
|
||||||
// and we're no longer in a translation string,
|
|
||||||
// so continue with the main loop
|
|
||||||
inToken = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// otherwise we lower the level
|
|
||||||
level -= 1;
|
|
||||||
// and skip past the ending brackets
|
|
||||||
cursor += 2;
|
|
||||||
} else {
|
|
||||||
// otherwise just move to the next character
|
|
||||||
cursor += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// skip to the next [[
|
|
||||||
cursor = str.indexOf('[[', cursor);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ending string of source
|
|
||||||
let last = str.slice(lastBreak);
|
|
||||||
|
|
||||||
// if we were mid-token, treat it as invalid
|
|
||||||
if (inToken) {
|
|
||||||
last = this.translate(last);
|
|
||||||
}
|
|
||||||
|
|
||||||
// add the remaining text after the last translation string
|
|
||||||
toTranslate.push(last);
|
|
||||||
|
|
||||||
// and return a promise for the concatenated translated string
|
|
||||||
return Promise.all(toTranslate).then(function (translated) {
|
|
||||||
return translated.join('');
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Translates a specific key and array of arguments
|
|
||||||
* @param {string} name - Translation key (ex. 'global:home')
|
|
||||||
* @param {string[]} args - Arguments for `%1`, `%2`, etc
|
|
||||||
* @param {string|Promise<string>} backup - Text to use in case the key can't be found
|
|
||||||
* @returns {Promise<string>}
|
|
||||||
*/
|
|
||||||
Translator.prototype.translateKey = function translateKey(name, args, backup) {
|
|
||||||
const self = this;
|
|
||||||
|
|
||||||
const result = name.split(':', 2);
|
|
||||||
const namespace = result[0];
|
|
||||||
const key = result[1];
|
|
||||||
|
|
||||||
if (self.modules[namespace]) {
|
|
||||||
return Promise.resolve(self.modules[namespace](key, args));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (namespace && result.length === 1) {
|
|
||||||
return Promise.resolve('[[' + namespace + ']]');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (namespace && !key) {
|
|
||||||
warn('Missing key in translation token "' + name + '" for language "' + self.lang + '"');
|
|
||||||
return Promise.resolve('[[' + namespace + ']]');
|
|
||||||
}
|
|
||||||
|
|
||||||
const translation = this.getTranslation(namespace, key);
|
|
||||||
return translation.then(function (translated) {
|
|
||||||
// check if the translation is missing first
|
|
||||||
if (!translated) {
|
|
||||||
warn('Missing translation "' + name + '" for language "' + self.lang + '"');
|
|
||||||
return backup || key;
|
|
||||||
}
|
|
||||||
|
|
||||||
const argsToTranslate = args.map(function (arg) {
|
|
||||||
return self.translate(escapeHTML(arg));
|
|
||||||
});
|
|
||||||
|
|
||||||
return Promise.all(argsToTranslate).then(function (translatedArgs) {
|
|
||||||
let out = translated;
|
|
||||||
translatedArgs.forEach(function (arg, i) {
|
|
||||||
let escaped = arg.replace(/%(?=\d)/g, '%').replace(/\\,/g, ',');
|
|
||||||
// fix double escaped translation keys, see https://github.com/NodeBB/NodeBB/issues/9206
|
|
||||||
escaped = escaped.replace(/&lsqb;/g, '[')
|
|
||||||
.replace(/&rsqb;/g, ']');
|
|
||||||
out = out.replace(new RegExp('%' + (i + 1), 'g'), escaped);
|
|
||||||
});
|
|
||||||
return out;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load translation file (or use a cached version), and optionally return the translation of a certain key
|
|
||||||
* @param {string} namespace - The file name of the translation namespace
|
|
||||||
* @param {string} [key] - The key of the specific translation to getJSON
|
|
||||||
* @returns {Promise<{ [key: string]: string } | string>}
|
|
||||||
*/
|
|
||||||
Translator.prototype.getTranslation = function getTranslation(namespace, key) {
|
|
||||||
let translation;
|
|
||||||
if (!namespace) {
|
|
||||||
warn('[translator] Parameter `namespace` is ' + namespace + (namespace === '' ? '(empty string)' : ''));
|
|
||||||
translation = Promise.resolve({});
|
|
||||||
} else {
|
|
||||||
this.translations[namespace] = this.translations[namespace] ||
|
|
||||||
this.load(this.lang, namespace).catch(function () { return {}; });
|
|
||||||
translation = this.translations[namespace];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key) {
|
|
||||||
return translation.then(function (x) {
|
|
||||||
if (typeof x[key] === 'string') return x[key];
|
|
||||||
const keyParts = key.split('.');
|
|
||||||
for (let i = 0; i <= keyParts.length; i++) {
|
|
||||||
if (i === keyParts.length) {
|
|
||||||
// default to trying to find key with the same name as parent or equal to empty string
|
|
||||||
return x[keyParts[i - 1]] !== undefined ? x[keyParts[i - 1]] : x[''];
|
|
||||||
}
|
|
||||||
switch (typeof x[keyParts[i]]) {
|
|
||||||
case 'object':
|
|
||||||
x = x[keyParts[i]];
|
|
||||||
break;
|
|
||||||
case 'string':
|
|
||||||
if (i === keyParts.length - 1) {
|
|
||||||
return x[keyParts[i]];
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return translation;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Node} node
|
|
||||||
* @returns {Node[]}
|
|
||||||
*/
|
|
||||||
function descendantTextNodes(node) {
|
|
||||||
const textNodes = [];
|
|
||||||
|
|
||||||
function helper(node) {
|
|
||||||
if (node.nodeType === 3) {
|
|
||||||
textNodes.push(node);
|
|
||||||
} else {
|
|
||||||
for (let i = 0, c = node.childNodes, l = c.length; i < l; i += 1) {
|
|
||||||
helper(c[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
helper(node);
|
|
||||||
return textNodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively translate a DOM element in place
|
|
||||||
* @param {Element} element - Root element to translate
|
|
||||||
* @param {string[]} [attributes] - Array of node attributes to translate
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
Translator.prototype.translateInPlace = function translateInPlace(element, attributes) {
|
|
||||||
attributes = attributes || ['placeholder', 'title'];
|
|
||||||
|
|
||||||
const nodes = descendantTextNodes(element);
|
|
||||||
const text = nodes.map(function (node) {
|
|
||||||
return utils.escapeHTML(node.nodeValue);
|
|
||||||
}).join(' || ');
|
|
||||||
|
|
||||||
const attrNodes = attributes.reduce(function (prev, attr) {
|
|
||||||
const tuples = Array.prototype.map.call(element.querySelectorAll('[' + attr + '*="[["]'), function (el) {
|
|
||||||
return [attr, el];
|
|
||||||
});
|
|
||||||
return prev.concat(tuples);
|
|
||||||
}, []);
|
|
||||||
const attrText = attrNodes.map(function (node) {
|
|
||||||
return node[1].getAttribute(node[0]);
|
|
||||||
}).join(' || ');
|
|
||||||
|
|
||||||
return Promise.all([
|
|
||||||
this.translate(text),
|
|
||||||
this.translate(attrText),
|
|
||||||
]).then(function (ref) {
|
|
||||||
const translated = ref[0];
|
|
||||||
const translatedAttrs = ref[1];
|
|
||||||
if (translated) {
|
|
||||||
translated.split(' || ').forEach(function (html, i) {
|
|
||||||
$(nodes[i]).replaceWith(html);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (translatedAttrs) {
|
|
||||||
translatedAttrs.split(' || ').forEach(function (text, i) {
|
|
||||||
attrNodes[i][1].setAttribute(attrNodes[i][0], text);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the language of the current environment, falling back to defaults
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
Translator.getLanguage = function getLanguage() {
|
|
||||||
let lang;
|
|
||||||
|
|
||||||
if (typeof window === 'object' && window.config && window.utils) {
|
|
||||||
lang = utils.params().lang || config.userLang || config.defaultLang || 'en-GB';
|
|
||||||
} else {
|
|
||||||
const meta = require('../../../src/meta');
|
|
||||||
lang = meta.config && meta.config.defaultLang ? meta.config.defaultLang : 'en-GB';
|
|
||||||
}
|
|
||||||
|
|
||||||
return lang;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create and cache a new Translator instance, or return a cached one
|
|
||||||
* @param {string} [language] - ('en-GB') Language string
|
|
||||||
* @returns {Translator}
|
|
||||||
*/
|
|
||||||
Translator.create = function create(language) {
|
|
||||||
if (!language) {
|
|
||||||
language = Translator.getLanguage();
|
|
||||||
}
|
|
||||||
|
|
||||||
Translator.cache[language] = Translator.cache[language] || new Translator(language);
|
|
||||||
|
|
||||||
return Translator.cache[language];
|
|
||||||
};
|
|
||||||
|
|
||||||
Translator.cache = {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a custom module to handle translations
|
|
||||||
* @param {string} namespace - Namespace to handle translation for
|
|
||||||
* @param {Function} factory - Function to return the translation function for this namespace
|
|
||||||
*/
|
|
||||||
Translator.registerModule = function registerModule(namespace, factory) {
|
|
||||||
Translator.moduleFactories[namespace] = factory;
|
|
||||||
|
|
||||||
Object.keys(Translator.cache).forEach(function (key) {
|
|
||||||
const translator = Translator.cache[key];
|
|
||||||
translator.modules[namespace] = factory(translator.lang);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Translator.moduleFactories = {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the translator patterns from text
|
|
||||||
* @param {string} text
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
Translator.removePatterns = function removePatterns(text) {
|
|
||||||
const len = text.length;
|
|
||||||
let cursor = 0;
|
|
||||||
let lastBreak = 0;
|
|
||||||
let level = 0;
|
|
||||||
let out = '';
|
|
||||||
let 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(/\[\[/g, '[[').replace(/\]\]/g, ']]') : text;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unescape escaped translator patterns in text
|
|
||||||
* @param {string} text
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
Translator.unescape = function unescape(text) {
|
|
||||||
return typeof text === 'string' ?
|
|
||||||
text.replace(/[/g, '[').replace(/\\\[/g, '[')
|
|
||||||
.replace(/]/g, ']').replace(/\\\]/g, ']') :
|
|
||||||
text;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Construct a translator pattern
|
|
||||||
* @param {string} name - Translation name
|
|
||||||
* @param {...string} arg - Optional argument for the pattern
|
|
||||||
*/
|
|
||||||
Translator.compile = function compile() {
|
|
||||||
const args = Array.prototype.slice.call(arguments, 0).map(function (text) {
|
|
||||||
// escape commas and percent signs in arguments
|
|
||||||
return String(text).replace(/%/g, '%').replace(/,/g, ',');
|
|
||||||
});
|
|
||||||
|
|
||||||
return '[[' + args.join(', ') + ']]';
|
|
||||||
};
|
|
||||||
|
|
||||||
return Translator;
|
|
||||||
}());
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @exports translator
|
|
||||||
*/
|
|
||||||
const adaptor = {
|
|
||||||
/**
|
|
||||||
* The Translator class
|
|
||||||
*/
|
|
||||||
Translator: Translator,
|
|
||||||
|
|
||||||
compile: Translator.compile,
|
|
||||||
escape: Translator.escape,
|
|
||||||
unescape: Translator.unescape,
|
|
||||||
getLanguage: Translator.getLanguage,
|
|
||||||
|
|
||||||
flush: function () {
|
|
||||||
Object.keys(Translator.cache).forEach(function (code) {
|
|
||||||
Translator.cache[code].translations = {};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
flushNamespace: function (namespace) {
|
|
||||||
Object.keys(Translator.cache).forEach(function (code) {
|
|
||||||
if (Translator.cache[code] &&
|
|
||||||
Translator.cache[code].translations &&
|
|
||||||
Translator.cache[code].translations[namespace]
|
|
||||||
) {
|
|
||||||
Translator.cache[code].translations[namespace] = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Legacy translator function for backwards compatibility
|
|
||||||
*/
|
|
||||||
translate: function translate(text, language, callback) {
|
|
||||||
// TODO: deprecate?
|
|
||||||
|
|
||||||
let cb = callback;
|
|
||||||
let lang = language;
|
|
||||||
if (typeof language === 'function') {
|
|
||||||
cb = language;
|
|
||||||
lang = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(typeof text === 'string' || text instanceof String) || text === '') {
|
|
||||||
if (cb) {
|
|
||||||
return setTimeout(cb, 0, '');
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return Translator.create(lang).translate(text).then(function (output) {
|
|
||||||
if (cb) {
|
|
||||||
setTimeout(cb, 0, output);
|
|
||||||
}
|
|
||||||
return output;
|
|
||||||
}, function (err) {
|
|
||||||
warn('Translation failed: ' + err.stack);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add translations to the cache
|
|
||||||
*/
|
|
||||||
addTranslation: function addTranslation(language, namespace, translation) {
|
|
||||||
Translator.create(language).getTranslation(namespace).then(function (translations) {
|
|
||||||
assign(translations, translation);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the translations object
|
|
||||||
*/
|
|
||||||
getTranslations: function getTranslations(language, namespace, callback) {
|
|
||||||
callback = callback || function () {};
|
|
||||||
Translator.create(language).getTranslation(namespace).then(callback);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Alias of getTranslations
|
|
||||||
*/
|
|
||||||
load: function load(language, namespace, callback) {
|
|
||||||
adaptor.getTranslations(language, namespace, callback);
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleTimeagoShorthand: function toggleTimeagoShorthand(callback) {
|
|
||||||
/* eslint "prefer-object-spread": "off" */
|
|
||||||
function toggle() {
|
|
||||||
const tmp = assign({}, jQuery.timeago.settings.strings);
|
|
||||||
jQuery.timeago.settings.strings = assign({}, adaptor.timeagoShort);
|
|
||||||
adaptor.timeagoShort = assign({}, tmp);
|
|
||||||
if (typeof callback === 'function') {
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!adaptor.timeagoShort) {
|
|
||||||
let languageCode = utils.userLangToTimeagoCode(config.userLang);
|
|
||||||
if (!config.timeagoCodes.includes(languageCode + '-short')) {
|
|
||||||
languageCode = 'en';
|
|
||||||
}
|
|
||||||
|
|
||||||
const originalSettings = assign({}, jQuery.timeago.settings.strings);
|
|
||||||
adaptor.switchTimeagoLanguage(languageCode + '-short', function () {
|
|
||||||
adaptor.timeagoShort = assign({}, jQuery.timeago.settings.strings);
|
|
||||||
jQuery.timeago.settings.strings = assign({}, originalSettings);
|
|
||||||
toggle();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
toggle();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
switchTimeagoLanguage: function switchTimeagoLanguage(langCode, callback) {
|
|
||||||
// Delete the cached shorthand strings if present
|
|
||||||
delete adaptor.timeagoShort;
|
|
||||||
|
|
||||||
const stringsModule = 'timeago/locales/jquery.timeago.' + langCode;
|
|
||||||
// without undef, requirejs won't load the strings a second time
|
|
||||||
require.undef(stringsModule);
|
|
||||||
require([stringsModule], function () {
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
prepareDOM: function prepareDOM() {
|
|
||||||
// Add directional code if necessary
|
|
||||||
adaptor.translate('[[language:dir]]', function (value) {
|
|
||||||
if (value && !$('html').attr('data-dir')) {
|
|
||||||
jQuery('html').css('direction', value).attr('data-dir', value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return adaptor;
|
|
||||||
}));
|
|
||||||
|
|||||||
@@ -1,89 +1,88 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const translator = require('./modules/translator');
|
||||||
|
|
||||||
overrides = window.overrides || {};
|
window.overrides = window.overrides || {};
|
||||||
|
|
||||||
|
function translate(elements, type, str) {
|
||||||
|
return elements.each(function () {
|
||||||
|
var el = $(this);
|
||||||
|
translator.translate(str, function (translated) {
|
||||||
|
el[type](translated);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
(function ($) {
|
(function ($) {
|
||||||
require(['translator'], function (translator) {
|
$.fn.getCursorPosition = function () {
|
||||||
$.fn.getCursorPosition = function () {
|
const el = $(this).get(0);
|
||||||
const el = $(this).get(0);
|
let pos = 0;
|
||||||
let pos = 0;
|
if ('selectionStart' in el) {
|
||||||
if ('selectionStart' in el) {
|
pos = el.selectionStart;
|
||||||
pos = el.selectionStart;
|
} else if ('selection' in document) {
|
||||||
} else if ('selection' in document) {
|
el.focus();
|
||||||
el.focus();
|
const Sel = document.selection.createRange();
|
||||||
const Sel = document.selection.createRange();
|
const SelLength = document.selection.createRange().text.length;
|
||||||
const SelLength = document.selection.createRange().text.length;
|
Sel.moveStart('character', -el.value.length);
|
||||||
Sel.moveStart('character', -el.value.length);
|
pos = Sel.text.length - SelLength;
|
||||||
pos = Sel.text.length - SelLength;
|
|
||||||
}
|
|
||||||
return pos;
|
|
||||||
};
|
|
||||||
|
|
||||||
$.fn.selectRange = function (start, end) {
|
|
||||||
if (!end) {
|
|
||||||
end = start;
|
|
||||||
}
|
|
||||||
return this.each(function () {
|
|
||||||
if (this.setSelectionRange) {
|
|
||||||
this.focus();
|
|
||||||
this.setSelectionRange(start, end);
|
|
||||||
} else if (this.createTextRange) {
|
|
||||||
const range = this.createTextRange();
|
|
||||||
range.collapse(true);
|
|
||||||
range.moveEnd('character', end);
|
|
||||||
range.moveStart('character', start);
|
|
||||||
range.select();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// http://stackoverflow.com/questions/511088/use-javascript-to-place-cursor-at-end-of-text-in-text-input-element
|
|
||||||
$.fn.putCursorAtEnd = function () {
|
|
||||||
return this.each(function () {
|
|
||||||
$(this).focus();
|
|
||||||
|
|
||||||
if (this.setSelectionRange) {
|
|
||||||
const len = $(this).val().length * 2;
|
|
||||||
this.setSelectionRange(len, len);
|
|
||||||
} else {
|
|
||||||
$(this).val($(this).val());
|
|
||||||
}
|
|
||||||
this.scrollTop = 999999;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
$.fn.translateHtml = function (str) {
|
|
||||||
return translate(this, 'html', str);
|
|
||||||
};
|
|
||||||
|
|
||||||
$.fn.translateText = function (str) {
|
|
||||||
return translate(this, 'text', str);
|
|
||||||
};
|
|
||||||
|
|
||||||
$.fn.translateVal = function (str) {
|
|
||||||
return translate(this, 'val', str);
|
|
||||||
};
|
|
||||||
|
|
||||||
$.fn.translateAttr = function (attr, str) {
|
|
||||||
return this.each(function () {
|
|
||||||
const el = $(this);
|
|
||||||
translator.translate(str, function (translated) {
|
|
||||||
el.attr(attr, translated);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
function translate(elements, type, str) {
|
|
||||||
return elements.each(function () {
|
|
||||||
const el = $(this);
|
|
||||||
translator.translate(str, function (translated) {
|
|
||||||
el[type](translated);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
return pos;
|
||||||
|
};
|
||||||
|
|
||||||
|
$.fn.selectRange = function (start, end) {
|
||||||
|
if (!end) {
|
||||||
|
end = start;
|
||||||
|
}
|
||||||
|
return this.each(function () {
|
||||||
|
if (this.setSelectionRange) {
|
||||||
|
this.focus();
|
||||||
|
this.setSelectionRange(start, end);
|
||||||
|
} else if (this.createTextRange) {
|
||||||
|
const range = this.createTextRange();
|
||||||
|
range.collapse(true);
|
||||||
|
range.moveEnd('character', end);
|
||||||
|
range.moveStart('character', start);
|
||||||
|
range.select();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// http://stackoverflow.com/questions/511088/use-javascript-to-place-cursor-at-end-of-text-in-text-input-element
|
||||||
|
$.fn.putCursorAtEnd = function () {
|
||||||
|
return this.each(function () {
|
||||||
|
$(this).focus();
|
||||||
|
|
||||||
|
if (this.setSelectionRange) {
|
||||||
|
const len = $(this).val().length * 2;
|
||||||
|
this.setSelectionRange(len, len);
|
||||||
|
} else {
|
||||||
|
$(this).val($(this).val());
|
||||||
|
}
|
||||||
|
this.scrollTop = 999999;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$.fn.translateHtml = function (str) {
|
||||||
|
return translate(this, 'html', str);
|
||||||
|
};
|
||||||
|
|
||||||
|
$.fn.translateText = function (str) {
|
||||||
|
return translate(this, 'text', str);
|
||||||
|
};
|
||||||
|
|
||||||
|
$.fn.translateVal = function (str) {
|
||||||
|
return translate(this, 'val', str);
|
||||||
|
};
|
||||||
|
|
||||||
|
$.fn.translateAttr = function (attr, str) {
|
||||||
|
return this.each(function () {
|
||||||
|
const el = $(this);
|
||||||
|
translator.translate(str, function (translated) {
|
||||||
|
el.attr(attr, translated);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
}(jQuery || { fn: {} }));
|
}(jQuery || { fn: {} }));
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
require.config({
|
|
||||||
baseUrl: config.assetBaseUrl + '/src/modules',
|
|
||||||
waitSeconds: 0,
|
|
||||||
urlArgs: config['cache-buster'],
|
|
||||||
paths: {
|
|
||||||
forum: '../client',
|
|
||||||
admin: '../admin',
|
|
||||||
vendor: '../../vendor',
|
|
||||||
plugins: '../../plugins',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-redeclare
|
||||||
|
const io = require('socket.io-client');
|
||||||
|
// eslint-disable-next-line no-redeclare
|
||||||
|
const $ = require('jquery');
|
||||||
|
|
||||||
app = window.app || {};
|
app = window.app || {};
|
||||||
socket = window.socket;
|
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
let reconnecting = false;
|
let reconnecting = false;
|
||||||
@@ -14,7 +17,7 @@ socket = window.socket;
|
|||||||
path: config.relative_path + '/socket.io',
|
path: config.relative_path + '/socket.io',
|
||||||
};
|
};
|
||||||
|
|
||||||
socket = io(config.websocketAddress, ioParams);
|
window.socket = io(config.websocketAddress, ioParams);
|
||||||
|
|
||||||
const oEmit = socket.emit;
|
const oEmit = socket.emit;
|
||||||
socket.emit = function (event, data, callback) {
|
socket.emit = function (event, data, callback) {
|
||||||
|
|||||||
777
public/src/utils.common.js
Normal file
777
public/src/utils.common.js
Normal file
@@ -0,0 +1,777 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
|
||||||
|
// add default escape function for escaping HTML entities
|
||||||
|
const escapeCharMap = Object.freeze({
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": ''',
|
||||||
|
'`': '`',
|
||||||
|
'=': '=',
|
||||||
|
});
|
||||||
|
function replaceChar(c) {
|
||||||
|
return escapeCharMap[c];
|
||||||
|
}
|
||||||
|
const escapeChars = /[&<>"'`=]/g;
|
||||||
|
|
||||||
|
const HTMLEntities = Object.freeze({
|
||||||
|
amp: '&',
|
||||||
|
gt: '>',
|
||||||
|
lt: '<',
|
||||||
|
quot: '"',
|
||||||
|
apos: "'",
|
||||||
|
AElig: 198,
|
||||||
|
Aacute: 193,
|
||||||
|
Acirc: 194,
|
||||||
|
Agrave: 192,
|
||||||
|
Aring: 197,
|
||||||
|
Atilde: 195,
|
||||||
|
Auml: 196,
|
||||||
|
Ccedil: 199,
|
||||||
|
ETH: 208,
|
||||||
|
Eacute: 201,
|
||||||
|
Ecirc: 202,
|
||||||
|
Egrave: 200,
|
||||||
|
Euml: 203,
|
||||||
|
Iacute: 205,
|
||||||
|
Icirc: 206,
|
||||||
|
Igrave: 204,
|
||||||
|
Iuml: 207,
|
||||||
|
Ntilde: 209,
|
||||||
|
Oacute: 211,
|
||||||
|
Ocirc: 212,
|
||||||
|
Ograve: 210,
|
||||||
|
Oslash: 216,
|
||||||
|
Otilde: 213,
|
||||||
|
Ouml: 214,
|
||||||
|
THORN: 222,
|
||||||
|
Uacute: 218,
|
||||||
|
Ucirc: 219,
|
||||||
|
Ugrave: 217,
|
||||||
|
Uuml: 220,
|
||||||
|
Yacute: 221,
|
||||||
|
aacute: 225,
|
||||||
|
acirc: 226,
|
||||||
|
aelig: 230,
|
||||||
|
agrave: 224,
|
||||||
|
aring: 229,
|
||||||
|
atilde: 227,
|
||||||
|
auml: 228,
|
||||||
|
ccedil: 231,
|
||||||
|
eacute: 233,
|
||||||
|
ecirc: 234,
|
||||||
|
egrave: 232,
|
||||||
|
eth: 240,
|
||||||
|
euml: 235,
|
||||||
|
iacute: 237,
|
||||||
|
icirc: 238,
|
||||||
|
igrave: 236,
|
||||||
|
iuml: 239,
|
||||||
|
ntilde: 241,
|
||||||
|
oacute: 243,
|
||||||
|
ocirc: 244,
|
||||||
|
ograve: 242,
|
||||||
|
oslash: 248,
|
||||||
|
otilde: 245,
|
||||||
|
ouml: 246,
|
||||||
|
szlig: 223,
|
||||||
|
thorn: 254,
|
||||||
|
uacute: 250,
|
||||||
|
ucirc: 251,
|
||||||
|
ugrave: 249,
|
||||||
|
uuml: 252,
|
||||||
|
yacute: 253,
|
||||||
|
yuml: 255,
|
||||||
|
copy: 169,
|
||||||
|
reg: 174,
|
||||||
|
nbsp: 160,
|
||||||
|
iexcl: 161,
|
||||||
|
cent: 162,
|
||||||
|
pound: 163,
|
||||||
|
curren: 164,
|
||||||
|
yen: 165,
|
||||||
|
brvbar: 166,
|
||||||
|
sect: 167,
|
||||||
|
uml: 168,
|
||||||
|
ordf: 170,
|
||||||
|
laquo: 171,
|
||||||
|
not: 172,
|
||||||
|
shy: 173,
|
||||||
|
macr: 175,
|
||||||
|
deg: 176,
|
||||||
|
plusmn: 177,
|
||||||
|
sup1: 185,
|
||||||
|
sup2: 178,
|
||||||
|
sup3: 179,
|
||||||
|
acute: 180,
|
||||||
|
micro: 181,
|
||||||
|
para: 182,
|
||||||
|
middot: 183,
|
||||||
|
cedil: 184,
|
||||||
|
ordm: 186,
|
||||||
|
raquo: 187,
|
||||||
|
frac14: 188,
|
||||||
|
frac12: 189,
|
||||||
|
frac34: 190,
|
||||||
|
iquest: 191,
|
||||||
|
times: 215,
|
||||||
|
divide: 247,
|
||||||
|
'OElig;': 338,
|
||||||
|
'oelig;': 339,
|
||||||
|
'Scaron;': 352,
|
||||||
|
'scaron;': 353,
|
||||||
|
'Yuml;': 376,
|
||||||
|
'fnof;': 402,
|
||||||
|
'circ;': 710,
|
||||||
|
'tilde;': 732,
|
||||||
|
'Alpha;': 913,
|
||||||
|
'Beta;': 914,
|
||||||
|
'Gamma;': 915,
|
||||||
|
'Delta;': 916,
|
||||||
|
'Epsilon;': 917,
|
||||||
|
'Zeta;': 918,
|
||||||
|
'Eta;': 919,
|
||||||
|
'Theta;': 920,
|
||||||
|
'Iota;': 921,
|
||||||
|
'Kappa;': 922,
|
||||||
|
'Lambda;': 923,
|
||||||
|
'Mu;': 924,
|
||||||
|
'Nu;': 925,
|
||||||
|
'Xi;': 926,
|
||||||
|
'Omicron;': 927,
|
||||||
|
'Pi;': 928,
|
||||||
|
'Rho;': 929,
|
||||||
|
'Sigma;': 931,
|
||||||
|
'Tau;': 932,
|
||||||
|
'Upsilon;': 933,
|
||||||
|
'Phi;': 934,
|
||||||
|
'Chi;': 935,
|
||||||
|
'Psi;': 936,
|
||||||
|
'Omega;': 937,
|
||||||
|
'alpha;': 945,
|
||||||
|
'beta;': 946,
|
||||||
|
'gamma;': 947,
|
||||||
|
'delta;': 948,
|
||||||
|
'epsilon;': 949,
|
||||||
|
'zeta;': 950,
|
||||||
|
'eta;': 951,
|
||||||
|
'theta;': 952,
|
||||||
|
'iota;': 953,
|
||||||
|
'kappa;': 954,
|
||||||
|
'lambda;': 955,
|
||||||
|
'mu;': 956,
|
||||||
|
'nu;': 957,
|
||||||
|
'xi;': 958,
|
||||||
|
'omicron;': 959,
|
||||||
|
'pi;': 960,
|
||||||
|
'rho;': 961,
|
||||||
|
'sigmaf;': 962,
|
||||||
|
'sigma;': 963,
|
||||||
|
'tau;': 964,
|
||||||
|
'upsilon;': 965,
|
||||||
|
'phi;': 966,
|
||||||
|
'chi;': 967,
|
||||||
|
'psi;': 968,
|
||||||
|
'omega;': 969,
|
||||||
|
'thetasym;': 977,
|
||||||
|
'upsih;': 978,
|
||||||
|
'piv;': 982,
|
||||||
|
'ensp;': 8194,
|
||||||
|
'emsp;': 8195,
|
||||||
|
'thinsp;': 8201,
|
||||||
|
'zwnj;': 8204,
|
||||||
|
'zwj;': 8205,
|
||||||
|
'lrm;': 8206,
|
||||||
|
'rlm;': 8207,
|
||||||
|
'ndash;': 8211,
|
||||||
|
'mdash;': 8212,
|
||||||
|
'lsquo;': 8216,
|
||||||
|
'rsquo;': 8217,
|
||||||
|
'sbquo;': 8218,
|
||||||
|
'ldquo;': 8220,
|
||||||
|
'rdquo;': 8221,
|
||||||
|
'bdquo;': 8222,
|
||||||
|
'dagger;': 8224,
|
||||||
|
'Dagger;': 8225,
|
||||||
|
'bull;': 8226,
|
||||||
|
'hellip;': 8230,
|
||||||
|
'permil;': 8240,
|
||||||
|
'prime;': 8242,
|
||||||
|
'Prime;': 8243,
|
||||||
|
'lsaquo;': 8249,
|
||||||
|
'rsaquo;': 8250,
|
||||||
|
'oline;': 8254,
|
||||||
|
'frasl;': 8260,
|
||||||
|
'euro;': 8364,
|
||||||
|
'image;': 8465,
|
||||||
|
'weierp;': 8472,
|
||||||
|
'real;': 8476,
|
||||||
|
'trade;': 8482,
|
||||||
|
'alefsym;': 8501,
|
||||||
|
'larr;': 8592,
|
||||||
|
'uarr;': 8593,
|
||||||
|
'rarr;': 8594,
|
||||||
|
'darr;': 8595,
|
||||||
|
'harr;': 8596,
|
||||||
|
'crarr;': 8629,
|
||||||
|
'lArr;': 8656,
|
||||||
|
'uArr;': 8657,
|
||||||
|
'rArr;': 8658,
|
||||||
|
'dArr;': 8659,
|
||||||
|
'hArr;': 8660,
|
||||||
|
'forall;': 8704,
|
||||||
|
'part;': 8706,
|
||||||
|
'exist;': 8707,
|
||||||
|
'empty;': 8709,
|
||||||
|
'nabla;': 8711,
|
||||||
|
'isin;': 8712,
|
||||||
|
'notin;': 8713,
|
||||||
|
'ni;': 8715,
|
||||||
|
'prod;': 8719,
|
||||||
|
'sum;': 8721,
|
||||||
|
'minus;': 8722,
|
||||||
|
'lowast;': 8727,
|
||||||
|
'radic;': 8730,
|
||||||
|
'prop;': 8733,
|
||||||
|
'infin;': 8734,
|
||||||
|
'ang;': 8736,
|
||||||
|
'and;': 8743,
|
||||||
|
'or;': 8744,
|
||||||
|
'cap;': 8745,
|
||||||
|
'cup;': 8746,
|
||||||
|
'int;': 8747,
|
||||||
|
'there4;': 8756,
|
||||||
|
'sim;': 8764,
|
||||||
|
'cong;': 8773,
|
||||||
|
'asymp;': 8776,
|
||||||
|
'ne;': 8800,
|
||||||
|
'equiv;': 8801,
|
||||||
|
'le;': 8804,
|
||||||
|
'ge;': 8805,
|
||||||
|
'sub;': 8834,
|
||||||
|
'sup;': 8835,
|
||||||
|
'nsub;': 8836,
|
||||||
|
'sube;': 8838,
|
||||||
|
'supe;': 8839,
|
||||||
|
'oplus;': 8853,
|
||||||
|
'otimes;': 8855,
|
||||||
|
'perp;': 8869,
|
||||||
|
'sdot;': 8901,
|
||||||
|
'lceil;': 8968,
|
||||||
|
'rceil;': 8969,
|
||||||
|
'lfloor;': 8970,
|
||||||
|
'rfloor;': 8971,
|
||||||
|
'lang;': 9001,
|
||||||
|
'rang;': 9002,
|
||||||
|
'loz;': 9674,
|
||||||
|
'spades;': 9824,
|
||||||
|
'clubs;': 9827,
|
||||||
|
'hearts;': 9829,
|
||||||
|
'diams;': 9830,
|
||||||
|
});
|
||||||
|
|
||||||
|
/* eslint-disable no-redeclare */
|
||||||
|
const utils = {
|
||||||
|
generateUUID: function () {
|
||||||
|
/* eslint-disable no-bitwise */
|
||||||
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||||
|
const r = Math.random() * 16 | 0;
|
||||||
|
const v = c === 'x' ? r : ((r & 0x3) | 0x8);
|
||||||
|
return v.toString(16);
|
||||||
|
});
|
||||||
|
/* eslint-enable no-bitwise */
|
||||||
|
},
|
||||||
|
// https://github.com/substack/node-ent/blob/master/index.js
|
||||||
|
decodeHTMLEntities: function (html) {
|
||||||
|
return String(html)
|
||||||
|
.replace(/&#(\d+);?/g, function (_, code) {
|
||||||
|
return String.fromCharCode(code);
|
||||||
|
})
|
||||||
|
.replace(/&#[xX]([A-Fa-f0-9]+);?/g, function (_, hex) {
|
||||||
|
return String.fromCharCode(parseInt(hex, 16));
|
||||||
|
})
|
||||||
|
.replace(/&([^;\W]+;?)/g, function (m, e) {
|
||||||
|
const ee = e.replace(/;$/, '');
|
||||||
|
const target = HTMLEntities[e] || (e.match(/;$/) && HTMLEntities[ee]);
|
||||||
|
|
||||||
|
if (typeof target === 'number') {
|
||||||
|
return String.fromCharCode(target);
|
||||||
|
} else if (typeof target === 'string') {
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
return m;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// https://github.com/jprichardson/string.js/blob/master/lib/string.js
|
||||||
|
stripHTMLTags: function (str, tags) {
|
||||||
|
const pattern = (tags || ['']).join('|');
|
||||||
|
return String(str).replace(new RegExp('<(\\/)?(' + (pattern || '[^\\s>]+') + ')(\\s+[^<>]*?)?\\s*(\\/)?>', 'gi'), '');
|
||||||
|
},
|
||||||
|
|
||||||
|
cleanUpTag: function (tag, maxLength) {
|
||||||
|
if (typeof tag !== 'string' || !tag.length) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
tag = tag.trim().toLowerCase();
|
||||||
|
// see https://github.com/NodeBB/NodeBB/issues/4378
|
||||||
|
tag = tag.replace(/\u202E/gi, '');
|
||||||
|
tag = tag.replace(/[,/#!$^*;:{}=_`<>'"~()?|]/g, '');
|
||||||
|
tag = tag.substr(0, maxLength || 15).trim();
|
||||||
|
const matches = tag.match(/^[.-]*(.+?)[.-]*$/);
|
||||||
|
if (matches && matches.length > 1) {
|
||||||
|
tag = matches[1];
|
||||||
|
}
|
||||||
|
return tag;
|
||||||
|
},
|
||||||
|
|
||||||
|
removePunctuation: function (str) {
|
||||||
|
return str.replace(/[.,-/#!$%^&*;:{}=\-_`<>'"~()?]/g, '');
|
||||||
|
},
|
||||||
|
|
||||||
|
isEmailValid: function (email) {
|
||||||
|
return typeof email === 'string' && email.length && email.indexOf('@') !== -1 && email.indexOf(',') === -1 && email.indexOf(';') === -1;
|
||||||
|
},
|
||||||
|
|
||||||
|
isUserNameValid: function (name) {
|
||||||
|
return (name && name !== '' && (/^['" \-+.*[\]0-9\u00BF-\u1FFF\u2C00-\uD7FF\w]+$/.test(name)));
|
||||||
|
},
|
||||||
|
|
||||||
|
isPasswordValid: function (password) {
|
||||||
|
return typeof password === 'string' && password.length;
|
||||||
|
},
|
||||||
|
|
||||||
|
isNumber: function (n) {
|
||||||
|
// `isFinite('') === true` so isNan parseFloat check is necessary
|
||||||
|
return !isNaN(parseFloat(n)) && isFinite(n);
|
||||||
|
},
|
||||||
|
|
||||||
|
languageKeyRegex: /\[\[[\w]+:.+\]\]/,
|
||||||
|
hasLanguageKey: function (input) {
|
||||||
|
return utils.languageKeyRegex.test(input);
|
||||||
|
},
|
||||||
|
userLangToTimeagoCode: function (userLang) {
|
||||||
|
const mapping = {
|
||||||
|
'en-GB': 'en',
|
||||||
|
'en-US': 'en',
|
||||||
|
'fa-IR': 'fa',
|
||||||
|
'pt-BR': 'pt-br',
|
||||||
|
nb: 'no',
|
||||||
|
};
|
||||||
|
return mapping.hasOwnProperty(userLang) ? mapping[userLang] : userLang;
|
||||||
|
},
|
||||||
|
// shallow objects merge
|
||||||
|
merge: function () {
|
||||||
|
const result = {};
|
||||||
|
let obj;
|
||||||
|
let keys;
|
||||||
|
for (let i = 0; i < arguments.length; i += 1) {
|
||||||
|
obj = arguments[i] || {};
|
||||||
|
keys = Object.keys(obj);
|
||||||
|
for (let j = 0; j < keys.length; j += 1) {
|
||||||
|
result[keys[j]] = obj[keys[j]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
fileExtension: function (path) {
|
||||||
|
return ('' + path).split('.').pop();
|
||||||
|
},
|
||||||
|
|
||||||
|
extensionMimeTypeMap: {
|
||||||
|
bmp: 'image/bmp',
|
||||||
|
cmx: 'image/x-cmx',
|
||||||
|
cod: 'image/cis-cod',
|
||||||
|
gif: 'image/gif',
|
||||||
|
ico: 'image/x-icon',
|
||||||
|
ief: 'image/ief',
|
||||||
|
jfif: 'image/pipeg',
|
||||||
|
jpe: 'image/jpeg',
|
||||||
|
jpeg: 'image/jpeg',
|
||||||
|
jpg: 'image/jpeg',
|
||||||
|
png: 'image/png',
|
||||||
|
pbm: 'image/x-portable-bitmap',
|
||||||
|
pgm: 'image/x-portable-graymap',
|
||||||
|
pnm: 'image/x-portable-anymap',
|
||||||
|
ppm: 'image/x-portable-pixmap',
|
||||||
|
ras: 'image/x-cmu-raster',
|
||||||
|
rgb: 'image/x-rgb',
|
||||||
|
svg: 'image/svg+xml',
|
||||||
|
tif: 'image/tiff',
|
||||||
|
tiff: 'image/tiff',
|
||||||
|
xbm: 'image/x-xbitmap',
|
||||||
|
xpm: 'image/x-xpixmap',
|
||||||
|
xwd: 'image/x-xwindowdump',
|
||||||
|
},
|
||||||
|
|
||||||
|
fileMimeType: function (path) {
|
||||||
|
return utils.extensionToMimeType(utils.fileExtension(path));
|
||||||
|
},
|
||||||
|
|
||||||
|
extensionToMimeType: function (extension) {
|
||||||
|
return utils.extensionMimeTypeMap.hasOwnProperty(extension) ? utils.extensionMimeTypeMap[extension] : '*';
|
||||||
|
},
|
||||||
|
|
||||||
|
isPromise: function (object) {
|
||||||
|
// https://stackoverflow.com/questions/27746304/how-do-i-tell-if-an-object-is-a-promise#comment97339131_27746324
|
||||||
|
return object && typeof object.then === 'function';
|
||||||
|
},
|
||||||
|
|
||||||
|
promiseParallel: function (obj) {
|
||||||
|
const keys = Object.keys(obj);
|
||||||
|
return Promise.all(
|
||||||
|
keys.map(function (k) { return obj[k]; })
|
||||||
|
).then(function (results) {
|
||||||
|
const data = {};
|
||||||
|
keys.forEach(function (k, i) {
|
||||||
|
data[k] = results[i];
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// https://github.com/sindresorhus/is-absolute-url
|
||||||
|
isAbsoluteUrlRE: /^[a-zA-Z][a-zA-Z\d+\-.]*:/,
|
||||||
|
isWinPathRE: /^[a-zA-Z]:\\/,
|
||||||
|
isAbsoluteUrl: function (url) {
|
||||||
|
if (utils.isWinPathRE.test(url)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return utils.isAbsoluteUrlRE.test(url);
|
||||||
|
},
|
||||||
|
|
||||||
|
isRelativeUrl: function (url) {
|
||||||
|
return !utils.isAbsoluteUrl(url);
|
||||||
|
},
|
||||||
|
|
||||||
|
makeNumbersHumanReadable: function (elements) {
|
||||||
|
elements.each(function () {
|
||||||
|
$(this)
|
||||||
|
.html(utils.makeNumberHumanReadable($(this).attr('title')))
|
||||||
|
.removeClass('hidden');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
makeNumberHumanReadable: function (num) {
|
||||||
|
const n = parseInt(num, 10);
|
||||||
|
if (!n) {
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
if (n > 999999) {
|
||||||
|
return (n / 1000000).toFixed(1) + 'm';
|
||||||
|
} else if (n > 999) {
|
||||||
|
return (n / 1000).toFixed(1) + 'k';
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
},
|
||||||
|
|
||||||
|
addCommasToNumbers: function (elements) {
|
||||||
|
elements.each(function (index, element) {
|
||||||
|
$(element)
|
||||||
|
.html(utils.addCommas($(element).html()))
|
||||||
|
.removeClass('hidden');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// takes a string like 1000 and returns 1,000
|
||||||
|
addCommas: function (text) {
|
||||||
|
return String(text).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,');
|
||||||
|
},
|
||||||
|
|
||||||
|
toISOString: function (timestamp) {
|
||||||
|
if (!timestamp || !Date.prototype.toISOString) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent too-high values to be passed to Date object
|
||||||
|
timestamp = Math.min(timestamp, 8640000000000000);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return new Date(parseInt(timestamp, 10)).toISOString();
|
||||||
|
} catch (e) {
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
tags: ['a', 'abbr', 'acronym', 'address', 'applet', 'area', 'article', 'aside', 'audio', 'b', 'base', 'basefont',
|
||||||
|
'bdi', 'bdo', 'big', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup',
|
||||||
|
'command', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'em', 'embed',
|
||||||
|
'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
||||||
|
'head', 'header', 'hr', 'html', 'i', 'iframe', 'img', 'input', 'ins', 'kbd', 'keygen', 'label', 'legend', 'li', 'link',
|
||||||
|
'map', 'mark', 'menu', 'meta', 'meter', 'nav', 'noframes', 'noscript', 'object', 'ol', 'optgroup', 'option',
|
||||||
|
'output', 'p', 'param', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'script', 'section', 'select',
|
||||||
|
'small', 'source', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'textarea', 'tfoot',
|
||||||
|
'th', 'thead', 'time', 'title', 'tr', 'track', 'tt', 'u', 'ul', 'const', 'video', 'wbr'],
|
||||||
|
|
||||||
|
stripTags: ['abbr', 'acronym', 'address', 'applet', 'area', 'article', 'aside', 'audio', 'base', 'basefont',
|
||||||
|
'bdi', 'bdo', 'big', 'blink', 'body', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup',
|
||||||
|
'command', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'em', 'embed',
|
||||||
|
'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
||||||
|
'head', 'header', 'hr', 'html', 'iframe', 'input', 'ins', 'kbd', 'keygen', 'label', 'legend', 'li', 'link',
|
||||||
|
'map', 'mark', 'marquee', 'menu', 'meta', 'meter', 'nav', 'noframes', 'noscript', 'object', 'ol', 'optgroup', 'option',
|
||||||
|
'output', 'param', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'script', 'section', 'select',
|
||||||
|
'source', 'span', 'strike', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'textarea', 'tfoot',
|
||||||
|
'th', 'thead', 'time', 'title', 'tr', 'track', 'tt', 'u', 'ul', 'const', 'video', 'wbr'],
|
||||||
|
|
||||||
|
escapeRegexChars: function (text) {
|
||||||
|
return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
|
||||||
|
},
|
||||||
|
|
||||||
|
escapeHTML: function (str) {
|
||||||
|
if (str == null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (!str) {
|
||||||
|
return String(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
return str.toString().replace(escapeChars, replaceChar);
|
||||||
|
},
|
||||||
|
|
||||||
|
isAndroidBrowser: function () {
|
||||||
|
// http://stackoverflow.com/questions/9286355/how-to-detect-only-the-native-android-browser
|
||||||
|
const nua = navigator.userAgent;
|
||||||
|
return ((nua.indexOf('Mozilla/5.0') > -1 && nua.indexOf('Android ') > -1 && nua.indexOf('AppleWebKit') > -1) && !(nua.indexOf('Chrome') > -1));
|
||||||
|
},
|
||||||
|
|
||||||
|
isTouchDevice: function () {
|
||||||
|
return 'ontouchstart' in document.documentElement;
|
||||||
|
},
|
||||||
|
|
||||||
|
findBootstrapEnvironment: function () {
|
||||||
|
// http://stackoverflow.com/questions/14441456/how-to-detect-which-device-view-youre-on-using-twitter-bootstrap-api
|
||||||
|
const envs = ['xs', 'sm', 'md', 'lg'];
|
||||||
|
const $el = $('<div>');
|
||||||
|
|
||||||
|
$el.appendTo($('body'));
|
||||||
|
|
||||||
|
for (let i = envs.length - 1; i >= 0; i -= 1) {
|
||||||
|
const env = envs[i];
|
||||||
|
|
||||||
|
$el.addClass('hidden-' + env);
|
||||||
|
if ($el.is(':hidden')) {
|
||||||
|
$el.remove();
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
isMobile: function () {
|
||||||
|
const env = utils.findBootstrapEnvironment();
|
||||||
|
return ['xs', 'sm'].some(function (targetEnv) {
|
||||||
|
return targetEnv === env;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getHoursArray: function () {
|
||||||
|
const currentHour = new Date().getHours();
|
||||||
|
const labels = [];
|
||||||
|
|
||||||
|
for (let i = currentHour, ii = currentHour - 24; i > ii; i -= 1) {
|
||||||
|
const hour = i < 0 ? 24 + i : i;
|
||||||
|
labels.push(hour + ':00');
|
||||||
|
}
|
||||||
|
|
||||||
|
return labels.reverse();
|
||||||
|
},
|
||||||
|
|
||||||
|
getDaysArray: function (from, amount) {
|
||||||
|
const currentDay = new Date(parseInt(from, 10) || Date.now()).getTime();
|
||||||
|
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||||
|
const labels = [];
|
||||||
|
let tmpDate;
|
||||||
|
|
||||||
|
for (let x = (amount || 30) - 1; x >= 0; x -= 1) {
|
||||||
|
tmpDate = new Date(currentDay - (1000 * 60 * 60 * 24 * x));
|
||||||
|
labels.push(months[tmpDate.getMonth()] + ' ' + tmpDate.getDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
return labels;
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Retrieved from http://stackoverflow.com/a/7557433 @ 27 Mar 2016 */
|
||||||
|
isElementInViewport: function (el) {
|
||||||
|
// special bonus for those using jQuery
|
||||||
|
if (typeof jQuery === 'function' && el instanceof jQuery) {
|
||||||
|
el = el[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
|
||||||
|
return (
|
||||||
|
rect.top >= 0 &&
|
||||||
|
rect.left >= 0 &&
|
||||||
|
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /* or $(window).height() */
|
||||||
|
rect.right <= (window.innerWidth || document.documentElement.clientWidth) /* or $(window).width() */
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
// get all the url params in a single key/value hash
|
||||||
|
params: function (options) {
|
||||||
|
const hash = {};
|
||||||
|
|
||||||
|
options = options || {};
|
||||||
|
options.skipToType = options.skipToType || {};
|
||||||
|
|
||||||
|
let searchStr = window.location.search;
|
||||||
|
if (options.hasOwnProperty('url')) {
|
||||||
|
if (options.url) {
|
||||||
|
const a = utils.urlToLocation(options.url);
|
||||||
|
searchStr = a ? a.search : '';
|
||||||
|
} else {
|
||||||
|
searchStr = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const params = searchStr.substring(1).split('&');
|
||||||
|
|
||||||
|
params.forEach(function (param) {
|
||||||
|
const val = param.split('=');
|
||||||
|
let key = decodeURIComponent(val[0]);
|
||||||
|
const value = (
|
||||||
|
options.disableToType ||
|
||||||
|
options.skipToType[key] ? decodeURIComponent(val[1]) : utils.toType(decodeURIComponent(val[1]))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (key) {
|
||||||
|
if (key.substr(-2, 2) === '[]') {
|
||||||
|
key = key.slice(0, -2);
|
||||||
|
}
|
||||||
|
if (!hash[key]) {
|
||||||
|
hash[key] = value;
|
||||||
|
} else {
|
||||||
|
if (!Array.isArray(hash[key])) {
|
||||||
|
hash[key] = [hash[key]];
|
||||||
|
}
|
||||||
|
hash[key].push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return hash;
|
||||||
|
},
|
||||||
|
|
||||||
|
param: function (key) {
|
||||||
|
return this.params()[key];
|
||||||
|
},
|
||||||
|
|
||||||
|
urlToLocation: function (url) {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
return a;
|
||||||
|
},
|
||||||
|
|
||||||
|
// return boolean if string 'true' or string 'false', or if a parsable string which is a number
|
||||||
|
// also supports JSON object and/or arrays parsing
|
||||||
|
toType: function (str) {
|
||||||
|
const type = typeof str;
|
||||||
|
if (type !== 'string') {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
const nb = parseFloat(str);
|
||||||
|
if (!isNaN(nb) && isFinite(str)) {
|
||||||
|
return nb;
|
||||||
|
}
|
||||||
|
if (str === 'false') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (str === 'true') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
str = JSON.parse(str);
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
return str;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Safely get/set chained properties on an object
|
||||||
|
// set example: utils.props(A, 'a.b.c.d', 10) // sets A to {a: {b: {c: {d: 10}}}}, and returns 10
|
||||||
|
// get example: utils.props(A, 'a.b.c') // returns {d: 10}
|
||||||
|
// get example: utils.props(A, 'a.b.c.foo.bar') // returns undefined without throwing a TypeError
|
||||||
|
// credits to github.com/gkindel
|
||||||
|
props: function (obj, props, value) {
|
||||||
|
if (obj === undefined) {
|
||||||
|
obj = window;
|
||||||
|
}
|
||||||
|
if (props == null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const i = props.indexOf('.');
|
||||||
|
if (i === -1) {
|
||||||
|
if (value !== undefined) {
|
||||||
|
obj[props] = value;
|
||||||
|
}
|
||||||
|
return obj[props];
|
||||||
|
}
|
||||||
|
const prop = props.slice(0, i);
|
||||||
|
const newProps = props.slice(i + 1);
|
||||||
|
|
||||||
|
if (props !== undefined && !(obj[prop] instanceof Object)) {
|
||||||
|
obj[prop] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return utils.props(obj[prop], newProps, value);
|
||||||
|
},
|
||||||
|
|
||||||
|
isInternalURI: function (targetLocation, referenceLocation, relative_path) {
|
||||||
|
return targetLocation.host === '' || // Relative paths are always internal links
|
||||||
|
(
|
||||||
|
targetLocation.host === referenceLocation.host &&
|
||||||
|
// Otherwise need to check if protocol and host match
|
||||||
|
targetLocation.protocol === referenceLocation.protocol &&
|
||||||
|
// Subfolder installs need this additional check
|
||||||
|
(relative_path.length > 0 ? targetLocation.pathname.indexOf(relative_path) === 0 : true)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
rtrim: function (str) {
|
||||||
|
return str.replace(/\s+$/g, '');
|
||||||
|
},
|
||||||
|
|
||||||
|
debounce: function (func, wait, immediate) {
|
||||||
|
// modified from https://davidwalsh.name/javascript-debounce-function
|
||||||
|
let timeout;
|
||||||
|
return function () {
|
||||||
|
const context = this;
|
||||||
|
const args = arguments;
|
||||||
|
const later = function () {
|
||||||
|
timeout = null;
|
||||||
|
if (!immediate) {
|
||||||
|
func.apply(context, args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const callNow = immediate && !timeout;
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(later, wait);
|
||||||
|
if (callNow) {
|
||||||
|
func.apply(context, args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
throttle: function (func, wait, immediate) {
|
||||||
|
let timeout;
|
||||||
|
return function () {
|
||||||
|
const context = this;
|
||||||
|
const args = arguments;
|
||||||
|
const later = function () {
|
||||||
|
timeout = null;
|
||||||
|
if (!immediate) {
|
||||||
|
func.apply(context, args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const callNow = immediate && !timeout;
|
||||||
|
if (!timeout) {
|
||||||
|
timeout = setTimeout(later, wait);
|
||||||
|
}
|
||||||
|
if (callNow) {
|
||||||
|
func.apply(context, args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = utils;
|
||||||
@@ -1,793 +1,58 @@
|
|||||||
|
/* eslint-disable no-redeclare */
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
(function (factory) {
|
const $ = require('jquery');
|
||||||
if (typeof module === 'object' && module.exports) {
|
const utils = require('./utils.common');
|
||||||
module.exports = factory();
|
|
||||||
|
|
||||||
process.profile = function (operation, start) {
|
utils.getLanguage = function () {
|
||||||
console.log('%s took %d milliseconds', operation, process.elapsedTimeSince(start));
|
let lang = 'en-GB';
|
||||||
};
|
if (typeof window === 'object' && window.config && window.utils) {
|
||||||
|
lang = utils.params().lang || config.userLang || config.defaultLang || 'en-GB';
|
||||||
process.elapsedTimeSince = function (start) {
|
|
||||||
const diff = process.hrtime(start);
|
|
||||||
return (diff[0] * 1e3) + (diff[1] / 1e6);
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
window.utils = factory();
|
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line
|
return lang;
|
||||||
}(function () {
|
};
|
||||||
// add default escape function for escaping HTML entities
|
|
||||||
const escapeCharMap = Object.freeze({
|
|
||||||
'&': '&',
|
utils.makeNumbersHumanReadable = function (elements) {
|
||||||
'<': '<',
|
elements.each(function () {
|
||||||
'>': '>',
|
$(this)
|
||||||
'"': '"',
|
.html(utils.makeNumberHumanReadable($(this).attr('title')))
|
||||||
"'": ''',
|
.removeClass('hidden');
|
||||||
'`': '`',
|
|
||||||
'=': '=',
|
|
||||||
});
|
});
|
||||||
function replaceChar(c) {
|
};
|
||||||
return escapeCharMap[c];
|
|
||||||
|
utils.addCommasToNumbers = function (elements) {
|
||||||
|
elements.each(function (index, element) {
|
||||||
|
$(element)
|
||||||
|
.html(utils.addCommas($(element).html()))
|
||||||
|
.removeClass('hidden');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
utils.findBootstrapEnvironment = function () {
|
||||||
|
// http://stackoverflow.com/questions/14441456/how-to-detect-which-device-view-youre-on-using-twitter-bootstrap-api
|
||||||
|
const envs = ['xs', 'sm', 'md', 'lg'];
|
||||||
|
const $el = $('<div>');
|
||||||
|
|
||||||
|
$el.appendTo($('body'));
|
||||||
|
|
||||||
|
for (let i = envs.length - 1; i >= 0; i -= 1) {
|
||||||
|
const env = envs[i];
|
||||||
|
|
||||||
|
$el.addClass('hidden-' + env);
|
||||||
|
if ($el.is(':hidden')) {
|
||||||
|
$el.remove();
|
||||||
|
return env;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const escapeChars = /[&<>"'`=]/g;
|
};
|
||||||
|
|
||||||
const HTMLEntities = Object.freeze({
|
utils.isMobile = function () {
|
||||||
amp: '&',
|
const env = utils.findBootstrapEnvironment();
|
||||||
gt: '>',
|
return ['xs', 'sm'].some(function (targetEnv) {
|
||||||
lt: '<',
|
return targetEnv === env;
|
||||||
quot: '"',
|
|
||||||
apos: "'",
|
|
||||||
AElig: 198,
|
|
||||||
Aacute: 193,
|
|
||||||
Acirc: 194,
|
|
||||||
Agrave: 192,
|
|
||||||
Aring: 197,
|
|
||||||
Atilde: 195,
|
|
||||||
Auml: 196,
|
|
||||||
Ccedil: 199,
|
|
||||||
ETH: 208,
|
|
||||||
Eacute: 201,
|
|
||||||
Ecirc: 202,
|
|
||||||
Egrave: 200,
|
|
||||||
Euml: 203,
|
|
||||||
Iacute: 205,
|
|
||||||
Icirc: 206,
|
|
||||||
Igrave: 204,
|
|
||||||
Iuml: 207,
|
|
||||||
Ntilde: 209,
|
|
||||||
Oacute: 211,
|
|
||||||
Ocirc: 212,
|
|
||||||
Ograve: 210,
|
|
||||||
Oslash: 216,
|
|
||||||
Otilde: 213,
|
|
||||||
Ouml: 214,
|
|
||||||
THORN: 222,
|
|
||||||
Uacute: 218,
|
|
||||||
Ucirc: 219,
|
|
||||||
Ugrave: 217,
|
|
||||||
Uuml: 220,
|
|
||||||
Yacute: 221,
|
|
||||||
aacute: 225,
|
|
||||||
acirc: 226,
|
|
||||||
aelig: 230,
|
|
||||||
agrave: 224,
|
|
||||||
aring: 229,
|
|
||||||
atilde: 227,
|
|
||||||
auml: 228,
|
|
||||||
ccedil: 231,
|
|
||||||
eacute: 233,
|
|
||||||
ecirc: 234,
|
|
||||||
egrave: 232,
|
|
||||||
eth: 240,
|
|
||||||
euml: 235,
|
|
||||||
iacute: 237,
|
|
||||||
icirc: 238,
|
|
||||||
igrave: 236,
|
|
||||||
iuml: 239,
|
|
||||||
ntilde: 241,
|
|
||||||
oacute: 243,
|
|
||||||
ocirc: 244,
|
|
||||||
ograve: 242,
|
|
||||||
oslash: 248,
|
|
||||||
otilde: 245,
|
|
||||||
ouml: 246,
|
|
||||||
szlig: 223,
|
|
||||||
thorn: 254,
|
|
||||||
uacute: 250,
|
|
||||||
ucirc: 251,
|
|
||||||
ugrave: 249,
|
|
||||||
uuml: 252,
|
|
||||||
yacute: 253,
|
|
||||||
yuml: 255,
|
|
||||||
copy: 169,
|
|
||||||
reg: 174,
|
|
||||||
nbsp: 160,
|
|
||||||
iexcl: 161,
|
|
||||||
cent: 162,
|
|
||||||
pound: 163,
|
|
||||||
curren: 164,
|
|
||||||
yen: 165,
|
|
||||||
brvbar: 166,
|
|
||||||
sect: 167,
|
|
||||||
uml: 168,
|
|
||||||
ordf: 170,
|
|
||||||
laquo: 171,
|
|
||||||
not: 172,
|
|
||||||
shy: 173,
|
|
||||||
macr: 175,
|
|
||||||
deg: 176,
|
|
||||||
plusmn: 177,
|
|
||||||
sup1: 185,
|
|
||||||
sup2: 178,
|
|
||||||
sup3: 179,
|
|
||||||
acute: 180,
|
|
||||||
micro: 181,
|
|
||||||
para: 182,
|
|
||||||
middot: 183,
|
|
||||||
cedil: 184,
|
|
||||||
ordm: 186,
|
|
||||||
raquo: 187,
|
|
||||||
frac14: 188,
|
|
||||||
frac12: 189,
|
|
||||||
frac34: 190,
|
|
||||||
iquest: 191,
|
|
||||||
times: 215,
|
|
||||||
divide: 247,
|
|
||||||
'OElig;': 338,
|
|
||||||
'oelig;': 339,
|
|
||||||
'Scaron;': 352,
|
|
||||||
'scaron;': 353,
|
|
||||||
'Yuml;': 376,
|
|
||||||
'fnof;': 402,
|
|
||||||
'circ;': 710,
|
|
||||||
'tilde;': 732,
|
|
||||||
'Alpha;': 913,
|
|
||||||
'Beta;': 914,
|
|
||||||
'Gamma;': 915,
|
|
||||||
'Delta;': 916,
|
|
||||||
'Epsilon;': 917,
|
|
||||||
'Zeta;': 918,
|
|
||||||
'Eta;': 919,
|
|
||||||
'Theta;': 920,
|
|
||||||
'Iota;': 921,
|
|
||||||
'Kappa;': 922,
|
|
||||||
'Lambda;': 923,
|
|
||||||
'Mu;': 924,
|
|
||||||
'Nu;': 925,
|
|
||||||
'Xi;': 926,
|
|
||||||
'Omicron;': 927,
|
|
||||||
'Pi;': 928,
|
|
||||||
'Rho;': 929,
|
|
||||||
'Sigma;': 931,
|
|
||||||
'Tau;': 932,
|
|
||||||
'Upsilon;': 933,
|
|
||||||
'Phi;': 934,
|
|
||||||
'Chi;': 935,
|
|
||||||
'Psi;': 936,
|
|
||||||
'Omega;': 937,
|
|
||||||
'alpha;': 945,
|
|
||||||
'beta;': 946,
|
|
||||||
'gamma;': 947,
|
|
||||||
'delta;': 948,
|
|
||||||
'epsilon;': 949,
|
|
||||||
'zeta;': 950,
|
|
||||||
'eta;': 951,
|
|
||||||
'theta;': 952,
|
|
||||||
'iota;': 953,
|
|
||||||
'kappa;': 954,
|
|
||||||
'lambda;': 955,
|
|
||||||
'mu;': 956,
|
|
||||||
'nu;': 957,
|
|
||||||
'xi;': 958,
|
|
||||||
'omicron;': 959,
|
|
||||||
'pi;': 960,
|
|
||||||
'rho;': 961,
|
|
||||||
'sigmaf;': 962,
|
|
||||||
'sigma;': 963,
|
|
||||||
'tau;': 964,
|
|
||||||
'upsilon;': 965,
|
|
||||||
'phi;': 966,
|
|
||||||
'chi;': 967,
|
|
||||||
'psi;': 968,
|
|
||||||
'omega;': 969,
|
|
||||||
'thetasym;': 977,
|
|
||||||
'upsih;': 978,
|
|
||||||
'piv;': 982,
|
|
||||||
'ensp;': 8194,
|
|
||||||
'emsp;': 8195,
|
|
||||||
'thinsp;': 8201,
|
|
||||||
'zwnj;': 8204,
|
|
||||||
'zwj;': 8205,
|
|
||||||
'lrm;': 8206,
|
|
||||||
'rlm;': 8207,
|
|
||||||
'ndash;': 8211,
|
|
||||||
'mdash;': 8212,
|
|
||||||
'lsquo;': 8216,
|
|
||||||
'rsquo;': 8217,
|
|
||||||
'sbquo;': 8218,
|
|
||||||
'ldquo;': 8220,
|
|
||||||
'rdquo;': 8221,
|
|
||||||
'bdquo;': 8222,
|
|
||||||
'dagger;': 8224,
|
|
||||||
'Dagger;': 8225,
|
|
||||||
'bull;': 8226,
|
|
||||||
'hellip;': 8230,
|
|
||||||
'permil;': 8240,
|
|
||||||
'prime;': 8242,
|
|
||||||
'Prime;': 8243,
|
|
||||||
'lsaquo;': 8249,
|
|
||||||
'rsaquo;': 8250,
|
|
||||||
'oline;': 8254,
|
|
||||||
'frasl;': 8260,
|
|
||||||
'euro;': 8364,
|
|
||||||
'image;': 8465,
|
|
||||||
'weierp;': 8472,
|
|
||||||
'real;': 8476,
|
|
||||||
'trade;': 8482,
|
|
||||||
'alefsym;': 8501,
|
|
||||||
'larr;': 8592,
|
|
||||||
'uarr;': 8593,
|
|
||||||
'rarr;': 8594,
|
|
||||||
'darr;': 8595,
|
|
||||||
'harr;': 8596,
|
|
||||||
'crarr;': 8629,
|
|
||||||
'lArr;': 8656,
|
|
||||||
'uArr;': 8657,
|
|
||||||
'rArr;': 8658,
|
|
||||||
'dArr;': 8659,
|
|
||||||
'hArr;': 8660,
|
|
||||||
'forall;': 8704,
|
|
||||||
'part;': 8706,
|
|
||||||
'exist;': 8707,
|
|
||||||
'empty;': 8709,
|
|
||||||
'nabla;': 8711,
|
|
||||||
'isin;': 8712,
|
|
||||||
'notin;': 8713,
|
|
||||||
'ni;': 8715,
|
|
||||||
'prod;': 8719,
|
|
||||||
'sum;': 8721,
|
|
||||||
'minus;': 8722,
|
|
||||||
'lowast;': 8727,
|
|
||||||
'radic;': 8730,
|
|
||||||
'prop;': 8733,
|
|
||||||
'infin;': 8734,
|
|
||||||
'ang;': 8736,
|
|
||||||
'and;': 8743,
|
|
||||||
'or;': 8744,
|
|
||||||
'cap;': 8745,
|
|
||||||
'cup;': 8746,
|
|
||||||
'int;': 8747,
|
|
||||||
'there4;': 8756,
|
|
||||||
'sim;': 8764,
|
|
||||||
'cong;': 8773,
|
|
||||||
'asymp;': 8776,
|
|
||||||
'ne;': 8800,
|
|
||||||
'equiv;': 8801,
|
|
||||||
'le;': 8804,
|
|
||||||
'ge;': 8805,
|
|
||||||
'sub;': 8834,
|
|
||||||
'sup;': 8835,
|
|
||||||
'nsub;': 8836,
|
|
||||||
'sube;': 8838,
|
|
||||||
'supe;': 8839,
|
|
||||||
'oplus;': 8853,
|
|
||||||
'otimes;': 8855,
|
|
||||||
'perp;': 8869,
|
|
||||||
'sdot;': 8901,
|
|
||||||
'lceil;': 8968,
|
|
||||||
'rceil;': 8969,
|
|
||||||
'lfloor;': 8970,
|
|
||||||
'rfloor;': 8971,
|
|
||||||
'lang;': 9001,
|
|
||||||
'rang;': 9002,
|
|
||||||
'loz;': 9674,
|
|
||||||
'spades;': 9824,
|
|
||||||
'clubs;': 9827,
|
|
||||||
'hearts;': 9829,
|
|
||||||
'diams;': 9830,
|
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const utils = {
|
module.exports = utils;
|
||||||
generateUUID: function () {
|
|
||||||
/* eslint-disable no-bitwise */
|
|
||||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
||||||
const r = Math.random() * 16 | 0;
|
|
||||||
const v = c === 'x' ? r : ((r & 0x3) | 0x8);
|
|
||||||
return v.toString(16);
|
|
||||||
});
|
|
||||||
/* eslint-enable no-bitwise */
|
|
||||||
},
|
|
||||||
// https://github.com/substack/node-ent/blob/master/index.js
|
|
||||||
decodeHTMLEntities: function (html) {
|
|
||||||
return String(html)
|
|
||||||
.replace(/&#(\d+);?/g, function (_, code) {
|
|
||||||
return String.fromCharCode(code);
|
|
||||||
})
|
|
||||||
.replace(/&#[xX]([A-Fa-f0-9]+);?/g, function (_, hex) {
|
|
||||||
return String.fromCharCode(parseInt(hex, 16));
|
|
||||||
})
|
|
||||||
.replace(/&([^;\W]+;?)/g, function (m, e) {
|
|
||||||
const ee = e.replace(/;$/, '');
|
|
||||||
const target = HTMLEntities[e] || (e.match(/;$/) && HTMLEntities[ee]);
|
|
||||||
|
|
||||||
if (typeof target === 'number') {
|
|
||||||
return String.fromCharCode(target);
|
|
||||||
} else if (typeof target === 'string') {
|
|
||||||
return target;
|
|
||||||
}
|
|
||||||
|
|
||||||
return m;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
// https://github.com/jprichardson/string.js/blob/master/lib/string.js
|
|
||||||
stripHTMLTags: function (str, tags) {
|
|
||||||
const pattern = (tags || ['']).join('|');
|
|
||||||
return String(str).replace(new RegExp('<(\\/)?(' + (pattern || '[^\\s>]+') + ')(\\s+[^<>]*?)?\\s*(\\/)?>', 'gi'), '');
|
|
||||||
},
|
|
||||||
|
|
||||||
cleanUpTag: function (tag, maxLength) {
|
|
||||||
if (typeof tag !== 'string' || !tag.length) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
tag = tag.trim().toLowerCase();
|
|
||||||
// see https://github.com/NodeBB/NodeBB/issues/4378
|
|
||||||
tag = tag.replace(/\u202E/gi, '');
|
|
||||||
tag = tag.replace(/[,/#!$^*;:{}=_`<>'"~()?|]/g, '');
|
|
||||||
tag = tag.substr(0, maxLength || 15).trim();
|
|
||||||
const matches = tag.match(/^[.-]*(.+?)[.-]*$/);
|
|
||||||
if (matches && matches.length > 1) {
|
|
||||||
tag = matches[1];
|
|
||||||
}
|
|
||||||
return tag;
|
|
||||||
},
|
|
||||||
|
|
||||||
removePunctuation: function (str) {
|
|
||||||
return str.replace(/[.,-/#!$%^&*;:{}=\-_`<>'"~()?]/g, '');
|
|
||||||
},
|
|
||||||
|
|
||||||
isEmailValid: function (email) {
|
|
||||||
return typeof email === 'string' && email.length && email.indexOf('@') !== -1 && email.indexOf(',') === -1 && email.indexOf(';') === -1;
|
|
||||||
},
|
|
||||||
|
|
||||||
isUserNameValid: function (name) {
|
|
||||||
return (name && name !== '' && (/^['" \-+.*[\]0-9\u00BF-\u1FFF\u2C00-\uD7FF\w]+$/.test(name)));
|
|
||||||
},
|
|
||||||
|
|
||||||
isPasswordValid: function (password) {
|
|
||||||
return typeof password === 'string' && password.length;
|
|
||||||
},
|
|
||||||
|
|
||||||
isNumber: function (n) {
|
|
||||||
// `isFinite('') === true` so isNan parseFloat check is necessary
|
|
||||||
return !isNaN(parseFloat(n)) && isFinite(n);
|
|
||||||
},
|
|
||||||
|
|
||||||
languageKeyRegex: /\[\[[\w]+:.+\]\]/,
|
|
||||||
hasLanguageKey: function (input) {
|
|
||||||
return utils.languageKeyRegex.test(input);
|
|
||||||
},
|
|
||||||
userLangToTimeagoCode: function (userLang) {
|
|
||||||
const mapping = {
|
|
||||||
'en-GB': 'en',
|
|
||||||
'en-US': 'en',
|
|
||||||
'fa-IR': 'fa',
|
|
||||||
'pt-BR': 'pt-br',
|
|
||||||
nb: 'no',
|
|
||||||
};
|
|
||||||
return mapping.hasOwnProperty(userLang) ? mapping[userLang] : userLang;
|
|
||||||
},
|
|
||||||
// shallow objects merge
|
|
||||||
merge: function () {
|
|
||||||
const result = {};
|
|
||||||
let obj;
|
|
||||||
let keys;
|
|
||||||
for (let i = 0; i < arguments.length; i += 1) {
|
|
||||||
obj = arguments[i] || {};
|
|
||||||
keys = Object.keys(obj);
|
|
||||||
for (let j = 0; j < keys.length; j += 1) {
|
|
||||||
result[keys[j]] = obj[keys[j]];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
|
|
||||||
fileExtension: function (path) {
|
|
||||||
return ('' + path).split('.').pop();
|
|
||||||
},
|
|
||||||
|
|
||||||
extensionMimeTypeMap: {
|
|
||||||
bmp: 'image/bmp',
|
|
||||||
cmx: 'image/x-cmx',
|
|
||||||
cod: 'image/cis-cod',
|
|
||||||
gif: 'image/gif',
|
|
||||||
ico: 'image/x-icon',
|
|
||||||
ief: 'image/ief',
|
|
||||||
jfif: 'image/pipeg',
|
|
||||||
jpe: 'image/jpeg',
|
|
||||||
jpeg: 'image/jpeg',
|
|
||||||
jpg: 'image/jpeg',
|
|
||||||
png: 'image/png',
|
|
||||||
pbm: 'image/x-portable-bitmap',
|
|
||||||
pgm: 'image/x-portable-graymap',
|
|
||||||
pnm: 'image/x-portable-anymap',
|
|
||||||
ppm: 'image/x-portable-pixmap',
|
|
||||||
ras: 'image/x-cmu-raster',
|
|
||||||
rgb: 'image/x-rgb',
|
|
||||||
svg: 'image/svg+xml',
|
|
||||||
tif: 'image/tiff',
|
|
||||||
tiff: 'image/tiff',
|
|
||||||
xbm: 'image/x-xbitmap',
|
|
||||||
xpm: 'image/x-xpixmap',
|
|
||||||
xwd: 'image/x-xwindowdump',
|
|
||||||
},
|
|
||||||
|
|
||||||
fileMimeType: function (path) {
|
|
||||||
return utils.extensionToMimeType(utils.fileExtension(path));
|
|
||||||
},
|
|
||||||
|
|
||||||
extensionToMimeType: function (extension) {
|
|
||||||
return utils.extensionMimeTypeMap.hasOwnProperty(extension) ? utils.extensionMimeTypeMap[extension] : '*';
|
|
||||||
},
|
|
||||||
|
|
||||||
isPromise: function (object) {
|
|
||||||
// https://stackoverflow.com/questions/27746304/how-do-i-tell-if-an-object-is-a-promise#comment97339131_27746324
|
|
||||||
return object && typeof object.then === 'function';
|
|
||||||
},
|
|
||||||
|
|
||||||
promiseParallel: function (obj) {
|
|
||||||
const keys = Object.keys(obj);
|
|
||||||
return Promise.all(
|
|
||||||
keys.map(function (k) { return obj[k]; })
|
|
||||||
).then(function (results) {
|
|
||||||
const data = {};
|
|
||||||
keys.forEach(function (k, i) {
|
|
||||||
data[k] = results[i];
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// https://github.com/sindresorhus/is-absolute-url
|
|
||||||
isAbsoluteUrlRE: /^[a-zA-Z][a-zA-Z\d+\-.]*:/,
|
|
||||||
isWinPathRE: /^[a-zA-Z]:\\/,
|
|
||||||
isAbsoluteUrl: function (url) {
|
|
||||||
if (utils.isWinPathRE.test(url)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return utils.isAbsoluteUrlRE.test(url);
|
|
||||||
},
|
|
||||||
|
|
||||||
isRelativeUrl: function (url) {
|
|
||||||
return !utils.isAbsoluteUrl(url);
|
|
||||||
},
|
|
||||||
|
|
||||||
makeNumbersHumanReadable: function (elements) {
|
|
||||||
elements.each(function () {
|
|
||||||
$(this)
|
|
||||||
.html(utils.makeNumberHumanReadable($(this).attr('title')))
|
|
||||||
.removeClass('hidden');
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
makeNumberHumanReadable: function (num) {
|
|
||||||
const n = parseInt(num, 10);
|
|
||||||
if (!n) {
|
|
||||||
return num;
|
|
||||||
}
|
|
||||||
if (n > 999999) {
|
|
||||||
return (n / 1000000).toFixed(1) + 'm';
|
|
||||||
} else if (n > 999) {
|
|
||||||
return (n / 1000).toFixed(1) + 'k';
|
|
||||||
}
|
|
||||||
return n;
|
|
||||||
},
|
|
||||||
|
|
||||||
addCommasToNumbers: function (elements) {
|
|
||||||
elements.each(function (index, element) {
|
|
||||||
$(element)
|
|
||||||
.html(utils.addCommas($(element).html()))
|
|
||||||
.removeClass('hidden');
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// takes a string like 1000 and returns 1,000
|
|
||||||
addCommas: function (text) {
|
|
||||||
return String(text).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,');
|
|
||||||
},
|
|
||||||
|
|
||||||
toISOString: function (timestamp) {
|
|
||||||
if (!timestamp || !Date.prototype.toISOString) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent too-high values to be passed to Date object
|
|
||||||
timestamp = Math.min(timestamp, 8640000000000000);
|
|
||||||
|
|
||||||
try {
|
|
||||||
return new Date(parseInt(timestamp, 10)).toISOString();
|
|
||||||
} catch (e) {
|
|
||||||
return timestamp;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
tags: ['a', 'abbr', 'acronym', 'address', 'applet', 'area', 'article', 'aside', 'audio', 'b', 'base', 'basefont',
|
|
||||||
'bdi', 'bdo', 'big', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup',
|
|
||||||
'command', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'em', 'embed',
|
|
||||||
'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
|
||||||
'head', 'header', 'hr', 'html', 'i', 'iframe', 'img', 'input', 'ins', 'kbd', 'keygen', 'label', 'legend', 'li', 'link',
|
|
||||||
'map', 'mark', 'menu', 'meta', 'meter', 'nav', 'noframes', 'noscript', 'object', 'ol', 'optgroup', 'option',
|
|
||||||
'output', 'p', 'param', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'script', 'section', 'select',
|
|
||||||
'small', 'source', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'textarea', 'tfoot',
|
|
||||||
'th', 'thead', 'time', 'title', 'tr', 'track', 'tt', 'u', 'ul', 'const', 'video', 'wbr'],
|
|
||||||
|
|
||||||
stripTags: ['abbr', 'acronym', 'address', 'applet', 'area', 'article', 'aside', 'audio', 'base', 'basefont',
|
|
||||||
'bdi', 'bdo', 'big', 'blink', 'body', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup',
|
|
||||||
'command', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'em', 'embed',
|
|
||||||
'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
|
||||||
'head', 'header', 'hr', 'html', 'iframe', 'input', 'ins', 'kbd', 'keygen', 'label', 'legend', 'li', 'link',
|
|
||||||
'map', 'mark', 'marquee', 'menu', 'meta', 'meter', 'nav', 'noframes', 'noscript', 'object', 'ol', 'optgroup', 'option',
|
|
||||||
'output', 'param', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'script', 'section', 'select',
|
|
||||||
'source', 'span', 'strike', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'textarea', 'tfoot',
|
|
||||||
'th', 'thead', 'time', 'title', 'tr', 'track', 'tt', 'u', 'ul', 'const', 'video', 'wbr'],
|
|
||||||
|
|
||||||
escapeRegexChars: function (text) {
|
|
||||||
return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
|
|
||||||
},
|
|
||||||
|
|
||||||
escapeHTML: function (str) {
|
|
||||||
if (str == null) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
if (!str) {
|
|
||||||
return String(str);
|
|
||||||
}
|
|
||||||
|
|
||||||
return str.toString().replace(escapeChars, replaceChar);
|
|
||||||
},
|
|
||||||
|
|
||||||
isAndroidBrowser: function () {
|
|
||||||
// http://stackoverflow.com/questions/9286355/how-to-detect-only-the-native-android-browser
|
|
||||||
const nua = navigator.userAgent;
|
|
||||||
return ((nua.indexOf('Mozilla/5.0') > -1 && nua.indexOf('Android ') > -1 && nua.indexOf('AppleWebKit') > -1) && !(nua.indexOf('Chrome') > -1));
|
|
||||||
},
|
|
||||||
|
|
||||||
isTouchDevice: function () {
|
|
||||||
return 'ontouchstart' in document.documentElement;
|
|
||||||
},
|
|
||||||
|
|
||||||
findBootstrapEnvironment: function () {
|
|
||||||
// http://stackoverflow.com/questions/14441456/how-to-detect-which-device-view-youre-on-using-twitter-bootstrap-api
|
|
||||||
const envs = ['xs', 'sm', 'md', 'lg'];
|
|
||||||
const $el = $('<div>');
|
|
||||||
|
|
||||||
$el.appendTo($('body'));
|
|
||||||
|
|
||||||
for (let i = envs.length - 1; i >= 0; i -= 1) {
|
|
||||||
const env = envs[i];
|
|
||||||
|
|
||||||
$el.addClass('hidden-' + env);
|
|
||||||
if ($el.is(':hidden')) {
|
|
||||||
$el.remove();
|
|
||||||
return env;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
isMobile: function () {
|
|
||||||
const env = utils.findBootstrapEnvironment();
|
|
||||||
return ['xs', 'sm'].some(function (targetEnv) {
|
|
||||||
return targetEnv === env;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
getHoursArray: function () {
|
|
||||||
const currentHour = new Date().getHours();
|
|
||||||
const labels = [];
|
|
||||||
|
|
||||||
for (let i = currentHour, ii = currentHour - 24; i > ii; i -= 1) {
|
|
||||||
const hour = i < 0 ? 24 + i : i;
|
|
||||||
labels.push(hour + ':00');
|
|
||||||
}
|
|
||||||
|
|
||||||
return labels.reverse();
|
|
||||||
},
|
|
||||||
|
|
||||||
getDaysArray: function (from, amount) {
|
|
||||||
const currentDay = new Date(parseInt(from, 10) || Date.now()).getTime();
|
|
||||||
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
||||||
const labels = [];
|
|
||||||
let tmpDate;
|
|
||||||
|
|
||||||
for (let x = (amount || 30) - 1; x >= 0; x -= 1) {
|
|
||||||
tmpDate = new Date(currentDay - (1000 * 60 * 60 * 24 * x));
|
|
||||||
labels.push(months[tmpDate.getMonth()] + ' ' + tmpDate.getDate());
|
|
||||||
}
|
|
||||||
|
|
||||||
return labels;
|
|
||||||
},
|
|
||||||
|
|
||||||
/* Retrieved from http://stackoverflow.com/a/7557433 @ 27 Mar 2016 */
|
|
||||||
isElementInViewport: function (el) {
|
|
||||||
// special bonus for those using jQuery
|
|
||||||
if (typeof jQuery === 'function' && el instanceof jQuery) {
|
|
||||||
el = el[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
const rect = el.getBoundingClientRect();
|
|
||||||
|
|
||||||
return (
|
|
||||||
rect.top >= 0 &&
|
|
||||||
rect.left >= 0 &&
|
|
||||||
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /* or $(window).height() */
|
|
||||||
rect.right <= (window.innerWidth || document.documentElement.clientWidth) /* or $(window).width() */
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
// get all the url params in a single key/value hash
|
|
||||||
params: function (options) {
|
|
||||||
const hash = {};
|
|
||||||
|
|
||||||
options = options || {};
|
|
||||||
options.skipToType = options.skipToType || {};
|
|
||||||
|
|
||||||
let searchStr = window.location.search;
|
|
||||||
if (options.hasOwnProperty('url')) {
|
|
||||||
if (options.url) {
|
|
||||||
const a = utils.urlToLocation(options.url);
|
|
||||||
searchStr = a ? a.search : '';
|
|
||||||
} else {
|
|
||||||
searchStr = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const params = searchStr.substring(1).split('&');
|
|
||||||
|
|
||||||
params.forEach(function (param) {
|
|
||||||
const val = param.split('=');
|
|
||||||
let key = decodeURIComponent(val[0]);
|
|
||||||
const value = (
|
|
||||||
options.disableToType ||
|
|
||||||
options.skipToType[key] ? decodeURIComponent(val[1]) : utils.toType(decodeURIComponent(val[1]))
|
|
||||||
);
|
|
||||||
|
|
||||||
if (key) {
|
|
||||||
if (key.substr(-2, 2) === '[]') {
|
|
||||||
key = key.slice(0, -2);
|
|
||||||
}
|
|
||||||
if (!hash[key]) {
|
|
||||||
hash[key] = value;
|
|
||||||
} else {
|
|
||||||
if (!Array.isArray(hash[key])) {
|
|
||||||
hash[key] = [hash[key]];
|
|
||||||
}
|
|
||||||
hash[key].push(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return hash;
|
|
||||||
},
|
|
||||||
|
|
||||||
param: function (key) {
|
|
||||||
return this.params()[key];
|
|
||||||
},
|
|
||||||
|
|
||||||
urlToLocation: function (url) {
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
return a;
|
|
||||||
},
|
|
||||||
|
|
||||||
// return boolean if string 'true' or string 'false', or if a parsable string which is a number
|
|
||||||
// also supports JSON object and/or arrays parsing
|
|
||||||
toType: function (str) {
|
|
||||||
const type = typeof str;
|
|
||||||
if (type !== 'string') {
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
const nb = parseFloat(str);
|
|
||||||
if (!isNaN(nb) && isFinite(str)) {
|
|
||||||
return nb;
|
|
||||||
}
|
|
||||||
if (str === 'false') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (str === 'true') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
str = JSON.parse(str);
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
return str;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Safely get/set chained properties on an object
|
|
||||||
// set example: utils.props(A, 'a.b.c.d', 10) // sets A to {a: {b: {c: {d: 10}}}}, and returns 10
|
|
||||||
// get example: utils.props(A, 'a.b.c') // returns {d: 10}
|
|
||||||
// get example: utils.props(A, 'a.b.c.foo.bar') // returns undefined without throwing a TypeError
|
|
||||||
// credits to github.com/gkindel
|
|
||||||
props: function (obj, props, value) {
|
|
||||||
if (obj === undefined) {
|
|
||||||
obj = window;
|
|
||||||
}
|
|
||||||
if (props == null) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const i = props.indexOf('.');
|
|
||||||
if (i === -1) {
|
|
||||||
if (value !== undefined) {
|
|
||||||
obj[props] = value;
|
|
||||||
}
|
|
||||||
return obj[props];
|
|
||||||
}
|
|
||||||
const prop = props.slice(0, i);
|
|
||||||
const newProps = props.slice(i + 1);
|
|
||||||
|
|
||||||
if (props !== undefined && !(obj[prop] instanceof Object)) {
|
|
||||||
obj[prop] = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
return utils.props(obj[prop], newProps, value);
|
|
||||||
},
|
|
||||||
|
|
||||||
isInternalURI: function (targetLocation, referenceLocation, relative_path) {
|
|
||||||
return targetLocation.host === '' || // Relative paths are always internal links
|
|
||||||
(
|
|
||||||
targetLocation.host === referenceLocation.host &&
|
|
||||||
// Otherwise need to check if protocol and host match
|
|
||||||
targetLocation.protocol === referenceLocation.protocol &&
|
|
||||||
// Subfolder installs need this additional check
|
|
||||||
(relative_path.length > 0 ? targetLocation.pathname.indexOf(relative_path) === 0 : true)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
rtrim: function (str) {
|
|
||||||
return str.replace(/\s+$/g, '');
|
|
||||||
},
|
|
||||||
|
|
||||||
debounce: function (func, wait, immediate) {
|
|
||||||
// modified from https://davidwalsh.name/javascript-debounce-function
|
|
||||||
let timeout;
|
|
||||||
return function () {
|
|
||||||
const context = this;
|
|
||||||
const args = arguments;
|
|
||||||
const later = function () {
|
|
||||||
timeout = null;
|
|
||||||
if (!immediate) {
|
|
||||||
func.apply(context, args);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const callNow = immediate && !timeout;
|
|
||||||
clearTimeout(timeout);
|
|
||||||
timeout = setTimeout(later, wait);
|
|
||||||
if (callNow) {
|
|
||||||
func.apply(context, args);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
throttle: function (func, wait, immediate) {
|
|
||||||
let timeout;
|
|
||||||
return function () {
|
|
||||||
const context = this;
|
|
||||||
const args = arguments;
|
|
||||||
const later = function () {
|
|
||||||
timeout = null;
|
|
||||||
if (!immediate) {
|
|
||||||
func.apply(context, args);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const callNow = immediate && !timeout;
|
|
||||||
if (!timeout) {
|
|
||||||
timeout = setTimeout(later, wait);
|
|
||||||
}
|
|
||||||
if (callNow) {
|
|
||||||
func.apply(context, args);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return utils;
|
|
||||||
}));
|
|
||||||
|
|||||||
@@ -1,55 +1,52 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
(function (ajaxify) {
|
module.exports.render = function (template) {
|
||||||
ajaxify.widgets = {};
|
if (template.match(/^admin/)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ajaxify.widgets.render = function (template) {
|
const locations = Object.keys(ajaxify.data.widgets);
|
||||||
if (template.match(/^admin/)) {
|
|
||||||
|
locations.forEach(function (location) {
|
||||||
|
let area = $('#content [widget-area="' + location + '"],#content [data-widget-area="' + location + '"]').eq(0);
|
||||||
|
if (area.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const locations = Object.keys(ajaxify.data.widgets);
|
const widgetsAtLocation = ajaxify.data.widgets[location] || [];
|
||||||
|
let html = '';
|
||||||
|
|
||||||
locations.forEach(function (location) {
|
widgetsAtLocation.forEach(function (widget) {
|
||||||
let area = $('#content [widget-area="' + location + '"],#content [data-widget-area="' + location + '"]').eq(0);
|
html += widget.html;
|
||||||
if (area.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const widgetsAtLocation = ajaxify.data.widgets[location] || [];
|
|
||||||
let html = '';
|
|
||||||
|
|
||||||
widgetsAtLocation.forEach(function (widget) {
|
|
||||||
html += widget.html;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (location === 'footer' && !$('#content [widget-area="footer"],#content [data-widget-area="footer"]').length) {
|
|
||||||
$('#content').append($('<div class="row"><div data-widget-area="footer" class="col-xs-12"></div></div>'));
|
|
||||||
} else if (location === 'sidebar' && !$('#content [widget-area="sidebar"],#content [data-widget-area="sidebar"]').length) {
|
|
||||||
if ($('[component="account/cover"]').length) {
|
|
||||||
$('[component="account/cover"]').nextAll().wrapAll($('<div class="row"><div class="col-lg-9 col-xs-12"></div><div data-widget-area="sidebar" class="col-lg-3 col-xs-12"></div></div></div>'));
|
|
||||||
} else if ($('[component="groups/cover"]').length) {
|
|
||||||
$('[component="groups/cover"]').nextAll().wrapAll($('<div class="row"><div class="col-lg-9 col-xs-12"></div><div data-widget-area="sidebar" class="col-lg-3 col-xs-12"></div></div></div>'));
|
|
||||||
} else {
|
|
||||||
$('#content > *').wrapAll($('<div class="row"><div class="col-lg-9 col-xs-12"></div><div data-widget-area="sidebar" class="col-lg-3 col-xs-12"></div></div></div>'));
|
|
||||||
}
|
|
||||||
} else if (location === 'header' && !$('#content [widget-area="header"],#content [data-widget-area="header"]').length) {
|
|
||||||
$('#content').prepend($('<div class="row"><div data-widget-area="header" class="col-xs-12"></div></div>'));
|
|
||||||
}
|
|
||||||
|
|
||||||
area = $('#content [widget-area="' + location + '"],#content [data-widget-area="' + location + '"]').eq(0);
|
|
||||||
if (html && area.length) {
|
|
||||||
area.html(html);
|
|
||||||
area.find('img:not(.not-responsive)').addClass('img-responsive');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (widgetsAtLocation.length) {
|
|
||||||
area.removeClass('hidden');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
require(['hooks'], function (hooks) {
|
if (location === 'footer' && !$('#content [widget-area="footer"],#content [data-widget-area="footer"]').length) {
|
||||||
hooks.fire('action:widgets.loaded', {});
|
$('#content').append($('<div class="row"><div data-widget-area="footer" class="col-xs-12"></div></div>'));
|
||||||
});
|
} else if (location === 'sidebar' && !$('#content [widget-area="sidebar"],#content [data-widget-area="sidebar"]').length) {
|
||||||
};
|
if ($('[component="account/cover"]').length) {
|
||||||
}(ajaxify || {}));
|
$('[component="account/cover"]').nextAll().wrapAll($('<div class="row"><div class="col-lg-9 col-xs-12"></div><div data-widget-area="sidebar" class="col-lg-3 col-xs-12"></div></div></div>'));
|
||||||
|
} else if ($('[component="groups/cover"]').length) {
|
||||||
|
$('[component="groups/cover"]').nextAll().wrapAll($('<div class="row"><div class="col-lg-9 col-xs-12"></div><div data-widget-area="sidebar" class="col-lg-3 col-xs-12"></div></div></div>'));
|
||||||
|
} else {
|
||||||
|
$('#content > *').wrapAll($('<div class="row"><div class="col-lg-9 col-xs-12"></div><div data-widget-area="sidebar" class="col-lg-3 col-xs-12"></div></div></div>'));
|
||||||
|
}
|
||||||
|
} else if (location === 'header' && !$('#content [widget-area="header"],#content [data-widget-area="header"]').length) {
|
||||||
|
$('#content').prepend($('<div class="row"><div data-widget-area="header" class="col-xs-12"></div></div>'));
|
||||||
|
}
|
||||||
|
|
||||||
|
area = $('#content [widget-area="' + location + '"],#content [data-widget-area="' + location + '"]').eq(0);
|
||||||
|
if (html && area.length) {
|
||||||
|
area.html(html);
|
||||||
|
area.find('img:not(.not-responsive)').addClass('img-responsive');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (widgetsAtLocation.length) {
|
||||||
|
area.removeClass('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
require(['hooks'], function (hooks) {
|
||||||
|
hooks.fire('action:widgets.loaded', {});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -181,6 +181,7 @@ program
|
|||||||
.command('build [targets...]')
|
.command('build [targets...]')
|
||||||
.description(`Compile static assets ${chalk.red('(JS, CSS, templates, languages)')}`)
|
.description(`Compile static assets ${chalk.red('(JS, CSS, templates, languages)')}`)
|
||||||
.option('-s, --series', 'Run builds in series without extra processes')
|
.option('-s, --series', 'Run builds in series without extra processes')
|
||||||
|
.option('-w, --webpack', 'Bundle assets with webpack', true)
|
||||||
.action((targets, options) => {
|
.action((targets, options) => {
|
||||||
if (program.opts().dev) {
|
if (program.opts().dev) {
|
||||||
process.env.NODE_ENV = 'development';
|
process.env.NODE_ENV = 'development';
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const navigationAdmin = require('../../navigation/admin');
|
|||||||
const social = require('../../social');
|
const social = require('../../social');
|
||||||
|
|
||||||
const helpers = require('../helpers');
|
const helpers = require('../helpers');
|
||||||
const translator = require('../../../public/src/modules/translator');
|
const translator = require('../../translator');
|
||||||
|
|
||||||
const settingsController = module.exports;
|
const settingsController = module.exports;
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const topics = require('./topics');
|
|||||||
const posts = require('./posts');
|
const posts = require('./posts');
|
||||||
const privileges = require('./privileges');
|
const privileges = require('./privileges');
|
||||||
const plugins = require('./plugins');
|
const plugins = require('./plugins');
|
||||||
const utils = require('../public/src/utils');
|
const utils = require('./utils');
|
||||||
const batch = require('./batch');
|
const batch = require('./batch');
|
||||||
|
|
||||||
const Flags = module.exports;
|
const Flags = module.exports;
|
||||||
|
|||||||
@@ -176,6 +176,11 @@ exports.build = async function (targets, options) {
|
|||||||
|
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
await buildTargets(targets, !series);
|
await buildTargets(targets, !series);
|
||||||
|
|
||||||
|
if (options.webpack) {
|
||||||
|
await exports.webpack(options);
|
||||||
|
}
|
||||||
|
|
||||||
const totalTime = (Date.now() - startTime) / 1000;
|
const totalTime = (Date.now() - startTime) / 1000;
|
||||||
await cacheBuster.write();
|
await cacheBuster.write();
|
||||||
winston.info(`[build] Asset compilation successful. Completed in ${totalTime}sec.`);
|
winston.info(`[build] Asset compilation successful. Completed in ${totalTime}sec.`);
|
||||||
@@ -185,8 +190,54 @@ exports.build = async function (targets, options) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getWebpackConfig() {
|
||||||
|
return require(process.env.NODE_ENV !== 'development' ? '../../webpack.prod' : '../../webpack.dev');
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.webpack = async function (options) {
|
||||||
|
winston.info(`[build] ${(options.watch ? 'Watching' : 'Bundling')} with Webpack.`);
|
||||||
|
const webpack = require('webpack');
|
||||||
|
const fs = require('fs');
|
||||||
|
const util = require('util');
|
||||||
|
const db = require('../database');
|
||||||
|
|
||||||
|
const activePlugins = await db.getSortedSetRange('plugins:active', 0, -1);
|
||||||
|
if (!activePlugins.includes('nodebb-plugin-composer-default')) {
|
||||||
|
activePlugins.push('nodebb-plugin-composer-default');
|
||||||
|
}
|
||||||
|
await fs.promises.writeFile(path.resolve(__dirname, '../../build/active_plugins.json'), JSON.stringify(activePlugins));
|
||||||
|
|
||||||
|
const webpackCfg = getWebpackConfig();
|
||||||
|
const compiler = webpack(webpackCfg);
|
||||||
|
const webpackRun = util.promisify(compiler.run).bind(compiler);
|
||||||
|
const webpackWatch = util.promisify(compiler.watch).bind(compiler);
|
||||||
|
try {
|
||||||
|
let stats;
|
||||||
|
if (options.watch) {
|
||||||
|
stats = await webpackWatch(webpackCfg.watchOptions);
|
||||||
|
compiler.hooks.assetEmitted.tap('nbbWatchPlugin', (file) => {
|
||||||
|
console.log(`webpack:assetEmitted > ${webpackCfg.output.publicPath}${file}`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
stats = await webpackRun();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stats.hasErrors() || stats.hasWarnings()) {
|
||||||
|
console.log(stats.toString('minimal'));
|
||||||
|
} else {
|
||||||
|
const statsJson = stats.toJson();
|
||||||
|
winston.info(`[build] ${(options.watch ? 'Watching' : 'Bundling')} took ${statsJson.time} ms`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.stack || err);
|
||||||
|
if (err.details) {
|
||||||
|
console.error(err.details);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
exports.buildAll = async function () {
|
exports.buildAll = async function () {
|
||||||
await exports.build(allTargets);
|
await exports.build(allTargets, { webpack: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
require('../promisify')(exports);
|
require('../promisify')(exports);
|
||||||
|
|||||||
266
src/meta/js.js
266
src/meta/js.js
@@ -14,104 +14,18 @@ const minifier = require('./minifier');
|
|||||||
|
|
||||||
const JS = module.exports;
|
const JS = module.exports;
|
||||||
|
|
||||||
|
|
||||||
JS.scripts = {
|
JS.scripts = {
|
||||||
base: [
|
base: [
|
||||||
'node_modules/socket.io-client/dist/socket.io.js',
|
|
||||||
'node_modules/requirejs/require.js',
|
|
||||||
'public/src/require-config.js',
|
|
||||||
'node_modules/jquery/dist/jquery.js',
|
|
||||||
'node_modules/textcomplete/dist/textcomplete.min.js',
|
|
||||||
'node_modules/textcomplete.contenteditable/dist/textcomplete.codemirror.min.js',
|
|
||||||
'node_modules/visibilityjs/lib/visibility.core.js',
|
|
||||||
'node_modules/bootstrap/dist/js/bootstrap.js',
|
|
||||||
'node_modules/@adactive/bootstrap-tagsinput/src/bootstrap-tagsinput.js',
|
'node_modules/@adactive/bootstrap-tagsinput/src/bootstrap-tagsinput.js',
|
||||||
'node_modules/benchpressjs/build/benchpress.js',
|
|
||||||
'node_modules/jquery-serializeobject/jquery.serializeObject.js',
|
'node_modules/jquery-serializeobject/jquery.serializeObject.js',
|
||||||
'node_modules/jquery-deserialize/src/jquery.deserialize.js',
|
'node_modules/jquery-deserialize/src/jquery.deserialize.js',
|
||||||
|
|
||||||
'public/vendor/bootbox/wrapper.js',
|
'public/vendor/bootbox/wrapper.js',
|
||||||
|
|
||||||
'public/src/utils.js',
|
|
||||||
'public/src/sockets.js',
|
|
||||||
'public/src/app.js',
|
|
||||||
'public/src/ajaxify.js',
|
|
||||||
'public/src/overrides.js',
|
|
||||||
'public/src/widgets.js',
|
|
||||||
],
|
],
|
||||||
|
|
||||||
// files listed below are only available client-side, or are bundled in to reduce # of network requests on cold load
|
// plugins add entries into this object,
|
||||||
rjs: [
|
// they get linked into /build/public/src/modules
|
||||||
'public/src/client/header.js',
|
modules: { },
|
||||||
'public/src/client/header/chat.js',
|
|
||||||
'public/src/client/header/notifications.js',
|
|
||||||
'public/src/client/infinitescroll.js',
|
|
||||||
'public/src/client/pagination.js',
|
|
||||||
'public/src/client/recent.js',
|
|
||||||
'public/src/client/unread.js',
|
|
||||||
'public/src/client/topic.js',
|
|
||||||
'public/src/client/topic/events.js',
|
|
||||||
'public/src/client/topic/posts.js',
|
|
||||||
'public/src/client/topic/images.js',
|
|
||||||
'public/src/client/topic/votes.js',
|
|
||||||
'public/src/client/topic/postTools.js',
|
|
||||||
'public/src/client/topic/threadTools.js',
|
|
||||||
'public/src/client/categories.js',
|
|
||||||
'public/src/client/category.js',
|
|
||||||
'public/src/client/category/tools.js',
|
|
||||||
|
|
||||||
'public/src/modules/translator.js',
|
|
||||||
'public/src/modules/components.js',
|
|
||||||
'public/src/modules/hooks.js',
|
|
||||||
'public/src/modules/sort.js',
|
|
||||||
'public/src/modules/navigator.js',
|
|
||||||
'public/src/modules/topicSelect.js',
|
|
||||||
'public/src/modules/topicList.js',
|
|
||||||
'public/src/modules/categoryFilter.js',
|
|
||||||
'public/src/modules/categorySelector.js',
|
|
||||||
'public/src/modules/categorySearch.js',
|
|
||||||
'public/src/modules/share.js',
|
|
||||||
'public/src/modules/alerts.js',
|
|
||||||
'public/src/modules/taskbar.js',
|
|
||||||
'public/src/modules/helpers.js',
|
|
||||||
'public/src/modules/storage.js',
|
|
||||||
'public/src/modules/handleBack.js',
|
|
||||||
'public/src/modules/messages.js',
|
|
||||||
'public/src/modules/search.js',
|
|
||||||
],
|
|
||||||
|
|
||||||
admin: [
|
|
||||||
'node_modules/material-design-lite/material.js',
|
|
||||||
'public/src/admin/admin.js',
|
|
||||||
'node_modules/jquery-deserialize/src/jquery.deserialize.js',
|
|
||||||
],
|
|
||||||
|
|
||||||
// modules listed below are built (/src/modules) so they can be defined anonymously
|
|
||||||
modules: {
|
|
||||||
'Chart.js': 'node_modules/chart.js/dist/Chart.min.js',
|
|
||||||
'mousetrap.js': 'node_modules/mousetrap/mousetrap.min.js',
|
|
||||||
'cropper.js': 'node_modules/cropperjs/dist/cropper.min.js',
|
|
||||||
'jquery-ui': 'node_modules/jquery-ui/ui',
|
|
||||||
'zxcvbn.js': 'node_modules/zxcvbn/dist/zxcvbn.js',
|
|
||||||
|
|
||||||
// only get ace files required by acp
|
|
||||||
'ace/ace.js': 'node_modules/ace-builds/src-min/ace.js',
|
|
||||||
'ace/mode-less.js': 'node_modules/ace-builds/src-min/mode-less.js',
|
|
||||||
'ace/mode-javascript.js': 'node_modules/ace-builds/src-min/mode-javascript.js',
|
|
||||||
'ace/mode-html.js': 'node_modules/ace-builds/src-min/mode-html.js',
|
|
||||||
'ace/theme-twilight.js': 'node_modules/ace-builds/src-min/theme-twilight.js',
|
|
||||||
'ace/worker-css.js': 'node_modules/ace-builds/src-min/worker-css.js',
|
|
||||||
'ace/worker-javascript.js': 'node_modules/ace-builds/src-min/worker-javascript.js',
|
|
||||||
'ace/worker-html.js': 'node_modules/ace-builds/src-min/worker-html.js',
|
|
||||||
'ace/ext-searchbox.js': 'node_modules/ace-builds/src-min/ext-searchbox.js',
|
|
||||||
|
|
||||||
'clipboard.js': 'node_modules/clipboard/dist/clipboard.min.js',
|
|
||||||
'tinycon.js': 'node_modules/tinycon/tinycon.js',
|
|
||||||
'slideout.js': 'node_modules/slideout/dist/slideout.min.js',
|
|
||||||
'compare-versions.js': 'node_modules/compare-versions/index.js',
|
|
||||||
'timeago/locales': 'node_modules/timeago/locales',
|
|
||||||
'jquery-form.js': 'node_modules/jquery-form/dist/jquery.form.min.js',
|
|
||||||
'xregexp.js': 'node_modules/xregexp/xregexp-all.js',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
async function linkIfLinux(srcPath, destPath) {
|
async function linkIfLinux(srcPath, destPath) {
|
||||||
@@ -124,36 +38,14 @@ async function linkIfLinux(srcPath, destPath) {
|
|||||||
|
|
||||||
const basePath = path.resolve(__dirname, '../..');
|
const basePath = path.resolve(__dirname, '../..');
|
||||||
|
|
||||||
async function minifyModules(modules, fork) {
|
|
||||||
const moduleDirs = modules.reduce((prev, mod) => {
|
|
||||||
const dir = path.resolve(path.dirname(mod.destPath));
|
|
||||||
if (!prev.includes(dir)) {
|
|
||||||
prev.push(dir);
|
|
||||||
}
|
|
||||||
return prev;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
await Promise.all(moduleDirs.map(dir => mkdirp(dir)));
|
|
||||||
|
|
||||||
const filtered = modules.reduce((prev, mod) => {
|
|
||||||
if (mod.srcPath.endsWith('.min.js') || path.dirname(mod.srcPath).endsWith('min')) {
|
|
||||||
prev.skip.push(mod);
|
|
||||||
} else {
|
|
||||||
prev.minify.push(mod);
|
|
||||||
}
|
|
||||||
|
|
||||||
return prev;
|
|
||||||
}, { minify: [], skip: [] });
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
minifier.js.minifyBatch(filtered.minify, fork),
|
|
||||||
...filtered.skip.map(mod => linkIfLinux(mod.srcPath, mod.destPath)),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function linkModules() {
|
async function linkModules() {
|
||||||
const { modules } = JS.scripts;
|
const { modules } = JS.scripts;
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
mkdirp(path.join(__dirname, '../../build/public/src/admin/plugins')),
|
||||||
|
mkdirp(path.join(__dirname, '../../build/public/src/client/plugins')),
|
||||||
|
]);
|
||||||
|
|
||||||
await Promise.all(Object.keys(modules).map(async (relPath) => {
|
await Promise.all(Object.keys(modules).map(async (relPath) => {
|
||||||
const srcPath = path.join(__dirname, '../../', modules[relPath]);
|
const srcPath = path.join(__dirname, '../../', modules[relPath]);
|
||||||
const destPath = path.join(__dirname, '../../build/public/src/modules', relPath);
|
const destPath = path.join(__dirname, '../../build/public/src/modules', relPath);
|
||||||
@@ -163,56 +55,14 @@ async function linkModules() {
|
|||||||
]);
|
]);
|
||||||
if (stats.isDirectory()) {
|
if (stats.isDirectory()) {
|
||||||
await file.linkDirs(srcPath, destPath, true);
|
await file.linkDirs(srcPath, destPath, true);
|
||||||
return;
|
} else {
|
||||||
|
await linkIfLinux(srcPath, destPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
await linkIfLinux(srcPath, destPath);
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
const moduleDirs = ['modules', 'admin', 'client'];
|
const moduleDirs = ['modules', 'admin', 'client'];
|
||||||
|
|
||||||
async function getModuleList() {
|
|
||||||
let modules = Object.keys(JS.scripts.modules).map(relPath => ({
|
|
||||||
srcPath: path.join(__dirname, '../../', JS.scripts.modules[relPath]),
|
|
||||||
destPath: path.join(__dirname, '../../build/public/src/modules', relPath),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const coreDirs = moduleDirs.map(dir => ({
|
|
||||||
srcPath: path.join(__dirname, '../../public/src', dir),
|
|
||||||
destPath: path.join(__dirname, '../../build/public/src', dir),
|
|
||||||
}));
|
|
||||||
|
|
||||||
modules = modules.concat(coreDirs);
|
|
||||||
|
|
||||||
const moduleFiles = [];
|
|
||||||
await Promise.all(modules.map(async (module) => {
|
|
||||||
const { srcPath } = module;
|
|
||||||
const { destPath } = module;
|
|
||||||
|
|
||||||
const stats = await fs.promises.stat(srcPath);
|
|
||||||
if (!stats.isDirectory()) {
|
|
||||||
moduleFiles.push(module);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const files = await file.walk(srcPath);
|
|
||||||
|
|
||||||
const mods = files.filter(
|
|
||||||
filePath => path.extname(filePath) === '.js'
|
|
||||||
).map(filePath => ({
|
|
||||||
srcPath: path.normalize(filePath),
|
|
||||||
destPath: path.join(destPath, path.relative(srcPath, filePath)),
|
|
||||||
}));
|
|
||||||
|
|
||||||
moduleFiles.push(...mods);
|
|
||||||
}));
|
|
||||||
moduleFiles.forEach((mod) => {
|
|
||||||
mod.filename = path.relative(basePath, mod.srcPath).replace(/\\/g, '/');
|
|
||||||
});
|
|
||||||
return moduleFiles;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function clearModules() {
|
async function clearModules() {
|
||||||
const builtPaths = moduleDirs.map(
|
const builtPaths = moduleDirs.map(
|
||||||
p => path.join(__dirname, '../../build/public/src', p)
|
p => path.join(__dirname, '../../build/public/src', p)
|
||||||
@@ -222,65 +72,18 @@ async function clearModules() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
JS.buildModules = async function (fork) {
|
JS.buildModules = async function () {
|
||||||
await clearModules();
|
await clearModules();
|
||||||
if (process.env.NODE_ENV === 'development') {
|
|
||||||
await linkModules();
|
const fse = require('fs-extra');
|
||||||
return;
|
await fse.copy(
|
||||||
}
|
path.join(__dirname, `../../public/src`),
|
||||||
const modules = await getModuleList();
|
path.join(__dirname, `../../build/public/src`)
|
||||||
await minifyModules(modules, fork);
|
);
|
||||||
|
|
||||||
|
await linkModules();
|
||||||
};
|
};
|
||||||
|
|
||||||
async function requirejsOptimize(target) {
|
|
||||||
const requirejs = require('requirejs');
|
|
||||||
let scriptText = '';
|
|
||||||
const sharedCfg = {
|
|
||||||
paths: {
|
|
||||||
jquery: 'empty:',
|
|
||||||
},
|
|
||||||
optimize: 'none',
|
|
||||||
out: function (text) {
|
|
||||||
scriptText += text;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const bundledModules = [
|
|
||||||
{
|
|
||||||
baseUrl: path.join(basePath, 'node_modules'),
|
|
||||||
name: 'timeago/jquery.timeago',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
baseUrl: path.join(basePath, 'node_modules/nprogress'),
|
|
||||||
name: 'nprogress',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
baseUrl: path.join(basePath, 'node_modules/bootbox'),
|
|
||||||
name: 'bootbox',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const targetModules = {
|
|
||||||
admin: [
|
|
||||||
{
|
|
||||||
baseUrl: path.join(basePath, 'node_modules/sortablejs'),
|
|
||||||
name: 'Sortable',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
client: [],
|
|
||||||
};
|
|
||||||
const optimizeAsync = util.promisify((config, cb) => {
|
|
||||||
requirejs.optimize(config, () => cb(), err => cb(err));
|
|
||||||
});
|
|
||||||
|
|
||||||
const allModules = bundledModules.concat(targetModules[target]);
|
|
||||||
|
|
||||||
for (const moduleCfg of allModules) {
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
await optimizeAsync({ ...sharedCfg, ...moduleCfg });
|
|
||||||
}
|
|
||||||
const filePath = path.join(__dirname, `../../build/public/rjs-bundle-${target}.js`);
|
|
||||||
await fs.promises.writeFile(filePath, scriptText);
|
|
||||||
}
|
|
||||||
|
|
||||||
JS.linkStatics = async function () {
|
JS.linkStatics = async function () {
|
||||||
await rimrafAsync(path.join(__dirname, '../../build/public/plugins'));
|
await rimrafAsync(path.join(__dirname, '../../build/public/plugins'));
|
||||||
|
|
||||||
@@ -313,15 +116,7 @@ async function getBundleScriptList(target) {
|
|||||||
pluginScripts = pluginScripts.concat(scripts);
|
pluginScripts = pluginScripts.concat(scripts);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let scripts = JS.scripts.base;
|
pluginScripts = JS.scripts.base.concat(pluginScripts).map((script) => {
|
||||||
|
|
||||||
if (target === 'client') {
|
|
||||||
scripts = scripts.concat(JS.scripts.rjs);
|
|
||||||
} else if (target === 'acp') {
|
|
||||||
scripts = scripts.concat(JS.scripts.admin);
|
|
||||||
}
|
|
||||||
|
|
||||||
scripts = scripts.concat(pluginScripts).map((script) => {
|
|
||||||
const srcPath = path.resolve(basePath, script).replace(/\\/g, '/');
|
const srcPath = path.resolve(basePath, script).replace(/\\/g, '/');
|
||||||
return {
|
return {
|
||||||
srcPath: srcPath,
|
srcPath: srcPath,
|
||||||
@@ -329,29 +124,18 @@ async function getBundleScriptList(target) {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return scripts;
|
return pluginScripts;
|
||||||
}
|
}
|
||||||
|
|
||||||
JS.buildBundle = async function (target, fork) {
|
JS.buildBundle = async function (target, fork) {
|
||||||
const fileNames = {
|
const filename = `scripts-${target}.min.js`;
|
||||||
client: 'nodebb.min.js',
|
|
||||||
admin: 'acp.min.js',
|
|
||||||
};
|
|
||||||
await requirejsOptimize(target);
|
|
||||||
const files = await getBundleScriptList(target);
|
const files = await getBundleScriptList(target);
|
||||||
|
|
||||||
const srcPath = path.join(__dirname, `../../build/public/rjs-bundle-${target}.js`);
|
|
||||||
files.push({
|
|
||||||
srcPath: srcPath,
|
|
||||||
filename: path.relative(basePath, srcPath).replace(/\\/g, '/'),
|
|
||||||
});
|
|
||||||
|
|
||||||
const minify = process.env.NODE_ENV !== 'development';
|
const minify = process.env.NODE_ENV !== 'development';
|
||||||
const filePath = path.join(__dirname, '../../build/public', fileNames[target]);
|
const filePath = path.join(__dirname, '../../build/public', filename);
|
||||||
|
|
||||||
await minifier.js.bundle({
|
await minifier.js.bundle({
|
||||||
files: files,
|
files: files,
|
||||||
filename: fileNames[target],
|
filename: filename,
|
||||||
destPath: filePath,
|
destPath: filePath,
|
||||||
}, minify, fork);
|
}, minify, fork);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const user = require('../user');
|
|||||||
const meta = require('../meta');
|
const meta = require('../meta');
|
||||||
const plugins = require('../plugins');
|
const plugins = require('../plugins');
|
||||||
const privileges = require('../privileges');
|
const privileges = require('../privileges');
|
||||||
const utils = require('../../public/src/utils');
|
const utils = require('../utils');
|
||||||
const versions = require('../admin/versions');
|
const versions = require('../admin/versions');
|
||||||
const helpers = require('./helpers');
|
const helpers = require('./helpers');
|
||||||
|
|
||||||
|
|||||||
@@ -118,8 +118,7 @@ Data.getStaticDirectories = async function (pluginData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(dirs.map(route => processDir(route)));
|
await Promise.all(dirs.map(route => processDir(route)));
|
||||||
winston.verbose(`[plugins] found ${Object.keys(staticDirs).length
|
winston.verbose(`[plugins] found ${Object.keys(staticDirs).length} static directories for ${pluginData.id}`);
|
||||||
} static directories for ${pluginData.id}`);
|
|
||||||
return staticDirs;
|
return staticDirs;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,16 @@ module.exports = function (app) {
|
|||||||
res.redirect(404);
|
res.redirect(404);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// const { setupPageRoute } = require('./helpers');
|
||||||
|
// setupPageRoute(app, '/debug/test', middleware, [], async (req, res) => {
|
||||||
|
// // res.redirect(404);
|
||||||
|
// const meta = require('../meta');
|
||||||
|
// res.render('test', {
|
||||||
|
// now: new Date().toISOString(),
|
||||||
|
// skins: [{ name: 'no-skin', value: '' }].concat(meta.css.supportedSkins.map(s => ({ name: s, value: s }))),
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
|
||||||
// Redoc
|
// Redoc
|
||||||
router.get('/spec/:type', async (req, res, next) => {
|
router.get('/spec/:type', async (req, res, next) => {
|
||||||
const types = ['read', 'write'];
|
const types = ['read', 'write'];
|
||||||
|
|||||||
@@ -170,6 +170,7 @@ function addCoreRoutes(app, router, middleware, mounts) {
|
|||||||
|
|
||||||
const statics = [
|
const statics = [
|
||||||
{ route: '/assets', path: path.join(__dirname, '../../build/public') },
|
{ route: '/assets', path: path.join(__dirname, '../../build/public') },
|
||||||
|
{ route: '/assets', path: path.join(__dirname, '../../build/webpack') }, // todo: messy; relocate in v3
|
||||||
{ route: '/assets', path: path.join(__dirname, '../../public') },
|
{ route: '/assets', path: path.join(__dirname, '../../public') },
|
||||||
{ route: '/plugins', path: path.join(__dirname, '../../build/public/plugins') },
|
{ route: '/plugins', path: path.join(__dirname, '../../build/public/plugins') },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const user = require('../user');
|
|||||||
const posts = require('../posts');
|
const posts = require('../posts');
|
||||||
const meta = require('../meta');
|
const meta = require('../meta');
|
||||||
const plugins = require('../plugins');
|
const plugins = require('../plugins');
|
||||||
const utils = require('../../public/src/utils');
|
const utils = require('../utils');
|
||||||
|
|
||||||
const backlinkRegex = new RegExp(`(?:${nconf.get('url').replace('/', '\\/')}|\b|\\s)\\/topic\\/(\\d+)(?:\\/\\w+)?`, 'g');
|
const backlinkRegex = new RegExp(`(?:${nconf.get('url').replace('/', '\\/')}|\b|\\s)\\/topic\\/(\\d+)(?:\\/\\w+)?`, 'g');
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
module.exports = require('../public/src/modules/translator');
|
const winston = require('winston');
|
||||||
|
|
||||||
|
function warn(msg) {
|
||||||
|
winston.warn(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = require('../public/src/modules/translator.common')(require('./utils'), (lang, namespace) => {
|
||||||
|
const languages = require('./languages');
|
||||||
|
return languages.get(lang, namespace);
|
||||||
|
}, warn);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const validator = require('validator');
|
|||||||
const db = require('../database');
|
const db = require('../database');
|
||||||
const posts = require('../posts');
|
const posts = require('../posts');
|
||||||
const topics = require('../topics');
|
const topics = require('../topics');
|
||||||
const utils = require('../../public/src/utils');
|
const utils = require('../utils');
|
||||||
|
|
||||||
module.exports = function (User) {
|
module.exports = function (User) {
|
||||||
User.getLatestBanInfo = async function (uid) {
|
User.getLatestBanInfo = async function (uid) {
|
||||||
|
|||||||
16
src/utils.js
16
src/utils.js
@@ -1,3 +1,17 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
module.exports = require('../public/src/utils');
|
process.profile = function (operation, start) {
|
||||||
|
console.log('%s took %d milliseconds', operation, process.elapsedTimeSince(start));
|
||||||
|
};
|
||||||
|
|
||||||
|
process.elapsedTimeSince = function (start) {
|
||||||
|
const diff = process.hrtime(start);
|
||||||
|
return (diff[0] * 1e3) + (diff[1] / 1e6);
|
||||||
|
};
|
||||||
|
const utils = require('../public/src/utils.common');
|
||||||
|
|
||||||
|
utils.getLanguage = function () {
|
||||||
|
const meta = require('./meta');
|
||||||
|
return meta.config && meta.config.defaultLang ? meta.config.defaultLang : 'en-GB';
|
||||||
|
};
|
||||||
|
module.exports = utils;
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
<script>
|
|
||||||
window.addEventListener('load', function () {
|
|
||||||
define(config.relative_path + '/assets/templates/500.js', function () {
|
|
||||||
function compiled(helpers, context, get, iter, helper) {
|
|
||||||
return '<div class="alert alert-danger">\n\t<strong>[[global:500.title]]</strong>\n\t<p>[[global:500.message]]</p>\n\t<p>' +
|
|
||||||
helpers.__escape(get(context && context['path'])) + '</p>\n\t' +
|
|
||||||
(get(context && context['error']) ? '<p>' + helpers.__escape(get(context && context['error'])) + '</p>' : '') + '\n\n\t' +
|
|
||||||
(get(context && context['returnLink']) ? '\n\t<p>[[error:goback]]</p>\n\t' : '') + '\n</div>\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
return compiled;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
|
<!-- IMPORT admin/partials/settings/header.tpl -->
|
||||||
<div class="row post-cache">
|
<div class="row post-cache">
|
||||||
<div class="col-lg-12">
|
<div class="col-lg-12">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -118,7 +118,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- IMPORT admin/partials/settings/footer.tpl -->
|
||||||
<button id="save" class="floating-button mdl-button mdl-js-button mdl-button--fab mdl-js-ripple-effect mdl-button--colored">
|
|
||||||
<i class="material-icons">save</i>
|
|
||||||
</button>
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
<!-- IMPORT admin/partials/settings/header.tpl -->
|
||||||
<div class="row logger">
|
<div class="row logger">
|
||||||
<div class="col-lg-9">
|
<div class="col-lg-12">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">[[admin/development/logger:logger-settings]]</div>
|
<div class="panel-heading">[[admin/development/logger:logger-settings]]</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
@@ -31,22 +32,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-lg-3 acp-sidebar">
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading">[[admin/development/logger:control-panel]]</div>
|
|
||||||
<div class="panel-body">
|
|
||||||
<button class="btn btn-primary" id="save">[[admin/development/logger:update-settings]]</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- IMPORT admin/partials/settings/footer.tpl -->
|
||||||
<script>
|
|
||||||
require(['admin/settings'], function(Settings) {
|
|
||||||
Settings.prepare();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script type="text/javascript" src="{relative_path}/assets/acp.min.js?{cache-buster}"></script>
|
<script type="text/javascript" src="{relative_path}/assets/admin.min.js?{cache-buster}"></script>
|
||||||
|
|
||||||
<!-- BEGIN scripts -->
|
<!-- BEGIN scripts -->
|
||||||
<script type="text/javascript" src="{scripts.src}"></script>
|
<script type="text/javascript" src="{scripts.src}"></script>
|
||||||
|
|||||||
@@ -3,10 +3,3 @@
|
|||||||
<button id="save" class="floating-button mdl-button mdl-js-button mdl-button--fab mdl-js-ripple-effect mdl-button--colored">
|
<button id="save" class="floating-button mdl-button mdl-js-button mdl-button--fab mdl-js-ripple-effect mdl-button--colored">
|
||||||
<i class="material-icons">save</i>
|
<i class="material-icons">save</i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<script>
|
|
||||||
require(['admin/settings'], function(Settings) {
|
|
||||||
Settings.prepare();
|
|
||||||
Settings.populateTOC();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
<!-- IMPORT admin/partials/settings/header.tpl -->
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-2 col-xs-12 settings-header">[[admin/settings/homepage:home-page]]</div>
|
<div class="col-sm-2 col-xs-12 settings-header">[[admin/settings/homepage:home-page]]</div>
|
||||||
<div class="col-sm-10 col-xs-12">
|
<div class="col-sm-10 col-xs-12">
|
||||||
@@ -33,12 +34,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button id="save" class="floating-button mdl-button mdl-js-button mdl-button--fab mdl-js-ripple-effect mdl-button--colored">
|
<!-- IMPORT admin/partials/settings/footer.tpl -->
|
||||||
<i class="material-icons">save</i>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
require(['admin/settings'], function(Settings) {
|
|
||||||
Settings.prepare();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<link rel="stylesheet" type="text/css" href="bootstrap.min.css">
|
<link rel="stylesheet" type="text/css" href="bootstrap.min.css">
|
||||||
<link rel="stylesheet" type="text/css" href="installer.css">
|
<link rel="stylesheet" type="text/css" href="installer.css">
|
||||||
|
|
||||||
<script type="text/javascript" async defer src="installer.min.js"></script>
|
<script type="text/javascript" async defer src="/assets/installer.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
59
src/views/test.tpl
Normal file
59
src/views/test.tpl
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<label>Tags Input</label>
|
||||||
|
<input id="inputTags" class="form-control" value="" placeholder="">
|
||||||
|
<hr/>
|
||||||
|
|
||||||
|
<label>Birthday</label>
|
||||||
|
<input id="inputBirthday" class="form-control" value="" placeholder="mm/dd/yyyy">
|
||||||
|
<hr/>
|
||||||
|
|
||||||
|
<label>Auto complete</label>
|
||||||
|
<input id="autocomplete" class="form-control">
|
||||||
|
<hr/>
|
||||||
|
|
||||||
|
<label>Color Picker</label>
|
||||||
|
<input id="colorpicker" type="color" class="form-control">
|
||||||
|
<hr/>
|
||||||
|
|
||||||
|
<label>Timeago</label>
|
||||||
|
<div>
|
||||||
|
<label>Language is [[language:name]]</label>
|
||||||
|
<br/>
|
||||||
|
<span id="timeago" class="timeago" title="{now}"></span>
|
||||||
|
<button id="change-language" type="button" class="btn btn-primary">Change Language</button>
|
||||||
|
</div>
|
||||||
|
<hr/>
|
||||||
|
|
||||||
|
<label>Change Skin</label>
|
||||||
|
<select id="change-skin" class="form-control">
|
||||||
|
{{{each skins}}}
|
||||||
|
<option value="{skins.value}">{skins.name}</option>
|
||||||
|
{{{end}}}
|
||||||
|
</select>
|
||||||
|
<hr/>
|
||||||
|
|
||||||
|
<label>Sortable</label>
|
||||||
|
<div>
|
||||||
|
<ul id="sortable-list">
|
||||||
|
<li>Item 1</li>
|
||||||
|
<li>Item 2</li>
|
||||||
|
<li>Item 3</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<hr/>
|
||||||
|
|
||||||
|
<label>Form Serialize</label>
|
||||||
|
<form id="form-serialize">
|
||||||
|
<input name="a" value="1">
|
||||||
|
<input name="a" value="2">
|
||||||
|
<input name="bar" value="test">
|
||||||
|
</form>
|
||||||
|
<pre id="json-form-data"></pre>
|
||||||
|
<hr/>
|
||||||
|
|
||||||
|
<label>Form Deserialize</label>
|
||||||
|
<form id="form-deserialize">
|
||||||
|
<input name="foo" value="">
|
||||||
|
<input name="foo" value="">
|
||||||
|
<input name="moo" value="">
|
||||||
|
</form>
|
||||||
|
<hr/>
|
||||||
@@ -34,7 +34,7 @@ const topicEvents = require('./topics/events');
|
|||||||
const routes = require('./routes');
|
const routes = require('./routes');
|
||||||
const auth = require('./routes/authentication');
|
const auth = require('./routes/authentication');
|
||||||
|
|
||||||
const helpers = require('../public/src/modules/helpers');
|
const helpers = require('../public/src/modules/helpers.common')(require('./utils'), Benchpress, nconf.get('relative_path'));
|
||||||
|
|
||||||
if (nconf.get('ssl')) {
|
if (nconf.get('ssl')) {
|
||||||
server = require('https').createServer({
|
server = require('https').createServer({
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const async = require('async');
|
|||||||
|
|
||||||
const db = require('./mocks/databasemock');
|
const db = require('./mocks/databasemock');
|
||||||
const file = require('../src/file');
|
const file = require('../src/file');
|
||||||
|
const helpers = require('./helpers');
|
||||||
|
|
||||||
describe('minifier', () => {
|
describe('minifier', () => {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
@@ -117,7 +118,7 @@ describe('minifier', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Build', (done) => {
|
describe('Build', () => {
|
||||||
const build = require('../src/meta/build');
|
const build = require('../src/meta/build');
|
||||||
|
|
||||||
before((done) => {
|
before((done) => {
|
||||||
@@ -137,9 +138,8 @@ describe('Build', (done) => {
|
|||||||
it('should build requirejs modules', (done) => {
|
it('should build requirejs modules', (done) => {
|
||||||
build.build(['requirejs modules'], (err) => {
|
build.build(['requirejs modules'], (err) => {
|
||||||
assert.ifError(err);
|
assert.ifError(err);
|
||||||
const filename = path.join(__dirname, '../build/public/src/modules/Chart.js');
|
const filename = path.join(__dirname, '../build/public/src/modules/alerts.js');
|
||||||
assert(file.existsSync(filename));
|
assert(file.existsSync(filename));
|
||||||
assert(fs.readFileSync(filename).toString().startsWith('/*!\n * Chart.js'));
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -147,7 +147,7 @@ describe('Build', (done) => {
|
|||||||
it('should build client js bundle', (done) => {
|
it('should build client js bundle', (done) => {
|
||||||
build.build(['client js bundle'], (err) => {
|
build.build(['client js bundle'], (err) => {
|
||||||
assert.ifError(err);
|
assert.ifError(err);
|
||||||
const filename = path.join(__dirname, '../build/public/nodebb.min.js');
|
const filename = path.join(__dirname, '../build/public/scripts-client.min.js');
|
||||||
assert(file.existsSync(filename));
|
assert(file.existsSync(filename));
|
||||||
assert(fs.readFileSync(filename).length > 1000);
|
assert(fs.readFileSync(filename).length > 1000);
|
||||||
done();
|
done();
|
||||||
@@ -157,7 +157,7 @@ describe('Build', (done) => {
|
|||||||
it('should build admin js bundle', (done) => {
|
it('should build admin js bundle', (done) => {
|
||||||
build.build(['admin js bundle'], (err) => {
|
build.build(['admin js bundle'], (err) => {
|
||||||
assert.ifError(err);
|
assert.ifError(err);
|
||||||
const filename = path.join(__dirname, '../build/public/acp.min.js');
|
const filename = path.join(__dirname, '../build/public/scripts-admin.min.js');
|
||||||
assert(file.existsSync(filename));
|
assert(file.existsSync(filename));
|
||||||
assert(fs.readFileSync(filename).length > 1000);
|
assert(fs.readFileSync(filename).length > 1000);
|
||||||
done();
|
done();
|
||||||
@@ -189,6 +189,25 @@ describe('Build', (done) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/* disabled, doesn't work on gh actions in prod mode
|
||||||
|
it('should build bundle files', function (done) {
|
||||||
|
this.timeout(0);
|
||||||
|
build.buildAll(async (err) => {
|
||||||
|
assert.ifError(err);
|
||||||
|
assert(file.existsSync(path.join(__dirname, '../build/webpack/nodebb.min.js')));
|
||||||
|
assert(file.existsSync(path.join(__dirname, '../build/webpack/admin.min.js')));
|
||||||
|
let { res, body } = await helpers.request('GET', `/assets/nodebb.min.js`, {});
|
||||||
|
assert(res.statusCode, 200);
|
||||||
|
assert(body);
|
||||||
|
({ res, body } = await helpers.request('GET', `/assets/admin.min.js`, {}));
|
||||||
|
assert(res.statusCode, 200);
|
||||||
|
assert(body);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
it('should build templates', function (done) {
|
it('should build templates', function (done) {
|
||||||
this.timeout(0);
|
this.timeout(0);
|
||||||
build.build(['templates'], (err) => {
|
build.build(['templates'], (err) => {
|
||||||
|
|||||||
@@ -718,25 +718,6 @@ describe('Controllers', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should load nodebb.min.js', (done) => {
|
|
||||||
request(`${nconf.get('url')}/assets/nodebb.min.js`, (err, res, body) => {
|
|
||||||
assert.ifError(err);
|
|
||||||
assert.equal(res.statusCode, 200);
|
|
||||||
assert(body);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should load acp.min.js', (done) => {
|
|
||||||
request(`${nconf.get('url')}/assets/acp.min.js`, (err, res, body) => {
|
|
||||||
assert.ifError(err);
|
|
||||||
assert.equal(res.statusCode, 200);
|
|
||||||
assert(body);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should load sitemap.xml', (done) => {
|
it('should load sitemap.xml', (done) => {
|
||||||
request(`${nconf.get('url')}/sitemap.xml`, (err, res, body) => {
|
request(`${nconf.get('url')}/sitemap.xml`, (err, res, body) => {
|
||||||
assert.ifError(err);
|
assert.ifError(err);
|
||||||
@@ -2004,33 +1985,6 @@ describe('Controllers', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('timeago locales', () => {
|
|
||||||
it('should load timeago locale', (done) => {
|
|
||||||
request(`${nconf.get('url')}/assets/src/modules/timeago/locales/jquery.timeago.af.js`, (err, res, body) => {
|
|
||||||
assert.ifError(err);
|
|
||||||
assert.equal(res.statusCode, 200);
|
|
||||||
assert(body.includes('"gelede"'));
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return not found if NodeBB language exists but timeago locale does not exist', (done) => {
|
|
||||||
request(`${nconf.get('url')}/assets/src/modules/timeago/locales/jquery.timeago.ms.js`, (err, res, body) => {
|
|
||||||
assert.ifError(err);
|
|
||||||
assert.equal(res.statusCode, 404);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return not found if NodeBB language does not exist', (done) => {
|
|
||||||
request(`${nconf.get('url')}/assets/src/modules/timeago/locales/jquery.timeago.muggle.js`, (err, res, body) => {
|
|
||||||
assert.ifError(err);
|
|
||||||
assert.equal(res.statusCode, 404);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('category', () => {
|
describe('category', () => {
|
||||||
let jar;
|
let jar;
|
||||||
before(async () => {
|
before(async () => {
|
||||||
@@ -2394,7 +2348,7 @@ describe('Controllers', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should load the composer route', (done) => {
|
it('should load the composer route', (done) => {
|
||||||
request(`${nconf.get('url')}/api/compose`, { json: true }, (err, res, body) => {
|
request(`${nconf.get('url')}/api/compose?cid=1`, { json: true }, (err, res, body) => {
|
||||||
assert.ifError(err);
|
assert.ifError(err);
|
||||||
assert.equal(res.statusCode, 200);
|
assert.equal(res.statusCode, 200);
|
||||||
assert(body.title);
|
assert(body.title);
|
||||||
@@ -2415,7 +2369,7 @@ describe('Controllers', () => {
|
|||||||
method: hookMethod,
|
method: hookMethod,
|
||||||
});
|
});
|
||||||
|
|
||||||
request(`${nconf.get('url')}/api/compose`, { json: true }, (err, res, body) => {
|
request(`${nconf.get('url')}/api/compose?cid=1`, { json: true }, (err, res, body) => {
|
||||||
assert.ifError(err);
|
assert.ifError(err);
|
||||||
assert.equal(res.statusCode, 200);
|
assert.equal(res.statusCode, 200);
|
||||||
assert(body.title);
|
assert(body.title);
|
||||||
@@ -2427,26 +2381,6 @@ describe('Controllers', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should 404 if plugin calls next', (done) => {
|
|
||||||
function hookMethod(hookData, callback) {
|
|
||||||
hookData.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
plugins.hooks.register('myTestPlugin', {
|
|
||||||
hook: 'filter:composer.build',
|
|
||||||
method: hookMethod,
|
|
||||||
});
|
|
||||||
|
|
||||||
request(`${nconf.get('url')}/api/compose`, { json: true }, (err, res, body) => {
|
|
||||||
assert.ifError(err);
|
|
||||||
assert.equal(res.statusCode, 404);
|
|
||||||
|
|
||||||
plugins.hooks.unregister('myTestPlugin', 'filter:composer.build', hookMethod);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should error with invalid data', (done) => {
|
it('should error with invalid data', (done) => {
|
||||||
request.post(`${nconf.get('url')}/compose`, {
|
request.post(`${nconf.get('url')}/compose`, {
|
||||||
form: {
|
form: {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const nconf = require('nconf');
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const winston = require('winston');
|
const winston = require('winston');
|
||||||
|
|
||||||
const utils = require('../../public/src/utils');
|
const utils = require('../../src/utils');
|
||||||
|
|
||||||
const helpers = module.exports;
|
const helpers = module.exports;
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const Groups = require('../src/groups');
|
|||||||
const Messaging = require('../src/messaging');
|
const Messaging = require('../src/messaging');
|
||||||
const helpers = require('./helpers');
|
const helpers = require('./helpers');
|
||||||
const socketModules = require('../src/socket.io/modules');
|
const socketModules = require('../src/socket.io/modules');
|
||||||
const utils = require('../public/src/utils');
|
const utils = require('../src/utils');
|
||||||
const translator = require('../src/translator');
|
const translator = require('../src/translator');
|
||||||
|
|
||||||
describe('Messaging Library', () => {
|
describe('Messaging Library', () => {
|
||||||
|
|||||||
@@ -252,6 +252,7 @@ async function enableDefaultPlugins() {
|
|||||||
const defaultEnabled = [
|
const defaultEnabled = [
|
||||||
'nodebb-plugin-dbsearch',
|
'nodebb-plugin-dbsearch',
|
||||||
'nodebb-widget-essentials',
|
'nodebb-widget-essentials',
|
||||||
|
'nodebb-plugin-composer-default',
|
||||||
].concat(testPlugins);
|
].concat(testPlugins);
|
||||||
|
|
||||||
winston.info('[install/enableDefaultPlugins] activating default plugins', defaultEnabled);
|
winston.info('[install/enableDefaultPlugins] activating default plugins', defaultEnabled);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const posts = require('../../src/posts');
|
|||||||
const user = require('../../src/user');
|
const user = require('../../src/user');
|
||||||
const meta = require('../../src/meta');
|
const meta = require('../../src/meta');
|
||||||
const file = require('../../src/file');
|
const file = require('../../src/file');
|
||||||
const utils = require('../../public/src/utils');
|
const utils = require('../../src/utils');
|
||||||
|
|
||||||
const _filenames = ['abracadabra.png', 'shazam.jpg', 'whoa.gif', 'amazeballs.jpg', 'wut.txt', 'test.bmp'];
|
const _filenames = ['abracadabra.png', 'shazam.jpg', 'whoa.gif', 'amazeballs.jpg', 'wut.txt', 'test.bmp'];
|
||||||
const _recreateFiles = () => {
|
const _recreateFiles = () => {
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
|
|
||||||
const nconf = require('nconf');
|
const nconf = require('nconf');
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
|
const benchpress = require('benchpressjs');
|
||||||
|
|
||||||
const db = require('./mocks/databasemock');
|
const db = require('./mocks/databasemock');
|
||||||
const helpers = require('../public/src/modules/helpers');
|
const helpers = require('../public/src/modules/helpers.common')(require('../src/utils'), benchpress, nconf.get('relative_path'));
|
||||||
|
|
||||||
describe('helpers', () => {
|
describe('helpers', () => {
|
||||||
it('should return false if item doesn\'t exist', (done) => {
|
it('should return false if item doesn\'t exist', (done) => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// For tests relating to Transifex configuration, check i18n.js
|
// For tests relating to Transifex configuration, check i18n.js
|
||||||
|
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const shim = require('../public/src/modules/translator');
|
const shim = require('../src/translator');
|
||||||
|
|
||||||
const { Translator } = shim;
|
const { Translator } = shim;
|
||||||
const db = require('./mocks/databasemock');
|
const db = require('./mocks/databasemock');
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const { JSDOM } = require('jsdom');
|
const { JSDOM } = require('jsdom');
|
||||||
const utils = require('../public/src/utils');
|
|
||||||
const slugify = require('../src/slugify');
|
const slugify = require('../src/slugify');
|
||||||
const db = require('./mocks/databasemock');
|
const db = require('./mocks/databasemock');
|
||||||
|
|
||||||
@@ -11,49 +10,13 @@ describe('Utility Methods', () => {
|
|||||||
// https://gist.github.com/robballou/9ee108758dc5e0e2d028
|
// https://gist.github.com/robballou/9ee108758dc5e0e2d028
|
||||||
// create some jsdom magic to allow jQuery to work
|
// create some jsdom magic to allow jQuery to work
|
||||||
const dom = new JSDOM('<html><body></body></html>');
|
const dom = new JSDOM('<html><body></body></html>');
|
||||||
const { window } = dom;
|
global.window = dom.window;
|
||||||
global.window = window;
|
global.document = dom.window.document;
|
||||||
global.jQuery = require('jquery');
|
global.jQuery = require('jquery');
|
||||||
global.$ = global.jQuery;
|
global.$ = global.jQuery;
|
||||||
const { $ } = global;
|
const { $ } = global;
|
||||||
require('jquery-deserialize');
|
|
||||||
require('jquery-serializeobject');
|
|
||||||
|
|
||||||
it('should serialize/deserialize form data properly', () => {
|
const utils = require('../public/src/utils');
|
||||||
const formSerialize = $(`
|
|
||||||
<form id="form-serialize">
|
|
||||||
<input name="a" value="1">
|
|
||||||
<input name="a" value="2">
|
|
||||||
<input name="bar" value="test">
|
|
||||||
<input name="check1" type="checkbox" checked>
|
|
||||||
<input name="check2" type="checkbox">
|
|
||||||
</form>
|
|
||||||
`);
|
|
||||||
const sampleData = {
|
|
||||||
a: ['1', '2'],
|
|
||||||
bar: 'test',
|
|
||||||
check1: 'on',
|
|
||||||
};
|
|
||||||
const data = formSerialize.serializeObject();
|
|
||||||
assert.deepStrictEqual(data, sampleData);
|
|
||||||
|
|
||||||
const formDeserialize = $(`
|
|
||||||
<form>
|
|
||||||
<input id="input1" name="a"/>
|
|
||||||
<input id="input2" name="a"/>
|
|
||||||
<input id="input3" name="bar"/>
|
|
||||||
<input id="input4" name="check1" type="checkbox">
|
|
||||||
<input id="input5" name="check2" type="checkbox">
|
|
||||||
</form>
|
|
||||||
`);
|
|
||||||
|
|
||||||
formDeserialize.deserialize(sampleData);
|
|
||||||
assert.strictEqual(formDeserialize.find('#input1').val(), sampleData.a[0]);
|
|
||||||
assert.strictEqual(formDeserialize.find('#input2').val(), sampleData.a[1]);
|
|
||||||
assert.strictEqual(formDeserialize.find('#input3').val(), sampleData.bar);
|
|
||||||
assert.strictEqual(formDeserialize.find('#input4').prop('checked'), true);
|
|
||||||
assert.strictEqual(formDeserialize.find('#input5').prop('checked'), false);
|
|
||||||
});
|
|
||||||
|
|
||||||
// https://github.com/jprichardson/string.js/blob/master/test/string.test.js
|
// https://github.com/jprichardson/string.js/blob/master/test/string.test.js
|
||||||
it('should decode HTML entities', (done) => {
|
it('should decode HTML entities', (done) => {
|
||||||
@@ -283,20 +246,10 @@ describe('Utility Methods', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return false if not touch device', (done) => {
|
it('should return false if not touch device', (done) => {
|
||||||
global.document = global.document || {};
|
|
||||||
global.document.documentElement = {};
|
|
||||||
assert(!utils.isTouchDevice());
|
assert(!utils.isTouchDevice());
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return true if touch device', (done) => {
|
|
||||||
global.document.documentElement = {
|
|
||||||
ontouchstart: 1,
|
|
||||||
};
|
|
||||||
assert(utils.isTouchDevice());
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should check if element is in viewport', (done) => {
|
it('should check if element is in viewport', (done) => {
|
||||||
const el = $('<div>some text</div>');
|
const el = $('<div>some text</div>');
|
||||||
assert(utils.isElementInViewport(el));
|
assert(utils.isElementInViewport(el));
|
||||||
@@ -304,7 +257,6 @@ describe('Utility Methods', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should get empty object for url params', (done) => {
|
it('should get empty object for url params', (done) => {
|
||||||
global.document = window.document;
|
|
||||||
const params = utils.params();
|
const params = utils.params();
|
||||||
assert.equal(Object.keys(params), 0);
|
assert.equal(Object.keys(params), 0);
|
||||||
done();
|
done();
|
||||||
|
|||||||
60
webpack.common.js
Normal file
60
webpack.common.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const url = require('url');
|
||||||
|
const nconf = require('nconf');
|
||||||
|
|
||||||
|
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
||||||
|
|
||||||
|
const activePlugins = require('./build/active_plugins.json');
|
||||||
|
|
||||||
|
let relativePath = nconf.get('relative_path');
|
||||||
|
if (relativePath === undefined) {
|
||||||
|
nconf.file({
|
||||||
|
file: path.resolve(__dirname, nconf.any(['config', 'CONFIG']) || 'config.json'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const urlObject = url.parse(nconf.get('url'));
|
||||||
|
relativePath = urlObject.pathname !== '/' ? urlObject.pathname.replace(/\/+$/, '') : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
plugins: [
|
||||||
|
new CleanWebpackPlugin(), // cleans dist folder
|
||||||
|
],
|
||||||
|
entry: {
|
||||||
|
nodebb: './build/public/src/client.js',
|
||||||
|
admin: './build/public/src/admin/admin.js',
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
filename: '[name].min.js',
|
||||||
|
chunkFilename: '[name].[contenthash].js',
|
||||||
|
path: path.resolve(__dirname, 'build/webpack'),
|
||||||
|
publicPath: `${relativePath}/assets/`,
|
||||||
|
},
|
||||||
|
watchOptions: {
|
||||||
|
poll: 500,
|
||||||
|
aggregateTimeout: 250,
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
symlinks: false,
|
||||||
|
modules: [
|
||||||
|
'build/public/src/modules',
|
||||||
|
'build/public/src',
|
||||||
|
'node_modules',
|
||||||
|
...activePlugins.map(p => `node_modules/${p}/node_modules`),
|
||||||
|
],
|
||||||
|
alias: {
|
||||||
|
assets: path.resolve(__dirname, 'build/public'),
|
||||||
|
forum: path.resolve(__dirname, 'build/public/src/client'),
|
||||||
|
admin: path.resolve(__dirname, 'build/public/src/admin'),
|
||||||
|
vendor: path.resolve(__dirname, 'public/vendor'),
|
||||||
|
benchpress: path.resolve(__dirname, 'node_modules/benchpressjs'),
|
||||||
|
Chart: path.resolve(__dirname, 'node_modules/chart.js'),
|
||||||
|
Sortable: path.resolve(__dirname, 'node_modules/sortablejs'),
|
||||||
|
cropper: path.resolve(__dirname, 'node_modules/cropperjs'),
|
||||||
|
'jquery-ui/widgets': path.resolve(__dirname, 'node_modules/jquery-ui/ui/widgets'),
|
||||||
|
'ace/ace': path.resolve(__dirname, 'build/public/src/modules/ace-editor.js'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
9
webpack.dev.js
Normal file
9
webpack.dev.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { merge } = require('webpack-merge');
|
||||||
|
const common = require('./webpack.common');
|
||||||
|
|
||||||
|
module.exports = merge(common, {
|
||||||
|
mode: 'development',
|
||||||
|
// devtool: 'inline-source-map',
|
||||||
|
});
|
||||||
24
webpack.installer.js
Normal file
24
webpack.installer.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// webpack config for webinstaller
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
mode: 'production',
|
||||||
|
entry: {
|
||||||
|
installer: './public/src/installer/install.js',
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
filename: '[name].min.js',
|
||||||
|
path: path.resolve(__dirname, 'build/webpack'),
|
||||||
|
publicPath: `/assets/`,
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
symlinks: false,
|
||||||
|
modules: [
|
||||||
|
'public/src',
|
||||||
|
'node_modules',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
8
webpack.prod.js
Normal file
8
webpack.prod.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { merge } = require('webpack-merge');
|
||||||
|
const common = require('./webpack.common');
|
||||||
|
|
||||||
|
module.exports = merge(common, {
|
||||||
|
mode: 'production',
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user