[1/4] WUI: Start implementing a simple JavaScript framework

Message ID 20230401144343.1483-1-hofmann@leo-andres.de
State New
Headers
Series [1/4] WUI: Start implementing a simple JavaScript framework |

Commit Message

Leo-Andres Hofmann April 1, 2023, 2:43 p.m. UTC
  This patch is an effort to modernize and unify the WUI JavaScript.
It will be possible to share e.g. translation strings and common
functions between the different modules.

This uses native ES6 modules that are supported by all major browsers.
Therefore, no change to the toolchain is necessary.

Signed-off-by: Leo-Andres Hofmann <hofmann@leo-andres.de>
---

Hi all,

this patchset brings new modern Javascript modules to the webinterface.

Still on my to-do/wish list:
- run make.sh lang
- convert pakfire.js to a module
- convert/rewrite refreshInetInfo.js, maybe add load displays to all interfaces in index.cgi

I hope this is in a presentable state to see where it is headed.
Let me know what you think and as always thanks for reading/testing/reviewing :)

Best,
Leo


 config/etc/mime.types                        |   1 +
 html/html/include/wui.js                     |  47 ++++++
 html/html/include/wui_core.mjs               | 154 +++++++++++++++++++
 html/html/include/wui_rrdimage.mjs           |  30 ++++
 html/html/themes/ipfire/include/functions.pl |   3 +-
 5 files changed, 234 insertions(+), 1 deletion(-)
 create mode 100644 html/html/include/wui.js
 create mode 100644 html/html/include/wui_core.mjs
 create mode 100644 html/html/include/wui_rrdimage.mjs
  

Comments

Michael Tremer April 11, 2023, 12:58 p.m. UTC | #1
Hello,

Thanks for the patchset.

It is a lot of code to look through indeed.

> On 1 Apr 2023, at 15:43, Leo-Andres Hofmann <hofmann@leo-andres.de> wrote:
> 
> This patch is an effort to modernize and unify the WUI JavaScript.
> It will be possible to share e.g. translation strings and common
> functions between the different modules.
> 
> This uses native ES6 modules that are supported by all major browsers.
> Therefore, no change to the toolchain is necessary.
> 
> Signed-off-by: Leo-Andres Hofmann <hofmann@leo-andres.de>
> ---
> 
> Hi all,
> 
> this patchset brings new modern Javascript modules to the webinterface.

I suppose there is no way around this.

Is this all written from scratch? Did you base this on anything or use some existing code as inspiration?

Are they any existing libraries that could be useful for example pulling in the translation?

-Michael

> Still on my to-do/wish list:
> - run make.sh lang
> - convert pakfire.js to a module
> - convert/rewrite refreshInetInfo.js, maybe add load displays to all interfaces in index.cgi
> 
> I hope this is in a presentable state to see where it is headed.
> Let me know what you think and as always thanks for reading/testing/reviewing :)
> 
> Best,
> Leo
> 
> 
> config/etc/mime.types                        |   1 +
> html/html/include/wui.js                     |  47 ++++++
> html/html/include/wui_core.mjs               | 154 +++++++++++++++++++
> html/html/include/wui_rrdimage.mjs           |  30 ++++
> html/html/themes/ipfire/include/functions.pl |   3 +-
> 5 files changed, 234 insertions(+), 1 deletion(-)
> create mode 100644 html/html/include/wui.js
> create mode 100644 html/html/include/wui_core.mjs
> create mode 100644 html/html/include/wui_rrdimage.mjs
> 
> diff --git a/config/etc/mime.types b/config/etc/mime.types
> index af82d4886..bb3816db9 100644
> --- a/config/etc/mime.types
> +++ b/config/etc/mime.types
> @@ -252,6 +252,7 @@ multipart/voice-message
> text/css css
> text/directory
> text/enriched
> +text/javascript mjs
> text/plain asc txt
> text/prs.lines.tag
> text/rfc822-headers
> diff --git a/html/html/include/wui.js b/html/html/include/wui.js
> new file mode 100644
> index 000000000..e65924e29
> --- /dev/null
> +++ b/html/html/include/wui.js
> @@ -0,0 +1,47 @@
> +/*#############################################################################
> +#                                                                             #
> +# IPFire.org - A linux based firewall                                         #
> +# Copyright (C) 2007-2023  IPFire Team  <info@ipfire.org>                     #
> +#                                                                             #
> +# This program is free software: you can redistribute it and/or modify        #
> +# it under the terms of the GNU General Public License as published by        #
> +# the Free Software Foundation, either version 3 of the License, or           #
> +# (at your option) any later version.                                         #
> +#                                                                             #
> +# This program is distributed in the hope that it will be useful,             #
> +# but WITHOUT ANY WARRANTY; without even the implied warranty of              #
> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               #
> +# GNU General Public License for more details.                                #
> +#                                                                             #
> +# You should have received a copy of the GNU General Public License           #
> +# along with this program.  If not, see <http://www.gnu.org/licenses/>.       #
> +#                                                                             #
> +#############################################################################*/
> +
> +// IPFire Web User Interface
> +// Collection of JavaScript functions and modules (requires jQuery)
> +
> +import {WUIcore_i18n as WUI_i18n} from "./wui_core.mjs";
> +
> +import {WUImodule_rrdimage as WUI_rrdimage} from "./wui_rrdimage.mjs";
> +
> +//--- WUI main class ---
> +class WUImain {
> + constructor() {
> + //- Public properties -
> + // Translation strings
> + this.i18n = new WUI_i18n();
> +
> + //- Modules -
> + // RRDtool graph images
> + this.rrdimage = new WUI_rrdimage(this.i18n);
> +
> + //- Defaults -
> + // These modules are available on every page:
> + this.rrdimage.enabled = true;
> + }
> +}
> +
> +//### Initialize WUI ###
> +const wui = new WUImain();
> +export default wui;
> diff --git a/html/html/include/wui_core.mjs b/html/html/include/wui_core.mjs
> new file mode 100644
> index 000000000..b7b729396
> --- /dev/null
> +++ b/html/html/include/wui_core.mjs
> @@ -0,0 +1,154 @@
> +/*#############################################################################
> +#                                                                             #
> +# IPFire.org - A linux based firewall                                         #
> +# Copyright (C) 2007-2023  IPFire Team  <info@ipfire.org>                     #
> +#                                                                             #
> +# This program is free software: you can redistribute it and/or modify        #
> +# it under the terms of the GNU General Public License as published by        #
> +# the Free Software Foundation, either version 3 of the License, or           #
> +# (at your option) any later version.                                         #
> +#                                                                             #
> +# This program is distributed in the hope that it will be useful,             #
> +# but WITHOUT ANY WARRANTY; without even the implied warranty of              #
> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               #
> +# GNU General Public License for more details.                                #
> +#                                                                             #
> +# You should have received a copy of the GNU General Public License           #
> +# along with this program.  If not, see <http://www. gnu.org/licenses/>.       #
> +#                                                                             #
> +#############################################################################*/
> +
> +// IPFire Web User Interface - JavaScript module
> +// Core functions & module helpers
> +
> +//--- Module template ---
> +// Make sure that overridden functions are still executed with super()!
> +export class WUIcore_moduleBase {
> + //- Private properties -
> + #enabled; // Activation state, disabled by default
> + #readyState; // Loading state similar to Document.readyState
> + #namespace; // Namespace derived from the class name (without "WUImod_" prefix)
> +
> + //- Class constructor -
> + constructor(translations) {
> + this.i18n = translations;
> +
> + this.#enabled = false;
> +
> + this.#readyState = "loading";
> + this.#namespace = this.constructor.name.slice(10);
> + }
> +
> + // Module activation state
> + // Note: Because a module likely changes the DOM, it is not intended that it can
> + // be cleanly disabled again. Disabling it anyway will trigger "_handleModuleDestroy".
> + set enabled(state) {
> + if(this.#enabled !== state) {
> + this.#enabled = state;
> +
> + if(state) {
> + this._handleModuleEnable();
> + } else {
> + this._handleModuleDestroy();
> + }
> + }
> + }
> + get enabled() {
> + return this.#enabled;
> + }
> +
> + // Module loading state
> + // loading: Instance created
> + // interactive: Module enabled, document not ready
> + // complete: DOMContentLoaded/jQuery.ready event fired
> + // destroyed: Module disabled, event handlers removed
> + get readyState() {
> + return this.#readyState;
> + }
> +
> + // Module namespace
> + get namespace() {
> + return this.#namespace;
> + }
> +
> + //### Protected properties ###
> +
> + // Module activation state event handlers
> + _handleModuleEnable() {
> + if(this.#readyState === "loading") {
> + this.#readyState = "interactive";
> +
> + // Attach jQuery.ready event
> + $(this._handleDOMReady.bind(this));
> + }
> + }
> + _handleModuleDestroy() {
> + this.#readyState = "destroyed";
> +
> + // Attempt to remove all event handlers of this module
> + $("body").off(`.${this.namespace}`);
> + }
> +
> + // DOMContentLoaded/jQuery.ready event handler
> + // The module must be enabled for this event to be triggered once
> + _handleDOMReady() {
> + this.#readyState = "complete";
> + }
> +
> + // Translations quick access in module namespace
> + _i18n(key) {
> + return this.i18n.get(key, this.namespace);
> + }
> +}
> +
> +//--- Simple translation strings helper ---
> +export class WUIcore_i18n {
> + //- Private properties -
> + #strings;
> +
> + //- Class constructor -
> + constructor() {
> + this.#strings = Object.create(null); //Object without prototypes
> + }
> +
> + // Default module prefix for general translations
> + get #mainModule() {
> + return "wui";
> + }
> +
> + // Get translation
> + get(key, module = this.#mainModule) {
> + const index = `${module}%${key}`;
> +
> + if(Object.hasOwn(this.#strings, index)) {
> + return this.#strings[index];
> + }
> + return `(missing string '${key}' in '${module}')`;
> + }
> +
> + // Add translation
> + add(key, value, module = this.#mainModule) {
> + if(typeof value === "string" || typeof value === "number") {
> + this.#strings[`${module}%${key}`] = value;
> + }
> + }
> +
> + // Load key/translation JS object
> + // Format: {"key": "translation"}
> + load(translations, module = this.#mainModule) {
> + if(translations instanceof Object) {
> + Object.entries(translations).forEach(([key, value]) => {
> + this.add(key, value, module);
> + });
> + }
> + }
> +}
> +
> +//--- Static utility functions ---
> +export class WUIcore_utilities {
> + // Reload document (clears POST/GET data from history)
> + static reloadDocument() {
> + const url = window.location.origin + window.location.pathname;
> + window.location.replace(url);
> + }
> +}
> diff --git a/html/html/include/wui_rrdimage.mjs b/html/html/include/wui_rrdimage.mjs
> new file mode 100644
> index 000000000..5254b1e98
> --- /dev/null
> +++ b/html/html/include/wui_rrdimage.mjs
> @@ -0,0 +1,30 @@
> +/*#############################################################################
> +#                                                                             #
> +# IPFire.org - A linux based firewall                                         #
> +# Copyright (C) 2007-2023  IPFire Team  <info@ipfire.org>                     #
> +#                                                                             #
> +# This program is free software: you can redistribute it and/or modify        #
> +# it under the terms of the GNU General Public License as published by        #
> +# the Free Software Foundation, either version 3 of the License, or           #
> +# (at your option) any later version.                                         #
> +#                                                                             #
> +# This program is distributed in the hope that it will be useful,             #
> +# but WITHOUT ANY WARRANTY; without even the implied warranty of              #
> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               #
> +# GNU General Public License for more details.                                #
> +#                                                                             #
> +# You should have received a copy of the GNU General Public License           #
> +# along with this program.  If not, see <http://www.gnu.org/licenses/>.       #
> +#                                                                             #
> +#############################################################################*/
> +
> +// IPFire Web User Interface - JavaScript module
> +
> +import {WUIcore_moduleBase as WUI_module} from "./wui_core.mjs";
> +
> +//--- RRDtool graph images ---
> +export class WUImodule_rrdimage extends WUI_module {
> + constructor(translations) {
> + super(translations);
> + }
> +}
> diff --git a/html/html/themes/ipfire/include/functions.pl b/html/html/themes/ipfire/include/functions.pl
> index cbd05d109..445597e51 100644
> --- a/html/html/themes/ipfire/include/functions.pl
> +++ b/html/html/themes/ipfire/include/functions.pl
> @@ -113,7 +113,8 @@ print <<END;
> <title>$headline - $title</title>
> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
> <link rel="shortcut icon" href="/favicon.ico" />
> - <script type="text/javascript" src="/include/jquery.js"></script>
> + <script src="/include/jquery.js"></script>
> + <script src="/include/wui.js" type="module"></script>
> <script src="/include/rrdimage.js"></script>
> 
> $extrahead
> -- 
> 2.37.1.windows.1
>
  
Leo-Andres Hofmann April 12, 2023, 7:14 a.m. UTC | #2
Hi Michael,

Am 11.04.2023 um 14:58 schrieb Michael Tremer:
> Hello,
> 
> Thanks for the patchset.
> 
> It is a lot of code to look through indeed.
Yes, I'm aware this is a bit unannounced and a lot of code. Thank you for looking at it!
>> On 1 Apr 2023, at 15:43, Leo-Andres Hofmann <hofmann@leo-andres.de> wrote:
>> 
>> This patch is an effort to modernize and unify the WUI JavaScript.
>> It will be possible to share e.g. translation strings and common
>> functions between the different modules.
>> 
>> This uses native ES6 modules that are supported by all major browsers.
>> Therefore, no change to the toolchain is necessary.
>> 
>> Signed-off-by: Leo-Andres Hofmann <hofmann@leo-andres.de>
>> ---
>> 
>> Hi all,
>> 
>> this patchset brings new modern Javascript modules to the webinterface.
> I suppose there is no way around this.
My thinking was: At the moment, the web interface is quite old-fashined and uses relatively little javascript, scattered in multiple files.
But new features are still added on a fairly regular basis. So I think it makes sense to start a good structure now rather than later.
> Is this all written from scratch? Did you base this on anything or use some existing code as inspiration?
This is written from scratch, in the sense that it is not just a modified copy of some existing library.
However, I have read the documentation of other libraries before, and there are only so many reasonable ways to implement this.
So I haven't tried to reinvent the wheel, and you'll certainly be able to find similarities to existing libraries. But the actual source code in this patchset is 100% my own work.

- The module.enable/readyState logic is inspired by jQuery's .ready and the HTML DOM readyState property
- The i18n class is based on the work I did for pakfire.js and extended to work with the module concept
- To learn how to create the CSS overlay window, I followed the w3schools tutorial "Overlay"
- The async/await interface for the dialog box is inspired by jQuery's deferred.then() interface for Ajax calls, but implemented with native JS
> Are they any existing libraries that could be useful for example pulling in the translation?
I looked at a few popular libraries, but in my opinion they are too extensive and do not fit well with the existing web interface. (jquery.i18n, Polyglot, ...)
For example, some libraries I found prefer to load the texts from a JSON file. This would result in additional language files parallel to the existing system.
Besides, dynamically changing translations based on user agent settings may break the form field mapping: if ($cgiparams{'ACTION'} eq $Lang::tr{'save'} )...
If we didn't have to pay attention to many specifics of the old CGIs, I would also prefer an existing library.

Best,
Leo
> 
> 
> -Michael
> 
>> Still on my to-do/wish list:
>> - run make.sh lang
>> - convert pakfire.js to a module
>> - convert/rewrite refreshInetInfo.js, maybe add load displays to all interfaces in index.cgi
>> 
>> I hope this is in a presentable state to see where it is headed.
>> Let me know what you think and as always thanks for reading/testing/reviewing :)
>> 
>> Best,
>> Leo
>> 
>> 
>> config/etc/mime.types                        |   1 +
>> html/html/include/wui.js                     |  47 ++++++
>> html/html/include/wui_core.mjs               | 154 +++++++++++++++++++
>> html/html/include/wui_rrdimage.mjs           |  30 ++++
>> html/html/themes/ipfire/include/functions.pl |   3 +-
>> 5 files changed, 234 insertions(+), 1 deletion(-)
>> create mode 100644 html/html/include/wui.js
>> create mode 100644 html/html/include/wui_core.mjs
>> create mode 100644 html/html/include/wui_rrdimage.mjs
>> 
>> diff --git a/config/etc/mime.types b/config/etc/mime.types
>> index af82d4886..bb3816db9 100644
>> --- a/config/etc/mime.types
>> +++ b/config/etc/mime.types
>> @@ -252,6 +252,7 @@ multipart/voice-message
>> text/css css
>> text/directory
>> text/enriched
>> +text/javascript mjs
>> text/plain asc txt
>> text/prs.lines.tag
>> text/rfc822-headers
>> diff --git a/html/html/include/wui.js b/html/html/include/wui.js
>> new file mode 100644
>> index 000000000..e65924e29
>> --- /dev/null
>> +++ b/html/html/include/wui.js
>> @@ -0,0 +1,47 @@
>> +/*#############################################################################
>> +#                                                                             #
>> +# IPFire.org - A linux based firewall                                         #
>> +# Copyright (C) 2007-2023  IPFire Team  <info@ipfire.org>                     #
>> +#                                                                             #
>> +# This program is free software: you can redistribute it and/or modify        #
>> +# it under the terms of the GNU General Public License as published by        #
>> +# the Free Software Foundation, either version 3 of the License, or           #
>> +# (at your option) any later version.                                         #
>> +#                                                                             #
>> +# This program is distributed in the hope that it will be useful,             #
>> +# but WITHOUT ANY WARRANTY; without even the implied warranty of              #
>> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               #
>> +# GNU General Public License for more details.                                #
>> +#                                                                             #
>> +# You should have received a copy of the GNU General Public License           #
>> +# along with this program.  If not, see <http://www.gnu.org/licenses/>.       #
>> +#                                                                             #
>> +#############################################################################*/
>> +
>> +// IPFire Web User Interface
>> +// Collection of JavaScript functions and modules (requires jQuery)
>> +
>> +import {WUIcore_i18n as WUI_i18n} from "./wui_core.mjs";
>> +
>> +import {WUImodule_rrdimage as WUI_rrdimage} from "./wui_rrdimage.mjs";
>> +
>> +//--- WUI main class ---
>> +class WUImain {
>> + constructor() {
>> + //- Public properties -
>> + // Translation strings
>> + this.i18n = new WUI_i18n();
>> +
>> + //- Modules -
>> + // RRDtool graph images
>> + this.rrdimage = new WUI_rrdimage(this.i18n);
>> +
>> + //- Defaults -
>> + // These modules are available on every page:
>> + this.rrdimage.enabled = true;
>> + }
>> +}
>> +
>> +//### Initialize WUI ###
>> +const wui = new WUImain();
>> +export default wui;
>> diff --git a/html/html/include/wui_core.mjs b/html/html/include/wui_core.mjs
>> new file mode 100644
>> index 000000000..b7b729396
>> --- /dev/null
>> +++ b/html/html/include/wui_core.mjs
>> @@ -0,0 +1,154 @@
>> +/*#############################################################################
>> +#                                                                             #
>> +# IPFire.org - A linux based firewall                                         #
>> +# Copyright (C) 2007-2023  IPFire Team  <info@ipfire.org>                     #
>> +#                                                                             #
>> +# This program is free software: you can redistribute it and/or modify        #
>> +# it under the terms of the GNU General Public License as published by        #
>> +# the Free Software Foundation, either version 3 of the License, or           #
>> +# (at your option) any later version.                                         #
>> +#                                                                             #
>> +# This program is distributed in the hope that it will be useful,             #
>> +# but WITHOUT ANY WARRANTY; without even the implied warranty of              #
>> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               #
>> +# GNU General Public License for more details.                                #
>> +#                                                                             #
>> +# You should have received a copy of the GNU General Public License           #
>> +# along with this program.  If not, see <http://www. gnu.org/licenses/>[http://www.gnu.org/licenses/].       #
>> +#                                                                             #
>> +#############################################################################*/
>> +
>> +// IPFire Web User Interface - JavaScript module
>> +// Core functions & module helpers
>> +
>> +//--- Module template ---
>> +// Make sure that overridden functions are still executed with super()!
>> +export class WUIcore_moduleBase {
>> + //- Private properties -
>> + #enabled; // Activation state, disabled by default
>> + #readyState; // Loading state similar to Document.readyState
>> + #namespace; // Namespace derived from the class name (without "WUImod_" prefix)
>> +
>> + //- Class constructor -
>> + constructor(translations) {
>> + this.i18n = translations;
>> +
>> + this.#enabled = false;
>> +
>> + this.#readyState = "loading";
>> + this.#namespace = this.constructor.name.slice(10);
>> + }
>> +
>> + // Module activation state
>> + // Note: Because a module likely changes the DOM, it is not intended that it can
>> + // be cleanly disabled again. Disabling it anyway will trigger "_handleModuleDestroy".
>> + set enabled(state) {
>> + if(this.#enabled !== state) {
>> + this.#enabled = state;
>> +
>> + if(state) {
>> + this._handleModuleEnable();
>> + } else {
>> + this._handleModuleDestroy();
>> + }
>> + }
>> + }
>> + get enabled() {
>> + return this.#enabled;
>> + }
>> +
>> + // Module loading state
>> + // loading: Instance created
>> + // interactive: Module enabled, document not ready
>> + // complete: DOMContentLoaded/jQuery.ready event fired
>> + // destroyed: Module disabled, event handlers removed
>> + get readyState() {
>> + return this.#readyState;
>> + }
>> +
>> + // Module namespace
>> + get namespace() {
>> + return this.#namespace;
>> + }
>> +
>> + //### Protected properties ###
>> +
>> + // Module activation state event handlers
>> + _handleModuleEnable() {
>> + if(this.#readyState === "loading") {
>> + this.#readyState = "interactive";
>> +
>> + // Attach jQuery.ready event
>> + $(this._handleDOMReady.bind(this));
>> + }
>> + }
>> + _handleModuleDestroy() {
>> + this.#readyState = "destroyed";
>> +
>> + // Attempt to remove all event handlers of this module
>> + $("body").off(`.${this.namespace}`);
>> + }
>> +
>> + // DOMContentLoaded/jQuery.ready event handler
>> + // The module must be enabled for this event to be triggered once
>> + _handleDOMReady() {
>> + this.#readyState = "complete";
>> + }
>> +
>> + // Translations quick access in module namespace
>> + _i18n(key) {
>> + return this.i18n.get(key, this.namespace);
>> + }
>> +}
>> +
>> +//--- Simple translation strings helper ---
>> +export class WUIcore_i18n {
>> + //- Private properties -
>> + #strings;
>> +
>> + //- Class constructor -
>> + constructor() {
>> + this.#strings = Object.create(null); //Object without prototypes
>> + }
>> +
>> + // Default module prefix for general translations
>> + get #mainModule() {
>> + return "wui";
>> + }
>> +
>> + // Get translation
>> + get(key, module = this.#mainModule) {
>> + const index = `${module}%${key}`;
>> +
>> + if(Object.hasOwn(this.#strings, index)) {
>> + return this.#strings[index];
>> + }
>> + return `(missing string '${key}' in '${module}')`;
>> + }
>> +
>> + // Add translation
>> + add(key, value, module = this.#mainModule) {
>> + if(typeof value === "string" || typeof value === "number") {
>> + this.#strings[`${module}%${key}`] = value;
>> + }
>> + }
>> +
>> + // Load key/translation JS object
>> + // Format: {"key": "translation"}
>> + load(translations, module = this.#mainModule) {
>> + if(translations instanceof Object) {
>> + Object.entries(translations).forEach(([key, value]) => {
>> + this.add(key, value, module);
>> + });
>> + }
>> + }
>> +}
>> +
>> +//--- Static utility functions ---
>> +export class WUIcore_utilities {
>> + // Reload document (clears POST/GET data from history)
>> + static reloadDocument() {
>> + const url = window.location.origin + window.location.pathname;
>> + window.location.replace(url);
>> + }
>> +}
>> diff --git a/html/html/include/wui_rrdimage.mjs b/html/html/include/wui_rrdimage.mjs
>> new file mode 100644
>> index 000000000..5254b1e98
>> --- /dev/null
>> +++ b/html/html/include/wui_rrdimage.mjs
>> @@ -0,0 +1,30 @@
>> +/*#############################################################################
>> +#                                                                             #
>> +# IPFire.org - A linux based firewall                                         #
>> +# Copyright (C) 2007-2023  IPFire Team  <info@ipfire.org>                     #
>> +#                                                                             #
>> +# This program is free software: you can redistribute it and/or modify        #
>> +# it under the terms of the GNU General Public License as published by        #
>> +# the Free Software Foundation, either version 3 of the License, or           #
>> +# (at your option) any later version.                                         #
>> +#                                                                             #
>> +# This program is distributed in the hope that it will be useful,             #
>> +# but WITHOUT ANY WARRANTY; without even the implied warranty of              #
>> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               #
>> +# GNU General Public License for more details.                                #
>> +#                                                                             #
>> +# You should have received a copy of the GNU General Public License           #
>> +# along with this program.  If not, see <http://www.gnu.org/licenses/>.       #
>> +#                                                                             #
>> +#############################################################################*/
>> +
>> +// IPFire Web User Interface - JavaScript module
>> +
>> +import {WUIcore_moduleBase as WUI_module} from "./wui_core.mjs";
>> +
>> +//--- RRDtool graph images ---
>> +export class WUImodule_rrdimage extends WUI_module {
>> + constructor(translations) {
>> + super(translations);
>> + }
>> +}
>> diff --git a/html/html/themes/ipfire/include/functions.pl b/html/html/themes/ipfire/include/functions.pl
>> index cbd05d109..445597e51 100644
>> --- a/html/html/themes/ipfire/include/functions.pl
>> +++ b/html/html/themes/ipfire/include/functions.pl
>> @@ -113,7 +113,8 @@ print <<END;
>> <title>$headline - $title</title>
>> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
>> <link rel="shortcut icon" href="/favicon.ico" />
>> - <script type="text/javascript" src="/include/jquery.js"></script>
>> + <script src="/include/jquery.js"></script>
>> + <script src="/include/wui.js" type="module"></script>
>> <script src="/include/rrdimage.js"></script>
>> 
>> $extrahead
>> -- 
>> 2.37.1.windows.1
>>
  
Adolf Belka April 13, 2023, 11:42 a.m. UTC | #3
Hi Leo,

On 12/04/2023 09:14, Leo-Andres Hofmann wrote:
>
> Hi Michael,
>
> Am 11.04.2023 um 14:58 schrieb Michael Tremer:
>> Hello,
>>
>> Thanks for the patchset.
>>
>> It is a lot of code to look through indeed.
> Yes, I'm aware this is a bit unannounced and a lot of code. Thank you 
> for looking at it!
>>> On 1 Apr 2023, at 15:43, Leo-Andres Hofmann<hofmann@leo-andres.de>  wrote:
>>>
>>> This patch is an effort to modernize and unify the WUI JavaScript.
>>> It will be possible to share e.g. translation strings and common
>>> functions between the different modules.
>>>
>>> This uses native ES6 modules that are supported by all major browsers.
>>> Therefore, no change to the toolchain is necessary.
>>>
>>> Signed-off-by: Leo-Andres Hofmann<hofmann@leo-andres.de>
>>> ---
>>>
>>> Hi all,
>>>
>>> this patchset brings new modern Javascript modules to the webinterface.
>> I suppose there is no way around this.
> My thinking was: At the moment, the web interface is quite 
> old-fashined and uses relatively little javascript, scattered in 
> multiple files.
> But new features are still added on a fairly regular basis. So I think 
> it makes sense to start a good structure now rather than later.
>> Is this all written from scratch? Did you base this on anything or use some existing code as inspiration?
> This is written from scratch, in the sense that it is not just a 
> modified copy of some existing library.
> However, I have read the documentation of other libraries before, and 
> there are only so many reasonable ways to implement this.
> So I haven't tried to reinvent the wheel, and you'll certainly be able 
> to find similarities to existing libraries. But the actual source code 
> in this patchset is 100% my own work.
>
> - The module.enable/readyState logic is inspired by jQuery's .ready 
> and the HTML DOM readyState property
> - The i18n class is based on the work I did for pakfire.js and 
> extended to work with the module concept
> - To learn how to create the CSS overlay window, I followed the 
> w3schools tutorial "Overlay"
I have done some changes on the dhcp.cgi and urlxlrator.cgi pages to 
replace bgcolor, which was deprecated in HTML 4.x and no longer 
supported at all in HTML 5, with its CSS equivalent approach. I used 
your CSS approach on the zone config cgi page as the basis for what I 
changed.

My plan was to go through the other cgi pages doing the same type of 
changes.

Should I hold on that until you have made this change to the CSS 
approach with the CSS Overlay?

Regards,
Adolf.
> - The async/await interface for the dialog box is inspired by jQuery's 
> deferred.then() interface for Ajax calls, but implemented with native JS
>> Are they any existing libraries that could be useful for example pulling in the translation?
> I looked at a few popular libraries, but in my opinion they are too 
> extensive and do not fit well with the existing web interface. 
> (jquery.i18n, Polyglot, ...)
> For example, some libraries I found prefer to load the texts from a 
> JSON file. This would result in additional language files parallel to 
> the existing system.
> Besides, dynamically changing translations based on user agent 
> settings may break the form field mapping: if ($cgiparams{'ACTION'} eq 
> $Lang::tr{'save'} )...
> If we didn't have to pay attention to many specifics of the old CGIs, 
> I would also prefer an existing library.
>
> Best,
> Leo
>> -Michael
>>
>>> Still on my to-do/wish list:
>>> - run make.sh lang
>>> - convert pakfire.js to a module
>>> - convert/rewrite refreshInetInfo.js, maybe add load displays to all interfaces in index.cgi
>>>
>>> I hope this is in a presentable state to see where it is headed.
>>> Let me know what you think and as always thanks for reading/testing/reviewing :)
>>>
>>> Best,
>>> Leo
>>>
>>>
>>> config/etc/mime.types                        |   1 +
>>> html/html/include/wui.js                     |  47 ++++++
>>> html/html/include/wui_core.mjs               | 154 +++++++++++++++++++
>>> html/html/include/wui_rrdimage.mjs           |  30 ++++
>>> html/html/themes/ipfire/include/functions.pl |   3 +-
>>> 5 files changed, 234 insertions(+), 1 deletion(-)
>>> create mode 100644 html/html/include/wui.js
>>> create mode 100644 html/html/include/wui_core.mjs
>>> create mode 100644 html/html/include/wui_rrdimage.mjs
>>>
>>> diff --git a/config/etc/mime.types b/config/etc/mime.types
>>> index af82d4886..bb3816db9 100644
>>> --- a/config/etc/mime.types
>>> +++ b/config/etc/mime.types
>>> @@ -252,6 +252,7 @@ multipart/voice-message
>>> text/css css
>>> text/directory
>>> text/enriched
>>> +text/javascript mjs
>>> text/plain asc txt
>>> text/prs.lines.tag
>>> text/rfc822-headers
>>> diff --git a/html/html/include/wui.js b/html/html/include/wui.js
>>> new file mode 100644
>>> index 000000000..e65924e29
>>> --- /dev/null
>>> +++ b/html/html/include/wui.js
>>> @@ -0,0 +1,47 @@
>>> +/*#############################################################################
>>> +#                                                                             #
>>> +# IPFire.org - A linux based firewall                                         #
>>> +# Copyright (C) 2007-2023  IPFire Team<info@ipfire.org>                      #
>>> +#                                                                             #
>>> +# This program is free software: you can redistribute it and/or modify        #
>>> +# it under the terms of the GNU General Public License as published by        #
>>> +# the Free Software Foundation, either version 3 of the License, or           #
>>> +# (at your option) any later version.                                         #
>>> +#                                                                             #
>>> +# This program is distributed in the hope that it will be useful,             #
>>> +# but WITHOUT ANY WARRANTY; without even the implied warranty of              #
>>> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               #
>>> +# GNU General Public License for more details.                                #
>>> +#                                                                             #
>>> +# You should have received a copy of the GNU General Public License           #
>>> +# along with this program.  If not, see<http://www.gnu.org/licenses/>.       #
>>> +#                                                                             #
>>> +#############################################################################*/
>>> +
>>> +// IPFire Web User Interface
>>> +// Collection of JavaScript functions and modules (requires jQuery)
>>> +
>>> +import {WUIcore_i18n as WUI_i18n} from "./wui_core.mjs";
>>> +
>>> +import {WUImodule_rrdimage as WUI_rrdimage} from "./wui_rrdimage.mjs";
>>> +
>>> +//--- WUI main class ---
>>> +class WUImain {
>>> + constructor() {
>>> + //- Public properties -
>>> + // Translation strings
>>> + this.i18n = new WUI_i18n();
>>> +
>>> + //- Modules -
>>> + // RRDtool graph images
>>> + this.rrdimage = new WUI_rrdimage(this.i18n);
>>> +
>>> + //- Defaults -
>>> + // These modules are available on every page:
>>> + this.rrdimage.enabled = true;
>>> + }
>>> +}
>>> +
>>> +//### Initialize WUI ###
>>> +const wui = new WUImain();
>>> +export default wui;
>>> diff --git a/html/html/include/wui_core.mjs b/html/html/include/wui_core.mjs
>>> new file mode 100644
>>> index 000000000..b7b729396
>>> --- /dev/null
>>> +++ b/html/html/include/wui_core.mjs
>>> @@ -0,0 +1,154 @@
>>> +/*#############################################################################
>>> +#                                                                             #
>>> +# IPFire.org - A linux based firewall                                         #
>>> +# Copyright (C) 2007-2023  IPFire Team<info@ipfire.org>                      #
>>> +#                                                                             #
>>> +# This program is free software: you can redistribute it and/or modify        #
>>> +# it under the terms of the GNU General Public License as published by        #
>>> +# the Free Software Foundation, either version 3 of the License, or           #
>>> +# (at your option) any later version.                                         #
>>> +#                                                                             #
>>> +# This program is distributed in the hope that it will be useful,             #
>>> +# but WITHOUT ANY WARRANTY; without even the implied warranty of              #
>>> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               #
>>> +# GNU General Public License for more details.                                #
>>> +#                                                                             #
>>> +# You should have received a copy of the GNU General Public License           #
>>> +# along with this program.  If not, see<http://www. gnu.org/licenses/>.       #
>>> +#                                                                             #
>>> +#############################################################################*/
>>> +
>>> +// IPFire Web User Interface - JavaScript module
>>> +// Core functions & module helpers
>>> +
>>> +//--- Module template ---
>>> +// Make sure that overridden functions are still executed with super()!
>>> +export class WUIcore_moduleBase {
>>> + //- Private properties -
>>> + #enabled; // Activation state, disabled by default
>>> + #readyState; // Loading state similar to Document.readyState
>>> + #namespace; // Namespace derived from the class name (without "WUImod_" prefix)
>>> +
>>> + //- Class constructor -
>>> + constructor(translations) {
>>> + this.i18n = translations;
>>> +
>>> + this.#enabled = false;
>>> +
>>> + this.#readyState = "loading";
>>> + this.#namespace = this.constructor.name.slice(10);
>>> + }
>>> +
>>> + // Module activation state
>>> + // Note: Because a module likely changes the DOM, it is not intended that it can
>>> + // be cleanly disabled again. Disabling it anyway will trigger "_handleModuleDestroy".
>>> + set enabled(state) {
>>> + if(this.#enabled !== state) {
>>> + this.#enabled = state;
>>> +
>>> + if(state) {
>>> + this._handleModuleEnable();
>>> + } else {
>>> + this._handleModuleDestroy();
>>> + }
>>> + }
>>> + }
>>> + get enabled() {
>>> + return this.#enabled;
>>> + }
>>> +
>>> + // Module loading state
>>> + // loading: Instance created
>>> + // interactive: Module enabled, document not ready
>>> + // complete: DOMContentLoaded/jQuery.ready event fired
>>> + // destroyed: Module disabled, event handlers removed
>>> + get readyState() {
>>> + return this.#readyState;
>>> + }
>>> +
>>> + // Module namespace
>>> + get namespace() {
>>> + return this.#namespace;
>>> + }
>>> +
>>> + //### Protected properties ###
>>> +
>>> + // Module activation state event handlers
>>> + _handleModuleEnable() {
>>> + if(this.#readyState === "loading") {
>>> + this.#readyState = "interactive";
>>> +
>>> + // Attach jQuery.ready event
>>> + $(this._handleDOMReady.bind(this));
>>> + }
>>> + }
>>> + _handleModuleDestroy() {
>>> + this.#readyState = "destroyed";
>>> +
>>> + // Attempt to remove all event handlers of this module
>>> + $("body").off(`.${this.namespace}`);
>>> + }
>>> +
>>> + // DOMContentLoaded/jQuery.ready event handler
>>> + // The module must be enabled for this event to be triggered once
>>> + _handleDOMReady() {
>>> + this.#readyState = "complete";
>>> + }
>>> +
>>> + // Translations quick access in module namespace
>>> + _i18n(key) {
>>> + return this.i18n.get(key, this.namespace);
>>> + }
>>> +}
>>> +
>>> +//--- Simple translation strings helper ---
>>> +export class WUIcore_i18n {
>>> + //- Private properties -
>>> + #strings;
>>> +
>>> + //- Class constructor -
>>> + constructor() {
>>> + this.#strings = Object.create(null); //Object without prototypes
>>> + }
>>> +
>>> + // Default module prefix for general translations
>>> + get #mainModule() {
>>> + return "wui";
>>> + }
>>> +
>>> + // Get translation
>>> + get(key, module = this.#mainModule) {
>>> + const index = `${module}%${key}`;
>>> +
>>> + if(Object.hasOwn(this.#strings, index)) {
>>> + return this.#strings[index];
>>> + }
>>> + return `(missing string '${key}' in '${module}')`;
>>> + }
>>> +
>>> + // Add translation
>>> + add(key, value, module = this.#mainModule) {
>>> + if(typeof value === "string" || typeof value === "number") {
>>> + this.#strings[`${module}%${key}`] = value;
>>> + }
>>> + }
>>> +
>>> + // Load key/translation JS object
>>> + // Format: {"key": "translation"}
>>> + load(translations, module = this.#mainModule) {
>>> + if(translations instanceof Object) {
>>> + Object.entries(translations).forEach(([key, value]) => {
>>> + this.add(key, value, module);
>>> + });
>>> + }
>>> + }
>>> +}
>>> +
>>> +//--- Static utility functions ---
>>> +export class WUIcore_utilities {
>>> + // Reload document (clears POST/GET data from history)
>>> + static reloadDocument() {
>>> + const url = window.location.origin + window.location.pathname;
>>> + window.location.replace(url);
>>> + }
>>> +}
>>> diff --git a/html/html/include/wui_rrdimage.mjs b/html/html/include/wui_rrdimage.mjs
>>> new file mode 100644
>>> index 000000000..5254b1e98
>>> --- /dev/null
>>> +++ b/html/html/include/wui_rrdimage.mjs
>>> @@ -0,0 +1,30 @@
>>> +/*#############################################################################
>>> +#                                                                             #
>>> +# IPFire.org - A linux based firewall                                         #
>>> +# Copyright (C) 2007-2023  IPFire Team<info@ipfire.org>                      #
>>> +#                                                                             #
>>> +# This program is free software: you can redistribute it and/or modify        #
>>> +# it under the terms of the GNU General Public License as published by        #
>>> +# the Free Software Foundation, either version 3 of the License, or           #
>>> +# (at your option) any later version.                                         #
>>> +#                                                                             #
>>> +# This program is distributed in the hope that it will be useful,             #
>>> +# but WITHOUT ANY WARRANTY; without even the implied warranty of              #
>>> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               #
>>> +# GNU General Public License for more details.                                #
>>> +#                                                                             #
>>> +# You should have received a copy of the GNU General Public License           #
>>> +# along with this program.  If not, see<http://www.gnu.org/licenses/>.       #
>>> +#                                                                             #
>>> +#############################################################################*/
>>> +
>>> +// IPFire Web User Interface - JavaScript module
>>> +
>>> +import {WUIcore_moduleBase as WUI_module} from "./wui_core.mjs";
>>> +
>>> +//--- RRDtool graph images ---
>>> +export class WUImodule_rrdimage extends WUI_module {
>>> + constructor(translations) {
>>> + super(translations);
>>> + }
>>> +}
>>> diff --git a/html/html/themes/ipfire/include/functions.pl b/html/html/themes/ipfire/include/functions.pl
>>> index cbd05d109..445597e51 100644
>>> --- a/html/html/themes/ipfire/include/functions.pl
>>> +++ b/html/html/themes/ipfire/include/functions.pl
>>> @@ -113,7 +113,8 @@ print <<END;
>>> <title>$headline - $title</title>
>>> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
>>> <link rel="shortcut icon" href="/favicon.ico" />
>>> - <script type="text/javascript" src="/include/jquery.js"></script>
>>> + <script src="/include/jquery.js"></script>
>>> + <script src="/include/wui.js" type="module"></script>
>>> <script src="/include/rrdimage.js"></script>
>>>
>>> $extrahead
>>> -- 
>>> 2.37.1.windows.1
>>>
  
Leo-Andres Hofmann April 13, 2023, 3:21 p.m. UTC | #4
Hi Adolf,

Thanks for the heads up!

Am 13.04.2023 um 13:42 schrieb Adolf Belka:
> Hi Leo,
>
> On 12/04/2023 09:14, Leo-Andres Hofmann wrote:
>>
>> Hi Michael,
>>
>> Am 11.04.2023 um 14:58 schrieb Michael Tremer:
>>> Hello,
>>>
>>> Thanks for the patchset.
>>>
>>> It is a lot of code to look through indeed.
>> Yes, I'm aware this is a bit unannounced and a lot of code. Thank you 
>> for looking at it!
>>>> On 1 Apr 2023, at 15:43, Leo-Andres Hofmann<hofmann@leo-andres.de>  
>>>> wrote:
>>>>
>>>> This patch is an effort to modernize and unify the WUI JavaScript.
>>>> It will be possible to share e.g. translation strings and common
>>>> functions between the different modules.
>>>>
>>>> This uses native ES6 modules that are supported by all major browsers.
>>>> Therefore, no change to the toolchain is necessary.
>>>>
>>>> Signed-off-by: Leo-Andres Hofmann<hofmann@leo-andres.de>
>>>> ---
>>>>
>>>> Hi all,
>>>>
>>>> this patchset brings new modern Javascript modules to the 
>>>> webinterface.
>>> I suppose there is no way around this.
>> My thinking was: At the moment, the web interface is quite 
>> old-fashined and uses relatively little javascript, scattered in 
>> multiple files.
>> But new features are still added on a fairly regular basis. So I 
>> think it makes sense to start a good structure now rather than later.
>>> Is this all written from scratch? Did you base this on anything or 
>>> use some existing code as inspiration?
>> This is written from scratch, in the sense that it is not just a 
>> modified copy of some existing library.
>> However, I have read the documentation of other libraries before, and 
>> there are only so many reasonable ways to implement this.
>> So I haven't tried to reinvent the wheel, and you'll certainly be 
>> able to find similarities to existing libraries. But the actual 
>> source code in this patchset is 100% my own work.
>>
>> - The module.enable/readyState logic is inspired by jQuery's .ready 
>> and the HTML DOM readyState property
>> - The i18n class is based on the work I did for pakfire.js and 
>> extended to work with the module concept
>> - To learn how to create the CSS overlay window, I followed the 
>> w3schools tutorial "Overlay"
> I have done some changes on the dhcp.cgi and urlxlrator.cgi pages to 
> replace bgcolor, which was deprecated in HTML 4.x and no longer 
> supported at all in HTML 5, with its CSS equivalent approach. I used 
> your CSS approach on the zone config cgi page as the basis for what I 
> changed.
>
> My plan was to go through the other cgi pages doing the same type of 
> changes.
>
> Should I hold on that until you have made this change to the CSS 
> approach with the CSS Overlay?

I mostly add new things here, so I don't think this could be a problem. 
You can go ahead!

By the way, if you want to make things a bit easier for yourself: Check 
out the nth-child CSS selector: "tr:nth-child(2n+3)"
With this, you can do the color assignment for odd/even rows entirely in 
CSS and remove all the uncessary "if ($id % 2) color1..., else 
color2..." Perl code.
Then you only need to print a plain HTML table, everything else is done 
by the browser.

Best,
Leo

>
> Regards,
> Adolf.
>> - The async/await interface for the dialog box is inspired by 
>> jQuery's deferred.then() interface for Ajax calls, but implemented 
>> with native JS
>>> Are they any existing libraries that could be useful for example 
>>> pulling in the translation?
>> I looked at a few popular libraries, but in my opinion they are too 
>> extensive and do not fit well with the existing web interface. 
>> (jquery.i18n, Polyglot, ...)
>> For example, some libraries I found prefer to load the texts from a 
>> JSON file. This would result in additional language files parallel to 
>> the existing system.
>> Besides, dynamically changing translations based on user agent 
>> settings may break the form field mapping: if ($cgiparams{'ACTION'} 
>> eq $Lang::tr{'save'} )...
>> If we didn't have to pay attention to many specifics of the old CGIs, 
>> I would also prefer an existing library.
>>
>> Best,
>> Leo
>>> -Michael
>>>
>>>> Still on my to-do/wish list:
>>>> - run make.sh lang
>>>> - convert pakfire.js to a module
>>>> - convert/rewrite refreshInetInfo.js, maybe add load displays to 
>>>> all interfaces in index.cgi
>>>>
>>>> I hope this is in a presentable state to see where it is headed.
>>>> Let me know what you think and as always thanks for 
>>>> reading/testing/reviewing :)
>>>>
>>>> Best,
>>>> Leo
>>>>
>>>>
>>>> config/etc/mime.types                        |   1 +
>>>> html/html/include/wui.js                     |  47 ++++++
>>>> html/html/include/wui_core.mjs               | 154 +++++++++++++++++++
>>>> html/html/include/wui_rrdimage.mjs           |  30 ++++
>>>> html/html/themes/ipfire/include/functions.pl |   3 +-
>>>> 5 files changed, 234 insertions(+), 1 deletion(-)
>>>> create mode 100644 html/html/include/wui.js
>>>> create mode 100644 html/html/include/wui_core.mjs
>>>> create mode 100644 html/html/include/wui_rrdimage.mjs
>>>>
>>>> diff --git a/config/etc/mime.types b/config/etc/mime.types
>>>> index af82d4886..bb3816db9 100644
>>>> --- a/config/etc/mime.types
>>>> +++ b/config/etc/mime.types
>>>> @@ -252,6 +252,7 @@ multipart/voice-message
>>>> text/css css
>>>> text/directory
>>>> text/enriched
>>>> +text/javascript mjs
>>>> text/plain asc txt
>>>> text/prs.lines.tag
>>>> text/rfc822-headers
>>>> diff --git a/html/html/include/wui.js b/html/html/include/wui.js
>>>> new file mode 100644
>>>> index 000000000..e65924e29
>>>> --- /dev/null
>>>> +++ b/html/html/include/wui.js
>>>> @@ -0,0 +1,47 @@
>>>> +/*############################################################################# 
>>>>
>>>> +# #
>>>> +# IPFire.org - A linux based 
>>>> firewall                                         #
>>>> +# Copyright (C) 2007-2023  IPFire 
>>>> Team<info@ipfire.org>                      #
>>>> +# #
>>>> +# This program is free software: you can redistribute it and/or 
>>>> modify        #
>>>> +# it under the terms of the GNU General Public License as 
>>>> published by        #
>>>> +# the Free Software Foundation, either version 3 of the License, 
>>>> or           #
>>>> +# (at your option) any later 
>>>> version.                                         #
>>>> +# #
>>>> +# This program is distributed in the hope that it will be 
>>>> useful,             #
>>>> +# but WITHOUT ANY WARRANTY; without even the implied warranty 
>>>> of              #
>>>> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See 
>>>> the               #
>>>> +# GNU General Public License for more 
>>>> details.                                #
>>>> +# #
>>>> +# You should have received a copy of the GNU General Public 
>>>> License           #
>>>> +# along with this program.  If not, 
>>>> see<http://www.gnu.org/licenses/>.       #
>>>> +# #
>>>> +#############################################################################*/ 
>>>>
>>>> +
>>>> +// IPFire Web User Interface
>>>> +// Collection of JavaScript functions and modules (requires jQuery)
>>>> +
>>>> +import {WUIcore_i18n as WUI_i18n} from "./wui_core.mjs";
>>>> +
>>>> +import {WUImodule_rrdimage as WUI_rrdimage} from 
>>>> "./wui_rrdimage.mjs";
>>>> +
>>>> +//--- WUI main class ---
>>>> +class WUImain {
>>>> + constructor() {
>>>> + //- Public properties -
>>>> + // Translation strings
>>>> + this.i18n = new WUI_i18n();
>>>> +
>>>> + //- Modules -
>>>> + // RRDtool graph images
>>>> + this.rrdimage = new WUI_rrdimage(this.i18n);
>>>> +
>>>> + //- Defaults -
>>>> + // These modules are available on every page:
>>>> + this.rrdimage.enabled = true;
>>>> + }
>>>> +}
>>>> +
>>>> +//### Initialize WUI ###
>>>> +const wui = new WUImain();
>>>> +export default wui;
>>>> diff --git a/html/html/include/wui_core.mjs 
>>>> b/html/html/include/wui_core.mjs
>>>> new file mode 100644
>>>> index 000000000..b7b729396
>>>> --- /dev/null
>>>> +++ b/html/html/include/wui_core.mjs
>>>> @@ -0,0 +1,154 @@
>>>> +/*############################################################################# 
>>>>
>>>> +# #
>>>> +# IPFire.org - A linux based 
>>>> firewall                                         #
>>>> +# Copyright (C) 2007-2023  IPFire 
>>>> Team<info@ipfire.org>                      #
>>>> +# #
>>>> +# This program is free software: you can redistribute it and/or 
>>>> modify        #
>>>> +# it under the terms of the GNU General Public License as 
>>>> published by        #
>>>> +# the Free Software Foundation, either version 3 of the License, 
>>>> or           #
>>>> +# (at your option) any later 
>>>> version.                                         #
>>>> +# #
>>>> +# This program is distributed in the hope that it will be 
>>>> useful,             #
>>>> +# but WITHOUT ANY WARRANTY; without even the implied warranty 
>>>> of              #
>>>> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See 
>>>> the               #
>>>> +# GNU General Public License for more 
>>>> details.                                #
>>>> +# #
>>>> +# You should have received a copy of the GNU General Public 
>>>> License           #
>>>> +# along with this program.  If not, see<http://www. 
>>>> gnu.org/licenses/>.       #
>>>> +# #
>>>> +#############################################################################*/ 
>>>>
>>>> +
>>>> +// IPFire Web User Interface - JavaScript module
>>>> +// Core functions & module helpers
>>>> +
>>>> +//--- Module template ---
>>>> +// Make sure that overridden functions are still executed with 
>>>> super()!
>>>> +export class WUIcore_moduleBase {
>>>> + //- Private properties -
>>>> + #enabled; // Activation state, disabled by default
>>>> + #readyState; // Loading state similar to Document.readyState
>>>> + #namespace; // Namespace derived from the class name (without 
>>>> "WUImod_" prefix)
>>>> +
>>>> + //- Class constructor -
>>>> + constructor(translations) {
>>>> + this.i18n = translations;
>>>> +
>>>> + this.#enabled = false;
>>>> +
>>>> + this.#readyState = "loading";
>>>> + this.#namespace = this.constructor.name.slice(10);
>>>> + }
>>>> +
>>>> + // Module activation state
>>>> + // Note: Because a module likely changes the DOM, it is not 
>>>> intended that it can
>>>> + // be cleanly disabled again. Disabling it anyway will trigger 
>>>> "_handleModuleDestroy".
>>>> + set enabled(state) {
>>>> + if(this.#enabled !== state) {
>>>> + this.#enabled = state;
>>>> +
>>>> + if(state) {
>>>> + this._handleModuleEnable();
>>>> + } else {
>>>> + this._handleModuleDestroy();
>>>> + }
>>>> + }
>>>> + }
>>>> + get enabled() {
>>>> + return this.#enabled;
>>>> + }
>>>> +
>>>> + // Module loading state
>>>> + // loading: Instance created
>>>> + // interactive: Module enabled, document not ready
>>>> + // complete: DOMContentLoaded/jQuery.ready event fired
>>>> + // destroyed: Module disabled, event handlers removed
>>>> + get readyState() {
>>>> + return this.#readyState;
>>>> + }
>>>> +
>>>> + // Module namespace
>>>> + get namespace() {
>>>> + return this.#namespace;
>>>> + }
>>>> +
>>>> + //### Protected properties ###
>>>> +
>>>> + // Module activation state event handlers
>>>> + _handleModuleEnable() {
>>>> + if(this.#readyState === "loading") {
>>>> + this.#readyState = "interactive";
>>>> +
>>>> + // Attach jQuery.ready event
>>>> + $(this._handleDOMReady.bind(this));
>>>> + }
>>>> + }
>>>> + _handleModuleDestroy() {
>>>> + this.#readyState = "destroyed";
>>>> +
>>>> + // Attempt to remove all event handlers of this module
>>>> + $("body").off(`.${this.namespace}`);
>>>> + }
>>>> +
>>>> + // DOMContentLoaded/jQuery.ready event handler
>>>> + // The module must be enabled for this event to be triggered once
>>>> + _handleDOMReady() {
>>>> + this.#readyState = "complete";
>>>> + }
>>>> +
>>>> + // Translations quick access in module namespace
>>>> + _i18n(key) {
>>>> + return this.i18n.get(key, this.namespace);
>>>> + }
>>>> +}
>>>> +
>>>> +//--- Simple translation strings helper ---
>>>> +export class WUIcore_i18n {
>>>> + //- Private properties -
>>>> + #strings;
>>>> +
>>>> + //- Class constructor -
>>>> + constructor() {
>>>> + this.#strings = Object.create(null); //Object without prototypes
>>>> + }
>>>> +
>>>> + // Default module prefix for general translations
>>>> + get #mainModule() {
>>>> + return "wui";
>>>> + }
>>>> +
>>>> + // Get translation
>>>> + get(key, module = this.#mainModule) {
>>>> + const index = `${module}%${key}`;
>>>> +
>>>> + if(Object.hasOwn(this.#strings, index)) {
>>>> + return this.#strings[index];
>>>> + }
>>>> + return `(missing string '${key}' in '${module}')`;
>>>> + }
>>>> +
>>>> + // Add translation
>>>> + add(key, value, module = this.#mainModule) {
>>>> + if(typeof value === "string" || typeof value === "number") {
>>>> + this.#strings[`${module}%${key}`] = value;
>>>> + }
>>>> + }
>>>> +
>>>> + // Load key/translation JS object
>>>> + // Format: {"key": "translation"}
>>>> + load(translations, module = this.#mainModule) {
>>>> + if(translations instanceof Object) {
>>>> + Object.entries(translations).forEach(([key, value]) => {
>>>> + this.add(key, value, module);
>>>> + });
>>>> + }
>>>> + }
>>>> +}
>>>> +
>>>> +//--- Static utility functions ---
>>>> +export class WUIcore_utilities {
>>>> + // Reload document (clears POST/GET data from history)
>>>> + static reloadDocument() {
>>>> + const url = window.location.origin + window.location.pathname;
>>>> + window.location.replace(url);
>>>> + }
>>>> +}
>>>> diff --git a/html/html/include/wui_rrdimage.mjs 
>>>> b/html/html/include/wui_rrdimage.mjs
>>>> new file mode 100644
>>>> index 000000000..5254b1e98
>>>> --- /dev/null
>>>> +++ b/html/html/include/wui_rrdimage.mjs
>>>> @@ -0,0 +1,30 @@
>>>> +/*############################################################################# 
>>>>
>>>> +# #
>>>> +# IPFire.org - A linux based 
>>>> firewall                                         #
>>>> +# Copyright (C) 2007-2023  IPFire 
>>>> Team<info@ipfire.org>                      #
>>>> +# #
>>>> +# This program is free software: you can redistribute it and/or 
>>>> modify        #
>>>> +# it under the terms of the GNU General Public License as 
>>>> published by        #
>>>> +# the Free Software Foundation, either version 3 of the License, 
>>>> or           #
>>>> +# (at your option) any later 
>>>> version.                                         #
>>>> +# #
>>>> +# This program is distributed in the hope that it will be 
>>>> useful,             #
>>>> +# but WITHOUT ANY WARRANTY; without even the implied warranty 
>>>> of              #
>>>> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See 
>>>> the               #
>>>> +# GNU General Public License for more 
>>>> details.                                #
>>>> +# #
>>>> +# You should have received a copy of the GNU General Public 
>>>> License           #
>>>> +# along with this program.  If not, 
>>>> see<http://www.gnu.org/licenses/>.       #
>>>> +# #
>>>> +#############################################################################*/ 
>>>>
>>>> +
>>>> +// IPFire Web User Interface - JavaScript module
>>>> +
>>>> +import {WUIcore_moduleBase as WUI_module} from "./wui_core.mjs";
>>>> +
>>>> +//--- RRDtool graph images ---
>>>> +export class WUImodule_rrdimage extends WUI_module {
>>>> + constructor(translations) {
>>>> + super(translations);
>>>> + }
>>>> +}
>>>> diff --git a/html/html/themes/ipfire/include/functions.pl 
>>>> b/html/html/themes/ipfire/include/functions.pl
>>>> index cbd05d109..445597e51 100644
>>>> --- a/html/html/themes/ipfire/include/functions.pl
>>>> +++ b/html/html/themes/ipfire/include/functions.pl
>>>> @@ -113,7 +113,8 @@ print <<END;
>>>> <title>$headline - $title</title>
>>>> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
>>>> <link rel="shortcut icon" href="/favicon.ico" />
>>>> - <script type="text/javascript" src="/include/jquery.js"></script>
>>>> + <script src="/include/jquery.js"></script>
>>>> + <script src="/include/wui.js" type="module"></script>
>>>> <script src="/include/rrdimage.js"></script>
>>>>
>>>> $extrahead
>>>> -- 
>>>> 2.37.1.windows.1
>>>>
>
  

Patch

diff --git a/config/etc/mime.types b/config/etc/mime.types
index af82d4886..bb3816db9 100644
--- a/config/etc/mime.types
+++ b/config/etc/mime.types
@@ -252,6 +252,7 @@  multipart/voice-message
 text/css			css
 text/directory
 text/enriched
+text/javascript		mjs
 text/plain			asc txt
 text/prs.lines.tag
 text/rfc822-headers
diff --git a/html/html/include/wui.js b/html/html/include/wui.js
new file mode 100644
index 000000000..e65924e29
--- /dev/null
+++ b/html/html/include/wui.js
@@ -0,0 +1,47 @@ 
+/*#############################################################################
+#                                                                             #
+# IPFire.org - A linux based firewall                                         #
+# Copyright (C) 2007-2023  IPFire Team  <info@ipfire.org>                     #
+#                                                                             #
+# This program is free software: you can redistribute it and/or modify        #
+# it under the terms of the GNU General Public License as published by        #
+# the Free Software Foundation, either version 3 of the License, or           #
+# (at your option) any later version.                                         #
+#                                                                             #
+# This program is distributed in the hope that it will be useful,             #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of              #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               #
+# GNU General Public License for more details.                                #
+#                                                                             #
+# You should have received a copy of the GNU General Public License           #
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.       #
+#                                                                             #
+#############################################################################*/
+
+// IPFire Web User Interface
+// Collection of JavaScript functions and modules (requires jQuery)
+
+import {WUIcore_i18n as WUI_i18n} from "./wui_core.mjs";
+
+import {WUImodule_rrdimage as WUI_rrdimage} from "./wui_rrdimage.mjs";
+
+//--- WUI main class ---
+class WUImain {
+	constructor() {
+		//- Public properties -
+		// Translation strings
+		this.i18n = new WUI_i18n();
+
+		//- Modules -
+		// RRDtool graph images
+		this.rrdimage = new WUI_rrdimage(this.i18n);
+
+		//- Defaults -
+		// These modules are available on every page:
+		this.rrdimage.enabled = true;
+	}
+}
+
+//### Initialize WUI ###
+const wui = new WUImain();
+export default wui;
diff --git a/html/html/include/wui_core.mjs b/html/html/include/wui_core.mjs
new file mode 100644
index 000000000..b7b729396
--- /dev/null
+++ b/html/html/include/wui_core.mjs
@@ -0,0 +1,154 @@ 
+/*#############################################################################
+#                                                                             #
+# IPFire.org - A linux based firewall                                         #
+# Copyright (C) 2007-2023  IPFire Team  <info@ipfire.org>                     #
+#                                                                             #
+# This program is free software: you can redistribute it and/or modify        #
+# it under the terms of the GNU General Public License as published by        #
+# the Free Software Foundation, either version 3 of the License, or           #
+# (at your option) any later version.                                         #
+#                                                                             #
+# This program is distributed in the hope that it will be useful,             #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of              #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               #
+# GNU General Public License for more details.                                #
+#                                                                             #
+# You should have received a copy of the GNU General Public License           #
+# along with this program.  If not, see <http://www. gnu.org/licenses/>.       #
+#                                                                             #
+#############################################################################*/
+
+// IPFire Web User Interface - JavaScript module
+// Core functions & module helpers
+
+//--- Module template ---
+// Make sure that overridden functions are still executed with super()!
+export class WUIcore_moduleBase {
+	//- Private properties -
+	#enabled;		// Activation state, disabled by default
+	#readyState; 	// Loading state similar to Document.readyState
+	#namespace;		// Namespace derived from the class name (without "WUImod_" prefix)
+
+	//- Class constructor -
+	constructor(translations) {
+		this.i18n = translations;
+
+		this.#enabled = false;
+
+		this.#readyState = "loading";
+		this.#namespace = this.constructor.name.slice(10);
+	}
+
+	// Module activation state
+	// Note: Because a module likely changes the DOM, it is not intended that it can
+	// be cleanly disabled again. Disabling it anyway will trigger "_handleModuleDestroy".
+	set enabled(state) {
+		if(this.#enabled !== state) {
+			this.#enabled = state;
+
+			if(state) {
+				this._handleModuleEnable();
+			} else {
+				this._handleModuleDestroy();
+			}
+		}
+	}
+	get enabled() {
+		return this.#enabled;
+	}
+
+	// Module loading state
+	// loading:		Instance created
+	// interactive:	Module enabled, document not ready
+	// complete:	DOMContentLoaded/jQuery.ready event fired
+	// destroyed:	Module disabled, event handlers removed
+	get readyState() {
+		return this.#readyState;
+	}
+
+	// Module namespace
+	get namespace() {
+		return this.#namespace;
+	}
+
+	//### Protected properties ###
+
+	// Module activation state event handlers
+	_handleModuleEnable() {
+		if(this.#readyState === "loading") {
+			this.#readyState = "interactive";
+
+			// Attach jQuery.ready event
+			$(this._handleDOMReady.bind(this));
+		}
+	}
+	_handleModuleDestroy() {
+		this.#readyState = "destroyed";
+
+		// Attempt to remove all event handlers of this module
+		$("body").off(`.${this.namespace}`);
+	}
+
+	// DOMContentLoaded/jQuery.ready event handler
+	// The module must be enabled for this event to be triggered once
+	_handleDOMReady() {
+		this.#readyState = "complete";
+	}
+
+	// Translations quick access in module namespace
+	_i18n(key) {
+		return this.i18n.get(key, this.namespace);
+	}
+}
+
+//--- Simple translation strings helper ---
+export class WUIcore_i18n {
+	//- Private properties -
+	#strings;
+
+	//- Class constructor -
+	constructor() {
+		this.#strings = Object.create(null); //Object without prototypes
+	}
+
+	// Default module prefix for general translations
+	get #mainModule() {
+		return "wui";
+	}
+
+	// Get translation
+	get(key, module = this.#mainModule) {
+		const index = `${module}%${key}`;
+
+		if(Object.hasOwn(this.#strings, index)) {
+			return this.#strings[index];
+		}
+		return `(missing string '${key}' in '${module}')`;
+	}
+
+	// Add translation
+	add(key, value, module = this.#mainModule) {
+		if(typeof value === "string" || typeof value === "number") {
+			this.#strings[`${module}%${key}`] = value;
+		}
+	}
+
+	// Load key/translation JS object
+	// Format: {"key": "translation"}
+	load(translations, module = this.#mainModule) {
+		if(translations instanceof Object) {
+			Object.entries(translations).forEach(([key, value]) => {
+				this.add(key, value, module);
+			});
+		}
+	}
+}
+
+//--- Static utility functions ---
+export class WUIcore_utilities {
+	// Reload document (clears POST/GET data from history)
+	static reloadDocument() {
+		const url = window.location.origin + window.location.pathname;
+		window.location.replace(url);
+	}
+}
diff --git a/html/html/include/wui_rrdimage.mjs b/html/html/include/wui_rrdimage.mjs
new file mode 100644
index 000000000..5254b1e98
--- /dev/null
+++ b/html/html/include/wui_rrdimage.mjs
@@ -0,0 +1,30 @@ 
+/*#############################################################################
+#                                                                             #
+# IPFire.org - A linux based firewall                                         #
+# Copyright (C) 2007-2023  IPFire Team  <info@ipfire.org>                     #
+#                                                                             #
+# This program is free software: you can redistribute it and/or modify        #
+# it under the terms of the GNU General Public License as published by        #
+# the Free Software Foundation, either version 3 of the License, or           #
+# (at your option) any later version.                                         #
+#                                                                             #
+# This program is distributed in the hope that it will be useful,             #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of              #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               #
+# GNU General Public License for more details.                                #
+#                                                                             #
+# You should have received a copy of the GNU General Public License           #
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.       #
+#                                                                             #
+#############################################################################*/
+
+// IPFire Web User Interface - JavaScript module
+
+import {WUIcore_moduleBase as WUI_module} from "./wui_core.mjs";
+
+//--- RRDtool graph images ---
+export class WUImodule_rrdimage extends WUI_module {
+	constructor(translations) {
+		super(translations);
+	}
+}
diff --git a/html/html/themes/ipfire/include/functions.pl b/html/html/themes/ipfire/include/functions.pl
index cbd05d109..445597e51 100644
--- a/html/html/themes/ipfire/include/functions.pl
+++ b/html/html/themes/ipfire/include/functions.pl
@@ -113,7 +113,8 @@  print <<END;
 	<title>$headline - $title</title>
 	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
 	<link rel="shortcut icon" href="/favicon.ico" />
-	<script type="text/javascript" src="/include/jquery.js"></script>
+	<script src="/include/jquery.js"></script>
+	<script src="/include/wui.js" type="module"></script>
 	<script src="/include/rrdimage.js"></script>
 
 	$extrahead