diff --git a/Gruntfile.js b/Gruntfile.js
index d95d14f7fd..d7201d1b1b 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -137,9 +137,10 @@ module.exports = function (grunt) {
});
const build = require('./src/meta/build');
if (!grunt.option('skip')) {
- await build.build(true);
+ await build.build(true, { webpack: false });
}
run();
+ await build.webpack({ watch: true });
done();
});
@@ -183,7 +184,7 @@ module.exports = function (grunt) {
return run();
}
- require('./src/meta/build').build([compiling], (err) => {
+ require('./src/meta/build').build([compiling], { webpack: false }, (err) => {
if (err) {
winston.error(err.stack);
}
diff --git a/install/package.json b/install/package.json
index 992af9563c..2344c516f5 100644
--- a/install/package.json
+++ b/install/package.json
@@ -60,6 +60,7 @@
"express": "4.17.2",
"express-session": "1.17.2",
"express-useragent": "1.0.15",
+ "file-loader": "6.2.0",
"graceful-fs": "4.2.9",
"helmet": "5.0.2",
"html-to-text": "8.1.0",
@@ -112,7 +113,6 @@
"ioredis": "4.28.5",
"request": "2.88.2",
"request-promise-native": "1.0.9",
- "requirejs": "2.3.6",
"rimraf": "3.0.2",
"rss": "1.2.2",
"sanitize-html": "2.7.0",
@@ -136,6 +136,7 @@
"uglify-es": "3.3.9",
"validator": "13.7.0",
"visibilityjs": "2.0.2",
+ "webpack": "^5.68.0",
"winston": "3.6.0",
"xml": "1.0.1",
"xregexp": "5.1.0",
@@ -159,7 +160,9 @@
"mocha-lcov-reporter": "1.3.0",
"mockdate": "3.0.5",
"nyc": "15.1.0",
- "smtp-server": "3.9.0"
+ "smtp-server": "3.9.0",
+ "webpack-merge": "5.8.0",
+ "clean-webpack-plugin": "4.0.0"
},
"bugs": {
"url": "https://github.com/NodeBB/NodeBB/issues"
diff --git a/public/src/admin/admin.js b/public/src/admin/admin.js
index a91572c009..2e8f661d4e 100644
--- a/public/src/admin/admin.js
+++ b/public/src/admin/admin.js
@@ -227,10 +227,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/');
- });
}());
diff --git a/public/src/admin/advanced/errors.js b/public/src/admin/advanced/errors.js
index b64f110b2a..9f904e2e14 100644
--- a/public/src/admin/advanced/errors.js
+++ b/public/src/admin/advanced/errors.js
@@ -1,7 +1,7 @@
'use strict';
-define('admin/advanced/errors', ['bootbox', 'alerts', 'Chart'], function (bootbox, alerts, Chart) {
+define('admin/advanced/errors', ['bootbox', 'alerts', 'chart.js'], function (bootbox, alerts, Chart) {
const Errors = {};
Errors.init = function () {
diff --git a/public/src/admin/appearance/customise.js b/public/src/admin/appearance/customise.js
index d0a95050fd..5744eeaf02 100644
--- a/public/src/admin/appearance/customise.js
+++ b/public/src/admin/appearance/customise.js
@@ -1,6 +1,6 @@
'use strict';
-define('admin/appearance/customise', ['admin/settings', 'ace/ace'], function (Settings, ace) {
+define('admin/appearance/customise', ['admin/settings', 'ace-editor'], function (Settings, ace) {
const Customise = {};
Customise.init = function () {
diff --git a/public/src/admin/dashboard.js b/public/src/admin/dashboard.js
index dc20cb9c41..026aba91e6 100644
--- a/public/src/admin/dashboard.js
+++ b/public/src/admin/dashboard.js
@@ -2,7 +2,7 @@
define('admin/dashboard', [
- 'Chart', 'translator', 'benchpress', 'bootbox', 'alerts',
+ 'chart.js', 'translator', 'benchpress', 'bootbox', 'alerts',
], function (Chart, translator, Benchpress, bootbox, alerts) {
const Admin = {};
const intervals = {
diff --git a/public/src/admin/extend/plugins.js b/public/src/admin/extend/plugins.js
index 142323c870..cd91dc0fb3 100644
--- a/public/src/admin/extend/plugins.js
+++ b/public/src/admin/extend/plugins.js
@@ -6,7 +6,7 @@ define('admin/extend/plugins', [
'benchpress',
'bootbox',
'alerts',
- 'jquery-ui/widgets/sortable',
+ 'jquery-ui/ui/widgets/sortable',
], function (translator, Benchpress, bootbox, alerts) {
const Plugins = {};
Plugins.init = function () {
diff --git a/public/src/admin/extend/widgets.js b/public/src/admin/extend/widgets.js
index 029bdc4a85..76d73d4d95 100644
--- a/public/src/admin/extend/widgets.js
+++ b/public/src/admin/extend/widgets.js
@@ -4,10 +4,10 @@
define('admin/extend/widgets', [
'bootbox',
'alerts',
- 'jquery-ui/widgets/sortable',
- 'jquery-ui/widgets/draggable',
- 'jquery-ui/widgets/droppable',
- 'jquery-ui/widgets/datepicker',
+ 'jquery-ui/ui/widgets/sortable',
+ 'jquery-ui/ui/widgets/draggable',
+ 'jquery-ui/ui/widgets/droppable',
+ 'jquery-ui/ui/widgets/datepicker',
], function (bootbox, alerts) {
const Widgets = {};
diff --git a/public/src/admin/manage/categories.js b/public/src/admin/manage/categories.js
index 6c2664a09a..c3b682e96f 100644
--- a/public/src/admin/manage/categories.js
+++ b/public/src/admin/manage/categories.js
@@ -5,7 +5,7 @@ define('admin/manage/categories', [
'benchpress',
'categorySelector',
'api',
- 'Sortable',
+ 'sortablejs',
'bootbox',
'alerts',
], function (translator, Benchpress, categorySelector, api, Sortable, bootbox, alerts) {
diff --git a/public/src/admin/manage/category-analytics.js b/public/src/admin/manage/category-analytics.js
index a4366d07d1..1768feabee 100644
--- a/public/src/admin/manage/category-analytics.js
+++ b/public/src/admin/manage/category-analytics.js
@@ -1,7 +1,7 @@
'use strict';
-define('admin/manage/category-analytics', ['Chart'], function (Chart) {
+define('admin/manage/category-analytics', ['chart.js'], function (Chart) {
const CategoryAnalytics = {};
CategoryAnalytics.init = function () {
diff --git a/public/src/admin/modules/dashboard-line-graph.js b/public/src/admin/modules/dashboard-line-graph.js
index 1e11b82e5b..dcfa1db80a 100644
--- a/public/src/admin/modules/dashboard-line-graph.js
+++ b/public/src/admin/modules/dashboard-line-graph.js
@@ -1,6 +1,6 @@
'use strict';
-define('admin/modules/dashboard-line-graph', ['Chart', 'translator', 'benchpress', 'api', 'hooks', 'bootbox'], function (Chart, translator, Benchpress, api, hooks, bootbox) {
+define('admin/modules/dashboard-line-graph', ['chart.js', 'translator', 'benchpress', 'api', 'hooks', 'bootbox'], function (Chart, translator, Benchpress, api, hooks, bootbox) {
const Graph = {
_current: null,
};
diff --git a/public/src/admin/modules/selectable.js b/public/src/admin/modules/selectable.js
index 470462d558..22dee352bc 100644
--- a/public/src/admin/modules/selectable.js
+++ b/public/src/admin/modules/selectable.js
@@ -2,7 +2,7 @@
define('admin/modules/selectable', [
- 'jquery-ui/widgets/selectable',
+ 'jquery-ui/ui/widgets/selectable',
], function () {
const selectable = {};
diff --git a/public/src/admin/settings/email.js b/public/src/admin/settings/email.js
index 20f5d8587d..663b564e83 100644
--- a/public/src/admin/settings/email.js
+++ b/public/src/admin/settings/email.js
@@ -1,7 +1,7 @@
'use strict';
-define('admin/settings/email', ['ace/ace', 'alerts', 'admin/settings'], function (ace, alerts) {
+define('admin/settings/email', ['alerts', 'admin/settings', 'ace-editor'], function (ace, alerts) {
const module = {};
let emailEditor;
diff --git a/public/src/admin/settings/navigation.js b/public/src/admin/settings/navigation.js
index bcd2bccc0d..aca3d530a8 100644
--- a/public/src/admin/settings/navigation.js
+++ b/public/src/admin/settings/navigation.js
@@ -6,9 +6,9 @@ define('admin/settings/navigation', [
'iconSelect',
'benchpress',
'alerts',
- 'jquery-ui/widgets/draggable',
- 'jquery-ui/widgets/droppable',
- 'jquery-ui/widgets/sortable',
+ 'jquery-ui/ui/widgets/draggable',
+ 'jquery-ui/ui/widgets/droppable',
+ 'jquery-ui/ui/widgets/sortable',
], function (translator, iconSelect, Benchpress, alerts) {
const navigation = {};
let available;
diff --git a/public/src/ajaxify.js b/public/src/ajaxify.js
index 04bcb6949e..e6ec0cf0d7 100644
--- a/public/src/ajaxify.js
+++ b/public/src/ajaxify.js
@@ -1,8 +1,14 @@
'use strict';
+import $ from 'jquery';
+import Benchpress from 'benchpressjs';
+import render from './widgets';
+import translator from './modules/translator';
-ajaxify = window.ajaxify || {};
+translator.translate('[[error:no-connection]]');
+window.ajaxify = window.ajaxify || {};
+ajaxify.widgets = { render: render };
(function () {
let apiXHR = null;
let ajaxifyTimer;
@@ -328,14 +334,9 @@ ajaxify = window.ajaxify || {};
};
ajaxify.loadScript = function (tpl_url, callback) {
- let location = !app.inAdmin ? 'forum/' : '';
-
- if (tpl_url.startsWith('admin')) {
- location = '';
- }
const data = {
tpl_url: tpl_url,
- scripts: [location + tpl_url],
+ scripts: [tpl_url],
};
// Hint: useful if you want to load a module on a specific page (append module name to `scripts`)
@@ -352,18 +353,14 @@ ajaxify = window.ajaxify || {};
};
}
if (typeof script === 'string') {
- return function (next) {
- require([script], function (module) {
- // Hint: useful if you want to override a loaded library (e.g. replace core client-side logic),
- // or call a method other than .init()
- hooks.fire('static:script.init', { tpl_url, name: script, module }).then(() => {
- if (module && module.init) {
- module.init();
- }
- next();
- });
- }, function () {
- // ignore 404 error
+ return async function (next) {
+ const module = await importScript(script);
+ // Hint: useful if you want to override a loaded library (e.g. replace core client-side logic),
+ // or call a method other than .init()
+ hooks.fire('static:script.init', { tpl_url, name: script, module }).then(() => {
+ if (module && module.init) {
+ module.init();
+ }
next();
});
};
@@ -386,6 +383,25 @@ ajaxify = window.ajaxify || {};
});
};
+ async function importScript(scriptName) {
+ let pageScript;
+ try {
+ if (scriptName.startsWith('admin/plugins')) {
+ pageScript = await import(/* webpackChunkName: "admin/plugins/[request]" */ 'admin/plugins/' + scriptName.replace(/^admin\/plugins\//, ''));
+ } else if (scriptName.startsWith('admin')) {
+ pageScript = await import(/* webpackChunkName: "admin/[request]" */ 'admin/' + scriptName.replace(/^admin\//, ''));
+ } else if (scriptName.startsWith('forum/plugins')) {
+ pageScript = await import(/* webpackChunkName: "forum/plugins/[request]" */ 'forum/plugins/' + scriptName.replace(/^forum\/plugins\//, ''));
+ } else {
+ pageScript = await import(/* webpackChunkName: "forum/[request]" */ 'forum/' + scriptName);
+ }
+ } catch (err) {
+ console.warn('error loading script' + err.stack);
+ }
+ return pageScript;
+ }
+
+
ajaxify.loadData = function (url, callback) {
url = ajaxify.removeRelativePath(url);
@@ -434,9 +450,24 @@ ajaxify = window.ajaxify || {};
};
ajaxify.loadTemplate = function (template, callback) {
- require([config.assetBaseUrl + '/templates/' + template + '.js'], callback, function (err) {
+ $.ajax({
+ url: `${config.assetBaseUrl}/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);
- throw err;
+ callback(new Error('[[error:unable-to-load-template]]'));
});
};
diff --git a/public/src/app.js b/public/src/app.js
index 92bdccc1bc..716514b72e 100644
--- a/public/src/app.js
+++ b/public/src/app.js
@@ -1,5 +1,24 @@
'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('../../build/public/client-scripts.min');
+
+require('./sockets');
+require('./overrides');
+require('./ajaxify');
app = window.app || {};
@@ -8,6 +27,7 @@ app.currentRoom = null;
app.widgets = {};
app.flags = {};
+
(function () {
let appLoaded = false;
const isTouchDevice = utils.isTouchDevice();
@@ -114,19 +134,6 @@ app.flags = {};
});
};
- app.require = async (modules) => { // allows you to await require.js modules
- const single = !Array.isArray(modules);
- if (single) {
- modules = [modules];
- }
-
- return new Promise((resolve, reject) => {
- require(modules, (...exports) => {
- resolve(single ? exports.pop() : exports);
- }, reject);
- });
- };
-
app.logout = function (redirect) {
console.warn('[deprecated] app.logout is deprecated, please use logout module directly');
require(['logout'], function (logout) {
@@ -322,11 +329,11 @@ app.flags = {};
return callback();
}
require([
- 'jquery-ui/widgets/datepicker',
- 'jquery-ui/widgets/autocomplete',
- 'jquery-ui/widgets/sortable',
- 'jquery-ui/widgets/resizable',
- 'jquery-ui/widgets/draggable',
+ 'jquery-ui/ui/widgets/datepicker',
+ 'jquery-ui/ui/widgets/autocomplete',
+ 'jquery-ui/ui/widgets/sortable',
+ 'jquery-ui/ui/widgets/resizable',
+ 'jquery-ui/ui/widgets/draggable',
], function () {
callback();
});
diff --git a/public/src/client/account/header.js b/public/src/client/account/header.js
index c7880123c9..206760072b 100644
--- a/public/src/client/account/header.js
+++ b/public/src/client/account/header.js
@@ -35,7 +35,7 @@ define('forum/account/header', [
components.get('account/chat').on('click', async function () {
const roomId = await socket.emit('modules.chats.hasPrivateChat', ajaxify.data.uid);
- const chat = await app.require('chat');
+ const chat = await import('chat');
if (roomId) {
chat.openChat(roomId);
} else {
@@ -44,7 +44,7 @@ define('forum/account/header', [
});
components.get('account/new-chat').on('click', async function () {
- const chat = await app.require('chat');
+ const chat = await import('chat');
chat.newChat(ajaxify.data.uid, function () {
components.get('account/chat').parent().removeClass('hidden');
});
diff --git a/public/src/client/flags/list.js b/public/src/client/flags/list.js
index f2e15cedab..5633a8df0f 100644
--- a/public/src/client/flags/list.js
+++ b/public/src/client/flags/list.js
@@ -1,7 +1,7 @@
'use strict';
define('forum/flags/list', [
- 'components', 'Chart', 'categoryFilter', 'autocomplete', 'api', 'alerts',
+ 'components', 'chart.js', 'categoryFilter', 'autocomplete', 'api', 'alerts',
], function (components, Chart, categoryFilter, autocomplete, api, alerts) {
const Flags = {};
diff --git a/public/src/client/ip-blacklist.js b/public/src/client/ip-blacklist.js
index 5fccd07a32..6eb95fed74 100644
--- a/public/src/client/ip-blacklist.js
+++ b/public/src/client/ip-blacklist.js
@@ -1,7 +1,7 @@
'use strict';
-define('forum/ip-blacklist', ['Chart', 'benchpress', 'bootbox', 'alerts'], function (Chart, Benchpress, bootbox, alerts) {
+define('forum/ip-blacklist', ['chart.js', 'benchpress', 'bootbox', 'alerts'], function (Chart, Benchpress, bootbox, alerts) {
const Blacklist = {};
Blacklist.init = function () {
diff --git a/public/src/client/test.js b/public/src/client/test.js
new file mode 100644
index 0000000000..1eaf384423
--- /dev/null
+++ b/public/src/client/test.js
@@ -0,0 +1,101 @@
+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 'jquery-deserialize';
+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);
+ }
+}
\ No newline at end of file
diff --git a/public/src/modules/ace-editor.js b/public/src/modules/ace-editor.js
new file mode 100644
index 0000000000..322878fe44
--- /dev/null
+++ b/public/src/modules/ace-editor.js
@@ -0,0 +1,20 @@
+import ace 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);
+
+
diff --git a/public/src/modules/helpers.common.js b/public/src/modules/helpers.common.js
new file mode 100644
index 0000000000..ae40828ead
--- /dev/null
+++ b/public/src/modules/helpers.common.js
@@ -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 '\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 '\n\t';
+ }
+
+ function stringify(obj) {
+ // Turns the incoming object into a JSON string
+ return JSON.stringify(obj).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 += '' +
+ '