Your IP : 216.73.216.219


Current Path : /home/pteabiscqq/www/media/com_akeeba/js/
Upload File :
Current File : /home/pteabiscqq/www/media/com_akeeba/js/Configuration.js

/**
 * @package   akeebabackup
 * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 3, or later
 */

// Object initialisation
if (typeof akeeba === "undefined")
{
    var akeeba = {};
}

if (typeof akeeba.Configuration === "undefined")
{
    akeeba.Configuration                = {};
    akeeba.Configuration.GUI            = {};
    akeeba.Configuration.engines        = {};
    akeeba.Configuration.installers     = {};
    akeeba.Configuration.URLs           = {};
    akeeba.Configuration.FtpBrowser     = {
        params: {}
    };
    akeeba.Configuration.SftpBrowser    = {
        params: {}
    };
    akeeba.Configuration.FtpTest        = {};
    akeeba.Configuration.SftpTest       = {};
    akeeba.Configuration.FtpModal       = null;
    akeeba.Configuration.passwordFields = {};
    akeeba.Configuration.fsBrowser      = {
        params:      {
            dialogId:     "folderBrowserDialog",
            dialogBodyId: "folderBrowserDialogBody"
        },
        modalObject: null
    };
}

/**
 * Parses the JSON decoded data object defining engine and GUI parameters for the
 * configuration page
 *
 * @param  data  The nested objects of engine and GUI definitions
 */
akeeba.Configuration.parseConfigData = function (data)
{
    akeeba.Configuration.engines    = data.engines;
    akeeba.Configuration.installers = data.installers;
    akeeba.Configuration.parseGuiData(data.gui);
};

/**
 * Restores the contents of the password fields after brain-dead browsers with broken password managers try to auto-fill
 * the wrong password to the wrong field without warning you or asking you.
 */
akeeba.Configuration.restoreDefaultPasswords = function ()
{
    for (curid in akeeba.Configuration.passwordFields)
    {
        if (!akeeba.Configuration.passwordFields.hasOwnProperty(curid))
        {
            continue;
        }

        var defvalue = akeeba.Configuration.passwordFields[curid];

        myElement = document.getElementById(curid);

        if (!myElement)
        {
            continue;
        }

        // Do not remove this line. It's required when defvalue is empty. Why? BECAUSE BROWSERS ARE BRAIN DEAD!
        myElement.value = "WORKAROUND FOR NAUGHTY BROWSERS";
        // This line finally sets the fields back to its default value.
        myElement.value = defvalue;
    }
};

/**
 * Opens a filesystem folder browser
 *
 * @param  folder   The folder to start browsing from
 * @param  element  The element whose value we'll modify when this browser returns
 */
akeeba.Configuration.onBrowser = function (folder, element)
{
    // Close dialog callback (user confirmed the new folder)
    akeeba.Configuration.onBrowserCallback = function (myFolder)
    {
        element.value = myFolder;

        if ((typeof akeeba.Configuration.fsBrowser.modalObject == "object") && akeeba.Configuration.fsBrowser.modalObject.close)
        {
            akeeba.Configuration.fsBrowser.modalObject.close()
        }
    };

    // URL to load the browser
    var browserSrc = akeeba.Configuration.URLs["browser"] + encodeURIComponent(folder);

    var dialogBody = document.getElementById(akeeba.Configuration.fsBrowser.params.dialogBodyId);

    dialogBody.innerHTML = "";

    var iFrame = document.createElement("iframe");
    iFrame.setAttribute("src", browserSrc);
    iFrame.setAttribute("width", "100%");
    iFrame.setAttribute("height", 400);
    iFrame.setAttribute("frameborder", 0);
    iFrame.setAttribute("allowtransparency", "true");

    dialogBody.appendChild(iFrame);

    akeeba.Loader.add('akeeba.Modal', function () {
        akeeba.Configuration.fsBrowser.modalObject = akeeba.Modal.open({
            inherit: "#" + akeeba.Configuration.fsBrowser.params.dialogId,
            width:   "80%"
        });
    });
};

/**
 * FTP browser callback, used to set the FTP root directory in an element
 *
 * @param  path  The path returned by the browser
 */
akeeba.Configuration.FtpBrowser.callback = function (path)
{
    var charlist = ("/").replace(/([\[\]\(\)\.\?\/\*\{\}\+\$\^\:])/g, "$1");
    var re       = new RegExp("^[" + charlist + "]+", "g");
    path         = "/" + (path + "").replace(re, "");

    document.getElementById("var[" + akeeba.Configuration.FtpBrowser.params.key + "]").value = path;
};

/**
 * Initialises an FTP folder browser
 *
 * @param  key        The Akeeba Engine configuration key of the field holding the FTP directory we're outputting
 * @param  paramsKey  The Akeeba Engine configuration key prefix of the fields holding FTP connection information
 */
akeeba.Configuration.FtpBrowser.initialise = function (key, paramsKey)
{
    akeeba.Configuration.FtpBrowser.params.host      = document.getElementById("var[" + paramsKey + ".host]").value;
    akeeba.Configuration.FtpBrowser.params.port      = document.getElementById("var[" + paramsKey + ".port]").value;
    akeeba.Configuration.FtpBrowser.params.username  = document.getElementById("var[" + paramsKey + ".user]").value;
    akeeba.Configuration.FtpBrowser.params.password  = document.getElementById("var[" + paramsKey + ".pass]").value;
    akeeba.Configuration.FtpBrowser.params.passive   =
        document.getElementById("var[" + paramsKey + ".passive_mode]").checked;
    akeeba.Configuration.FtpBrowser.params.ssl       = document.getElementById("var[" + paramsKey + ".ftps]").checked;
    akeeba.Configuration.FtpBrowser.params.directory =
        document.getElementById("var[" + paramsKey + ".initial_directory]").value;

    akeeba.Configuration.FtpBrowser.params.key = key;

    akeeba.Configuration.FtpBrowser.open();
};

/**
 * Opens the FTP directory browser
 */
akeeba.Configuration.FtpBrowser.open = function ()
{
    var ftp_dialog_element = document.getElementById("ftpdialog");

    akeeba.System.addEventListener(document.getElementById("ftpdialogOkButton"), "click", function (e)
    {
        akeeba.Configuration.FtpBrowser.callback(akeeba.Configuration.FtpBrowser.params.directory);

        if ((typeof akeeba.Configuration.FtpModal == "object") && akeeba.Configuration.FtpModal.close)
        {
            akeeba.Configuration.FtpModal.close();
        }
    });

    akeeba.System.addEventListener(document.getElementById("ftpdialogCancelButton"), "click", function (e)
    {
        if ((typeof akeeba.Configuration.FtpModal == "object") && akeeba.Configuration.FtpModal.close)
        {
            akeeba.Configuration.FtpModal.close();
        }
    });

    akeeba.Configuration.FtpModal = akeeba.System.Modal.open({
        inherit: ftp_dialog_element,
        width:   "80%"
    });

    document.getElementById("ftpBrowserErrorContainer").style.display = "none";
    document.getElementById("ftpBrowserFolderList").innerHTML         = "";
    document.getElementById("ftpBrowserCrumbs").innerHTML             = "";

    // URL to load the browser
    akeeba.System.params.AjaxURL = akeeba.Configuration.URLs.ftpBrowser;

    if (empty(akeeba.Configuration.FtpBrowser.params.directory))
    {
        akeeba.Configuration.FtpBrowser.params.directory = "";
    }

    var data = {
        "host":      akeeba.Configuration.FtpBrowser.params.host,
        "username":  akeeba.Configuration.FtpBrowser.params.username,
        "password":  akeeba.Configuration.FtpBrowser.params.password,
        "passive":   (akeeba.Configuration.FtpBrowser.params.passive ? 1 : 0),
        "ssl":       (akeeba.Configuration.FtpBrowser.params.ssl ? 1 : 0),
        "directory": akeeba.Configuration.FtpBrowser.params.directory
    };

    // Do AJAX call & Render results
    akeeba.System.doAjax(
        data,
        function (data)
        {
            var elBreadCrumbs          = document.getElementById("ak_crumbs2");
            var elFTPBrowserFolderList = document.getElementById("ftpBrowserFolderList");

            elFTPBrowserFolderList.style.display = "none";
            elBreadCrumbs.style.display          = "none";

            if (data.error != false)
            {
                // An error occured
                document.getElementById("ftpBrowserError").innerHTML              = data.error;
                document.getElementById("ftpBrowserErrorContainer").style.display = "block";

                return;
            }

            // Create the interface
            document.getElementById("ftpBrowserErrorContainer").style.display = "none";

            // Display the crumbs
            if (!empty(data.breadcrumbs))
            {
                elBreadCrumbs.style.display = "block";
                elBreadCrumbs.innerHTML     = "";
                var relativePath            = "/";

                akeeba.Configuration.FtpBrowser.addCrumb(akeeba.System.Text._("COM_AKEEBA_FILEFILTERS_LABEL_UIROOT"),
                    "/",
                    elBreadCrumbs
                );

                for (i = 0; i < data.breadcrumbs.length; i++)
                {
                    var crumb = data.breadcrumbs[i];

                    relativePath += "/" + crumb;

                    akeeba.Configuration.FtpBrowser.addCrumb(crumb, relativePath, elBreadCrumbs);
                }
            }

            // Display the list of directories
            if (!empty(data.list))
            {
                elFTPBrowserFolderList.style.display = "block";

                /**
                 * If the directory in the browser is empty, let's inject it with the parent dir, otherwise if the
                 * user immediately clicks on "Use" gets a wrong path.
                 */
                if (!akeeba.Configuration.FtpBrowser.params.directory)
                {
                    akeeba.Configuration.FtpBrowser.params.directory = data.directory;
                }

                for (i = 0; i < data.list.length; i++)
                {
                    var item = data.list[i];

                    akeeba.Configuration.FtpBrowser.createLink(data.directory + "/" + item, item,
                        elFTPBrowserFolderList
                    );
                }
            }
        },
        function (message)
        {
            document.getElementById("ftpBrowserError").innerHTML              = message;
            document.getElementById("ftpBrowserErrorContainer").style.display = "block";
            document.getElementById("ftpBrowserFolderList").style.display     = "none";
            document.getElementById("ftpBrowserCrumbs").style.display         = "none";
        },
        false
    );
};

/**
 * Creates a directory link for the FTP browser UI
 *
 * @param  path       The directory to link to
 * @param  label      How to display it
 * @param  container  The containing element
 * @param  ftpObject  The object which contains the FTP browser methods
 */
akeeba.Configuration.FtpBrowser.createLink = function (path, label, container, ftpObject)
{
    if (typeof ftpObject == "undefined")
    {
        ftpObject = akeeba.Configuration.FtpBrowser;
    }

    var row  = document.createElement("tr");
    var cell = document.createElement("td");
    row.appendChild(cell);

    var myElement         = document.createElement("a");
    myElement.textContent = label;
    akeeba.System.addEventListener(myElement, "click", function ()
    {
        ftpObject.params.directory = path;
        ftpObject.open();
    });
    cell.appendChild(myElement);

    container.appendChild(row);
};

/**
 * Adds a breadcrumb to the FTP browser
 *
 * @param  crumb         How to display it
 * @param  relativePath  The relative path to the current directory
 * @param  container     The containing element
 * @param  ftpObject  The object which contains the FTP browser methods
 */
akeeba.Configuration.FtpBrowser.addCrumb = function (crumb, relativePath, container, ftpObject)
{
    if (typeof ftpObject == "undefined")
    {
        ftpObject = akeeba.Configuration.FtpBrowser;
    }

    var li = document.createElement("li");

    var myLink         = document.createElement("a");
    myLink.textContent = crumb;
    akeeba.System.addEventListener(myLink, "click", function (e)
    {
        ftpObject.params.directory = relativePath;
        ftpObject.open();

        if (e.preventDefault)
        {
            e.preventDefault();
        }
        else
        {
            e.returnValue = false;
        }
    });

    li.appendChild(myLink);
    container.appendChild(li);
};

/**
 * Initialises an SFTP folder browser
 *
 * @param  key        The Akeeba Engine configuration key of the field holding the SFTP directory we're outputting
 * @param  paramsKey  The Akeeba Engine configuration key prefix of the fields holding SFTP connection information
 */
akeeba.Configuration.SftpBrowser.initialise = function (key, paramsKey)
{
    akeeba.Configuration.SftpBrowser.params.host      = document.getElementById("var[" + paramsKey + ".host]").value;
    akeeba.Configuration.SftpBrowser.params.port      = document.getElementById("var[" + paramsKey + ".port]").value;
    akeeba.Configuration.SftpBrowser.params.username  = document.getElementById("var[" + paramsKey + ".user]").value;
    akeeba.Configuration.SftpBrowser.params.password  = document.getElementById("var[" + paramsKey + ".pass]").value;
    akeeba.Configuration.SftpBrowser.params.directory =
        document.getElementById("var[" + paramsKey + ".initial_directory]").value;
    akeeba.Configuration.SftpBrowser.params.privKey   = document.getElementById("var[" + paramsKey + ".privkey]").value;
    akeeba.Configuration.SftpBrowser.params.pubKey    = document.getElementById("var[" + paramsKey + ".pubkey]").value;

    akeeba.Configuration.SftpBrowser.params.key = key;

    akeeba.Configuration.SftpBrowser.open();
};

/**
 * Opens the SFTP directory browser
 */
akeeba.Configuration.SftpBrowser.open = function ()
{
    var ftp_dialog_element = document.getElementById("sftpdialog");

    ftp_dialog_element.style.display = "block";

    akeeba.System.addEventListener(document.getElementById("sftpdialogOkButton"), "click", function (e)
    {
        akeeba.Configuration.SftpBrowser.callback(akeeba.Configuration.SftpBrowser.params.directory);

        if ((typeof akeeba.Configuration.FtpModal == "object") && akeeba.Configuration.FtpModal.close)
        {
            akeeba.Configuration.FtpModal.close();
        }
    });

    akeeba.System.addEventListener(document.getElementById("sftpdialogCancelButton"), "click", function (e)
    {
        if ((typeof akeeba.Configuration.FtpModal == "object") && akeeba.Configuration.FtpModal.close)
        {
            akeeba.Configuration.FtpModal.close();
        }
    });

    akeeba.Configuration.FtpModal = akeeba.System.Modal.open({
        inherit: ftp_dialog_element,
        width:   "80%"
    });

    document.getElementById("sftpBrowserErrorContainer").style.display = "none";
    document.getElementById("sftpBrowserFolderList").innerHTML         = "";
    document.getElementById("sftpBrowserCrumbs").innerHTML             = "";

    // URL to load the browser
    akeeba.System.params.AjaxURL = akeeba.Configuration.URLs.sftpBrowser;

    if (empty(akeeba.Configuration.SftpBrowser.params.directory))
    {
        akeeba.Configuration.SftpBrowser.params.directory = "";
    }

    var data = {
        "host":      akeeba.Configuration.SftpBrowser.params.host,
        "port":      akeeba.Configuration.SftpBrowser.params.port,
        "username":  akeeba.Configuration.SftpBrowser.params.username,
        "password":  akeeba.Configuration.SftpBrowser.params.password,
        "directory": akeeba.Configuration.SftpBrowser.params.directory,
        "privkey":   akeeba.Configuration.SftpBrowser.params.privKey,
        "pubkey":    akeeba.Configuration.SftpBrowser.params.pubKey
    };

    // Do AJAX call & Render results
    akeeba.System.doAjax(
        data,
        function (data)
        {
            var elSFTPBrowserFolderList = document.getElementById("sftpBrowserFolderList");
            var elSFTPCrumbs            = document.getElementById("ak_scrumbs");

            elSFTPBrowserFolderList.style.display = "none";
            elSFTPCrumbs.style.display            = "none";

            if (data.error != false)
            {
                // An error occured
                document.getElementById("sftpBrowserError").innerHTML              = data.error;
                document.getElementById("sftpBrowserErrorContainer").style.display = "block";

                return;
            }

            // Create the interface
            document.getElementById("ftpBrowserErrorContainer").style.display = "none";

            // Display the crumbs
            if (!empty(data.breadcrumbs))
            {
                elSFTPCrumbs.style.display = "block";
                elSFTPCrumbs.innerHTML     = "";
                var relativePath           = "/";

                akeeba.Configuration.FtpBrowser.addCrumb(akeeba.System.Text._("COM_AKEEBA_FILEFILTERS_LABEL_UIROOT"),
                    "/",
                    elSFTPCrumbs, akeeba.Configuration.SftpBrowser
                );

                for (i = 0; i < data.breadcrumbs.length; i++)
                {
                    var crumb = data.breadcrumbs[i];

                    relativePath += "/" + crumb;

                    akeeba.Configuration.FtpBrowser.addCrumb(crumb, relativePath, elSFTPCrumbs,
                        akeeba.Configuration.SftpBrowser
                    );
                }
            }

            // Display the list of directories
            if (!empty(data.list))
            {
                elSFTPBrowserFolderList.style.display = "block";

                // If the directory in the browser is empty, let's inject it with the parent dir, otherwise if the user
                // immediately clicks on "Use" gets a wrong path
                if (!akeeba.Configuration.SftpBrowser.params.directory)
                {
                    akeeba.Configuration.SftpBrowser.params.directory = data.directory;
                }

                for (i = 0; i < data.list.length; i++)
                {
                    var item = data.list[i];

                    akeeba.Configuration.FtpBrowser.createLink(data.directory + "/" + item, item,
                        elSFTPBrowserFolderList, akeeba.Configuration.SftpBrowser
                    );
                }
            }
        },
        function (message)
        {
            document.getElementById("sftpBrowserError").innerHTML              = message;
            document.getElementById("sftpBrowserErrorContainer").style.display = "block";
            document.getElementById("sftpBrowserFolderList").style.display     = "none";
            document.getElementById("sftpBrowserCrumbs").style.display         = "none";
        },
        false
    );
};

/**
 * SFTP browser callback, used to set the FTP root directory in an element
 *
 * @param  path  The path returned by the browser
 */
akeeba.Configuration.SftpBrowser.callback = function (path)
{
    var charlist = ("/").replace(/([\[\]\(\)\.\?\/\*\{\}\+\$\^\:])/g, "$1");
    var re       = new RegExp("^[" + charlist + "]+", "g");
    path         = "/" + (path + "").replace(re, "");

    document.getElementById("var[" + akeeba.Configuration.SftpBrowser.params.key + "]").value = path;
};

akeeba.Configuration.FtpTest.testConnection = function (buttonKey, configKey, isCurl)
{
    var button = document.getElementById("var[" + buttonKey + "]");
    akeeba.Configuration.FtpTest.buttonKey = "var[" + buttonKey + "]";

    if (button === null)
    {
        button = document.getElementById(buttonKey);
        akeeba.Configuration.FtpTest.buttonKey = buttonKey;
    }

    if (button === null)
    {
        console.warn("Button " + akeeba.Configuration.FtpTest.buttonKey + " not found");
    }

    button.setAttribute("disabled", "disabled");

    var data = {};

    try
    {
        data = {
            isCurl:                  (isCurl ? 1 : 0),
            host:                    document.getElementById("var[" + configKey + ".host]").value,
            port:                    document.getElementById("var[" + configKey + ".port]").value,
            user:                    document.getElementById("var[" + configKey + ".user]").value,
            pass:                    document.getElementById("var[" + configKey + ".pass]").value,
            initdir:                 document.getElementById("var[" + configKey + ".initial_directory]").value,
            usessl:                  document.getElementById("var[" + configKey + ".ftps]").checked,
            passive:                 document.getElementById("var[" + configKey + ".passive_mode]").checked,
            passive_mode_workaround: 0
        };
    }
    catch (e)
    {
        data = {
            isCurl:                  (isCurl ? 1 : 0),
            host:                    document.getElementById(configKey + "_host").value,
            port:                    document.getElementById(configKey + "_port").value,
            user:                    document.getElementById(configKey + "_user").value,
            pass:                    document.getElementById(configKey + "_pass").value,
            initdir:                 document.getElementById(configKey + "_initial_directory").value,
            usessl:                  document.getElementById(configKey + "_ftps").checked,
            passive:                 document.getElementById(configKey + "_passive_mode").checked,
            passive_mode_workaround: 0
        };
    }

    // The passive_mode_workaround input is only defined for cURL
    if (isCurl)
    {
        try
        {
            data.passive_mode_workaround =
                document.getElementById("var[" + configKey + ".passive_mode_workaround]").checked;
        }
        catch (e)
        {
            data.passive_mode_workaround =
                document.getElementById(configKey + "_passive_mode_workaround").checked;
        }
    }

    // Construct the query
    akeeba.System.params.AjaxURL = akeeba.Configuration.URLs.testFtp;

    akeeba.System.doAjax(
        data,
        function (res)
        {
            var button = document.getElementById(akeeba.Configuration.FtpTest.buttonKey);
            button.removeAttribute("disabled");

            var elTestFTPBodyOK   = document.getElementById("testFtpDialogBodyOk");
            var elTestFTPBodyFail = document.getElementById("testFtpDialogBodyFail");
            var elTestFTPLabel    = document.getElementById("testFtpDialogLabel");

            elTestFTPBodyOK.style.display   = "none";
            elTestFTPBodyFail.style.display = "none";

            if (res === true)
            {
                elTestFTPLabel.textContent      = akeeba.System.Text._("COM_AKEEBA_CONFIG_DIRECTFTP_TEST_OK");
                elTestFTPBodyOK.textContent     = akeeba.System.Text._("COM_AKEEBA_CONFIG_DIRECTFTP_TEST_OK");
                elTestFTPBodyOK.style.display   = "block";
                elTestFTPBodyFail.style.display = "none";
            }
            else
            {
                elTestFTPLabel.textContent      = akeeba.System.Text._("COM_AKEEBA_CONFIG_DIRECTFTP_TEST_FAIL");
                elTestFTPBodyFail.textContent   = res;
                elTestFTPBodyOK.style.display   = "none";
                elTestFTPBodyFail.style.display = "block";
            }

            akeeba.Loader.add('akeeba.Modal', function () {
                akeeba.Modal.open({
                    inherit: "#testFtpDialog",
                    width:   "80%"
                });
            });

        }, null, false, 15000
    )
};

akeeba.Configuration.SftpTest.testConnection = function (buttonKey, configKey, isCurl)
{
    var button                              = document.getElementById("var[" + buttonKey + "]");
    akeeba.Configuration.SftpTest.buttonKey = "var[" + buttonKey + "]";

    button.setAttribute("disabled", "disabled");

    var data = {
        isCurl:  (isCurl ? 1 : 0),
        host:    document.getElementById("var[" + configKey + ".host]").value,
        port:    document.getElementById("var[" + configKey + ".port]").value,
        user:    document.getElementById("var[" + configKey + ".user]").value,
        pass:    document.getElementById("var[" + configKey + ".pass]").value,
        initdir: document.getElementById("var[" + configKey + ".initial_directory]").value,
        privkey: document.getElementById("var[" + configKey + ".privkey]").value,
        pubkey:  document.getElementById("var[" + configKey + ".pubkey]").value
    };

    // Construct the query
    akeeba.System.params.AjaxURL = akeeba.Configuration.URLs.testSftp;

    akeeba.System.doAjax(
        data,
        function (res)
        {
            var button = document.getElementById(akeeba.Configuration.SftpTest.buttonKey);
            button.removeAttribute("disabled");

            var elTestFTPBodyOK   = document.getElementById("testFtpDialogBodyOk");
            var elTestFTPBodyFail = document.getElementById("testFtpDialogBodyFail");
            var elTestFTPLabel    = document.getElementById("testFtpDialogLabel");

            elTestFTPBodyOK.style.display   = "none";
            elTestFTPBodyFail.style.display = "none";

            if (res === true)
            {
                elTestFTPLabel.textContent      = akeeba.System.Text._("COM_AKEEBA_CONFIG_DIRECTSFTP_TEST_OK");
                elTestFTPBodyOK.textContent     = akeeba.System.Text._("COM_AKEEBA_CONFIG_DIRECTSFTP_TEST_OK");
                elTestFTPBodyOK.style.display   = "block";
                elTestFTPBodyFail.style.display = "none";
            }
            else
            {
                elTestFTPLabel.textContent      = akeeba.System.Text._("COM_AKEEBA_CONFIG_DIRECTSFTP_TEST_FAIL");
                elTestFTPBodyFail.textContent   = res;
                elTestFTPBodyOK.style.display   = "none";
                elTestFTPBodyFail.style.display = "block";
            }

            akeeba.Loader.add('akeeba.Modal', function () {
                akeeba.Modal.open({
                    inherit: "#testFtpDialog",
                    width:   "80%"
                });
            });
        }, null, false, 15000
    )
};

akeeba.Configuration.enablePopoverFor = function (el)
{
    if ((typeof el == "object") && NodeList.prototype.isPrototypeOf(el))
    {
        for (i = 0; i < el.length; i++)
        {
            var e = el[i];

            akeeba.Configuration.enablePopoverFor(e);
        }

        return;
    }

    akeeba.Loader.add('akeeba.Tooltip', function () {
        akeeba.Tooltip.enableFor(el);
    });
};

/**
 * Parses the main configuration GUI definition, generating the on-page widgets
 *
 * @param  data      The nested objects of the GUI definition ('gui' key of JSON data)
 * @param  rootnode  The jroot DOM element in which to create the widgets
 */
akeeba.Configuration.parseGuiData = function (data, rootnode)
{
    if (rootnode == null)
    {
        // The default root node is the form itself
        rootnode = document.getElementById("akeebagui");
    }

    // Begin by slashing contents of the akeebagui DIV
    rootnode.innerHTML = "";

    // This is the workhorse, looping through groupdefs and creating HTML elements
    var group_id = 0;

    for (var headertext in data)
    {
        if (!data.hasOwnProperty(headertext))
        {
            continue;
        }

        var groupdef = data[headertext];

        // Loop for each group definition
        group_id++;

        if (empty(groupdef))
        {
            continue;
        }

        // Create a container for the group
        var container       = document.createElement("div");
        container.className = "akeeba-panel--info";

        rootnode.appendChild(container);

        // Create a group header
        var header            = document.createElement("header");
        header.id             = "auigrp_" + rootnode.id + "_" + group_id;
        header.className      = "akeeba-block-header";
        var headerInner       = document.createElement("h5");
        headerInner.innerHTML = headertext;
        header.appendChild(headerInner);

        container.appendChild(header);

        // Loop each element
        for (var config_key in groupdef)
        {
            if (!groupdef.hasOwnProperty(config_key))
            {
                continue;
            }

            var defdata = groupdef[config_key];

            // Parameter ID
            var current_id = "var[" + config_key + "]";

            // Option row DIV
            var showOn        = defdata['showon'] ?? null;
            var row_div       = document.createElement("div");
            row_div.className = "akeeba-ui-optionrow akeeba-form-group";
            row_div.id        = "akconfigrow." + config_key;

            if (showOn)
            {
                row_div.dataset.showon = JSON.stringify(showOn);
            }
            /**
             * We must append the option row to the container only if the option type is NOT 'hidden' or 'none'.
             * These two option types are non-GUI elements. We only render a hidden field for them. The hidden field
             * is rendered without a row container so that we don't create an empty row in the interface.
             */
            if ((defdata["type"] != "hidden") && (defdata["type"] != "none"))
            {
                container.appendChild(row_div);
            }

            // Render the label, if applicable
            akeeba.Configuration.GUI.renderOptionLabel(current_id, defdata, row_div);

            // Create GUI representation based on type
            var controlWrapper       = document.createElement("div");
            controlWrapper.className = "akeeba-form-controls";

            var ucfirstType  = defdata["type"][0].toUpperCase() + defdata["type"].slice(1);
            var renderMethod = "renderOptionType" + ucfirstType;

            if (typeof akeeba.Configuration.GUI[renderMethod] === "function")
            {
                akeeba.Configuration.GUI[renderMethod](current_id, defdata, controlWrapper, row_div, container);
            }
            else
            {
                akeeba.Configuration.GUI.renderOptionTypeUnknown(current_id, defdata, controlWrapper, row_div,
                    container
                );
            }
        }

        // Re-initialise the ShowOn JavaScript
        if (Joomla.Showon) {
            Joomla.Showon.initialise(rootnode);
        }
    }

    // Enable popovers
    akeeba.Configuration.enablePopoverFor(rootnode.querySelectorAll("[rel=\"akeeba-sticky-tooltip\"]"));
};

/**
 * Renders the label of a configuration option, appending it to the container element
 *
 * @param   {string}   current_id  The input name, e.g. var[something.or.another]
 * @param   {Object}   defdata     The option definition data
 * @param   {Element}  row_div    The element which contains the option itself (the DIV of the current row)
 */
akeeba.Configuration.GUI.renderOptionLabel = function (current_id, defdata, row_div)
{
    const noLabel = (defdata['nolabel'] ?? 0) != 0;

    // No interface is rendered for 'hidden' and 'none' option types
    if ((noLabel || defdata["type"] == "hidden") || (defdata["type"] == "none"))
    {
        return;
    }

    // Create label
    var label       = document.createElement("label");
    label.className = "akeeba-control-label";
    label.setAttribute("for", current_id);
    label.innerHTML = defdata["title"];

    if (defdata["description"])
    {
        label.setAttribute("rel", "akeeba-sticky-tooltip");
        label.setAttribute("data-original-title", defdata["title"]);
        label.setAttribute("data-content", defdata["description"]);
    }

    if (defdata["bold"])
    {
        label.style.fontWeight = "bold";
    }

    row_div.appendChild(label);
};

/**
 * Renders an option of type "none". A do-not-display field. It doesn't render any input element at all.
 *
 * @param   {string}  current_id       The input name, e.g. var[something.or.another]
 * @param   {Object}  defdata          The option definition data
 * @param   {Element} controlWrapper  The element which contains the option's input object
 * @param   {Element} row_div         The element which contains the option itself (the DIV of the current row)
 * @param   {Element} container       The element which contains the row_div (option group container)
 */
akeeba.Configuration.GUI.renderOptionTypeNone = function (current_id, defdata, controlWrapper, row_div, container)
{
    // Nothing to render
};

/**
 * Renders an option of type "hidden". A hidden field.
 *
 * @param   {string}  current_id      The input name, e.g. var[something.or.another]
 * @param   {Object}  defdata         The option definition data
 * @param   {Element} controlWrapper  The element which contains the option's input object
 * @param   {Element} row_div         The element which contains the option itself (the DIV of the current row)
 * @param   {Element} container       The element which contains the row_div (option group container)
 */
akeeba.Configuration.GUI.renderOptionTypeHidden = function (current_id, defdata, controlWrapper, row_div, container)
{
    var hiddenfield = document.createElement("input");
    hiddenfield.id  = current_id;
    hiddenfield.setAttribute("type", "hidden");
    hiddenfield.setAttribute("name", current_id);
    hiddenfield.setAttribute("size", "40");
    hiddenfield.value = defdata["default"];

    container.appendChild(hiddenfield);
};

/**
 * Renders an option of type "separator". A GUI row separator.
 *
 * @param   {string}  current_id      The input name, e.g. var[something.or.another]
 * @param   {Object}  defdata         The option definition data
 * @param   {Element} controlWrapper  The element which contains the option's input object
 * @param   {Element} row_div         The element which contains the option itself (the DIV of the current row)
 * @param   {Element} container       The element which contains the row_div (option group container)
 */
akeeba.Configuration.GUI.renderOptionTypeSeparator =
    function (current_id, defdata, controlWrapper, row_div, container)
    {
        const element   = defdata["element"] ?? "div";
        const content   = defdata["content"] ?? null;
        const separator = document.createElement(element);
        separator.className = "akeeba_ui_separator";

        if (content)
        {
            separator.innerHTML = content;
        }

        container.appendChild(separator);
    };

/**
 * Renders an option of type "checkandhide". Checks if the field data is empty and renders the data in a hidden
 * field.
 *
 * TODO Do we still use this? I cannot find any reference to it.
 *
 * @param   {string}  current_id      The input name, e.g. var[something.or.another]
 * @param   {Object}  defdata         The option definition data
 * @param   {Element} controlWrapper  The element which contains the option's input object
 * @param   {Element} row_div         The element which contains the option itself (the DIV of the current row)
 * @param   {Element} container       The element which contains the row_div (option group container)
 */
akeeba.Configuration.GUI.renderOptionTypeCheckandhide =
    function (current_id, defdata, controlWrapper, row_div, container)
    {
        // Container for selection & button
        var span = document.createElement("span");
        controlWrapper.appendChild(span);
        row_div.appendChild(controlWrapper);

        var hiddenfield = document.createElement("input");


        hiddenfield.setAttribute("type", "hidden");
        hiddenfield.id = current_id;
        hiddenfield.setAttribute("name", current_id);
        hiddenfield.setAttribute("size", "40");
        hiddenfield.value = defdata["default"];
        span.appendChild(hiddenfield);

        var myLabel = defdata["labelempty"];

        if (defdata["default"] != "")
        {
            myLabel = defdata["labelnotempty"];
        }

        var span2 = document.createElement("span");

        span2.textContent = myLabel;
        span.appendChild(span2);
        akeeba.System.data.set(span2, "labelempty", defdata["labelempty"]);
        akeeba.System.data.set(span2, "labelnotempty", defdata["labelnotempty"]);
    };

/**
 * Renders an option of type "installer". An installer selection.
 *
 * @param   {string}  current_id      The input name, e.g. var[something.or.another]
 * @param   {Object}  defdata         The option definition data
 * @param   {Element} controlWrapper  The element which contains the option's input object
 * @param   {Element} row_div         The element which contains the option itself (the DIV of the current row)
 * @param   {Element} container       The element which contains the row_div (option group container)
 */
akeeba.Configuration.GUI.renderOptionTypeInstaller =
    function (current_id, defdata, controlWrapper, row_div, container)
    {
        // Create the select element
        var editor       = document.createElement("select");
        editor.className = "akeeba-configuration-select-installer";
        editor.id        = current_id;
        editor.setAttribute("name", current_id);

        for (key in akeeba.Configuration.installers)
        {
            if (!akeeba.Configuration.installers.hasOwnProperty(key))
            {
                continue;
            }

            var element = akeeba.Configuration.installers[key];

            var option       = document.createElement("option");
            option.value     = key;
            option.innerHTML = element.name;

            if (defdata["default"] == key)
            {
                option.setAttribute("selected", 1);
            }

            editor.appendChild(option);
        }

        controlWrapper.appendChild(editor);
        row_div.appendChild(controlWrapper);
    };

/**
 * Renders an option of type "engine". An engine selection.
 *
 * @param   {string}  current_id      The input name, e.g. var[something.or.another]
 * @param   {Object}  defdata         The option definition data
 * @param   {Element} controlWrapper  The element which contains the option's input object
 * @param   {Element} row_div         The element which contains the option itself (the DIV of the current row)
 * @param   {Element} container       The element which contains the row_div (option group container)
 */
akeeba.Configuration.GUI.renderOptionTypeEngine = function (current_id, defdata, controlWrapper, row_div, container)
{
    var engine_type = defdata["subtype"];

    if (akeeba.Configuration.engines[engine_type] == null)
    {
        return;
    }

    var config_key = current_id.substr(4, current_id.length - 5);

    // Container for engine parameters, initially hidden
    var engine_config_container       = document.createElement("div");
    engine_config_container.id        = config_key + "_config";
    engine_config_container.className = "akeeba-hidden-mobile akeeba-hidden-desktop akeeba-engine-options";

    // Create the select element
    var editor = document.createElement("select");
    editor.id  = current_id;
    editor.setAttribute("name", current_id);

    var engineOptions = akeeba.Configuration.engines[engine_type];

    for (key in engineOptions)
    {
        if (!engineOptions.hasOwnProperty(key))
        {
            continue;
        }

        var element = engineOptions[key];

        var option       = document.createElement("option");
        option.value     = key;
        option.innerHTML = element.information.title;

        if (defdata["default"] == key)
        {
            option.setAttribute("selected", "selected");
        }

        editor.appendChild(option);
    }

    akeeba.System.addEventListener(editor, "change", function (e)
    {
        // When the selection changes, we have to repopulate the config container
        // First, save any changed values
        var old_values = {};

        var allElements = [
            document.getElementById(config_key + "_config").querySelectorAll("input"),
            document.getElementById(config_key + "_config").querySelectorAll("select")
        ];

        var allInputs = null;
        var input     = null;
        var id        = null;

        for (i = 0; i < allElements.length; i++)
        {
            allInputs = allElements[i];

            if (!allInputs.length)
            {
                continue;
            }

            for (j = 0; j < allInputs.length; j++)
            {
                input = allInputs[j];
                id    = input.id;

                old_values[id] = input.value;

                if (input.getAttribute("type") == "checkbox")
                {
                    old_values[id] = input.checked;
                }
                else if (input.getAttribute("type") == "select")
                {
                    old_values[id] = input.options[input.selectedIndex].value;
                }
            }

        }

        // Create the new interface
        var new_engine        = editor.value;
        var enginedef         = akeeba.Configuration.engines[engine_type][new_engine];
        var enginetitle       = enginedef.information.title;
        var new_data          = {};
        new_data[enginetitle] = enginedef.parameters;

        akeeba.Configuration.parseGuiData(new_data, engine_config_container);

        var elLegend = engine_config_container.querySelector("header");
        if (elLegend instanceof Element)
        {
            elLegend.insertAdjacentHTML(
                "afterend",
                "<p class=\"akeeba-block--information\">" + enginedef.information.description + "</p>"
            );
        }

        // Reapply changed values
        allElements = [
            document.getElementById(config_key + "_config").querySelectorAll("input"),
            document.getElementById(config_key + "_config").querySelectorAll("select")
        ];

        for (i = 0; i < allElements.length; i++)
        {
            allInputs = allElements[i];

            if (!allInputs.length)
            {
                continue;
            }

            for (j = 0; j < allInputs.length; j++)
            {
                input = allInputs[j];
                id    = input.id;

                var old = old_values[id];

                if ((old == null) || (old == undefined))
                {
                    continue;
                }

                if (input.getAttribute("type") == "checkbox")
                {
                    if (old)
                    {
                        input.setAttribute("checked", "checked");
                    }
                    else
                    {
                        input.removeAttribute("checked");
                    }
                }
                else
                {
                    input.value = old;
                }

                // Trigger the change event for drop-downs
                if (i == 1)
                {
                    akeeba.System.triggerEvent(input, "change");
                }
            }
        }

        // Finally, run the activation_callback
        if (typeof enginedef.information.activation_callback !== "undefined")
        {
            window[enginedef.information.activation_callback](enginedef.parameters);
        }
    });

    // Add a configuration show/hide button
    var button       = document.createElement("button");
    button.className = "akeeba-btn--small--dark";

    var icon       = document.createElement("span");
    icon.className = "akion-wrench";
    button.appendChild(icon);

    var btnText       = document.createElement("span");
    btnText.innerHTML = akeeba.System.Text._("COM_AKEEBA_CONFIG_UI_CONFIG");
    button.appendChild(btnText);

    akeeba.System.addEventListener(button, "click", function (e)
    {
        akeeba.System.toggleClass(engine_config_container, "akeeba-hidden-mobile");
        akeeba.System.toggleClass(engine_config_container, "akeeba-hidden-desktop");

        if (e.preventDefault)
        {
            e.preventDefault();
        }
        else
        {
            e.returnValue = false;
        }
    });

    var spacerSpan       = document.createElement("span");
    spacerSpan.innerHTML = "&nbsp;";

    controlWrapper.appendChild(editor);
    controlWrapper.appendChild(spacerSpan);
    controlWrapper.appendChild(button);
    controlWrapper.appendChild(engine_config_container);

    row_div.appendChild(controlWrapper);

    // Populate config container with the default engine data
    if (akeeba.Configuration.engines[engine_type][defdata["default"]] != null)
    {
        var new_engine        = defdata["default"];
        var enginedef         = akeeba.Configuration.engines[engine_type][new_engine];
        var enginetitle       = enginedef.information.title;
        var new_data          = {};
        new_data[enginetitle] = enginedef.parameters;

        // Is it a protected field?
        if (defdata["protected"] != 0)
        {
            var titleSpan         = document.createElement("span");
            titleSpan.textContent = enginetitle;
            //span.appendChild(titleSpan);
            editor.style.display  = "none";
        }

        akeeba.Configuration.parseGuiData(new_data, engine_config_container);

        var elLegend = engine_config_container.querySelector("header");
        if (elLegend instanceof Element)
        {
            elLegend.insertAdjacentHTML(
                "afterend",
                "<p class=\"akeeba-block--information\">" + enginedef.information.description + "</p>"
            );
        }

        // Finally, run the activation_callback
        if (typeof enginedef.information.activation_callback !== "undefined")
        {
            window[enginedef.information.activation_callback](enginedef.parameters);
        }
    }
};

/**
 * Renders an option of type "browsedir". A text box with an option to launch a browser.
 *
 * @param   {string}  current_id      The input name, e.g. var[something.or.another]
 * @param   {Object}  defdata         The option definition data
 * @param   {Element} controlWrapper  The element which contains the option's input object
 * @param   {Element} row_div         The element which contains the option itself (the DIV of the current row)
 * @param   {Element} container       The element which contains the row_div (option group container)
 */
akeeba.Configuration.GUI.renderOptionTypeBrowsedir =
    function (current_id, defdata, controlWrapper, row_div, container)
    {
        var editor = document.createElement("input");
        editor.setAttribute("type", "text");
        editor.setAttribute("name", current_id);
        editor.setAttribute("size", "30");
        editor.id    = current_id;
        editor.value = defdata["default"];

        var button       = document.createElement("button");
        button.className = "akeeba-btn--grey";
        button.setAttribute("title", akeeba.System.Text._("COM_AKEEBA_CONFIG_UI_BROWSE"));

        var icon       = document.createElement("span");
        icon.className = "akion-folder";
        button.appendChild(icon);

        akeeba.System.addEventListener(button, "click", function (e)
        {
            if (e.preventDefault)
            {
                e.preventDefault();
            }
            else
            {
                e.returnValue = false;
            }

            if (akeeba.Configuration.onBrowser != null)
            {
                akeeba.Configuration.onBrowser(editor.value, editor);
            }

            return false;
        });

        var containerDiv       = document.createElement("div");
        containerDiv.className = "akeeba-input-group";

        var buttonContainer       = document.createElement("span");
        buttonContainer.className = "akeeba-input-group-btn";

        containerDiv.appendChild(editor);
        buttonContainer.appendChild(button);
        containerDiv.appendChild(buttonContainer);
        controlWrapper.appendChild(containerDiv);
        row_div.appendChild(controlWrapper);
    };

/**
 * Renders an option of type "buttonedit". A text box with a button.
 *
 * @param   {string}  current_id      The input name, e.g. var[something.or.another]
 * @param   {Object}  defdata         The option definition data
 * @param   {Element} controlWrapper  The element which contains the option's input object
 * @param   {Element} row_div         The element which contains the option itself (the DIV of the current row)
 * @param   {Element} container       The element which contains the row_div (option group container)
 */
akeeba.Configuration.GUI.renderOptionTypeButtonedit =
    function (current_id, defdata, controlWrapper, row_div, container)
    {
        var editortype = defdata["editortype"] == "hidden" ? "hidden" : "text";

        var editor       = document.createElement("input");
        editor.className = "akeeba-configuration-buttonedit";
        editor.setAttribute("type", editortype);
        editor.setAttribute("name", current_id);
        editor.setAttribute("size", 30);
        editor.value = defdata["default"];

        if (defdata["editordisabled"] == "1")
        {
            editor.setAttribute("disabled", "disabled");
        }

        var button       = document.createElement("button");
        button.innerHTML = akeeba.System.Text._(defdata["buttontitle"]);
        button.className = "akeeba-btn--grey";
        akeeba.System.addEventListener(button, "click", function (event)
        {
            if (event.preventDefault)
            {
                event.preventDefault();
            }
            else
            {
                event.returnValue = false;
            }

            var hook = defdata["hook"];

            try
            {
                window[hook]();
            }
            catch (err)
            {
            }
        });

        var span       = document.createElement("span");
        span.className = "input-append";

        span.appendChild(editor);
        span.appendChild(button);
        controlWrapper.appendChild(span);
        row_div.appendChild(controlWrapper);
    };

/**
 * Renders an option of type "enum". A drop-down list.
 *
 * @param   {string}  current_id      The input name, e.g. var[something.or.another]
 * @param   {Object}  defdata         The option definition data
 * @param   {Element} controlWrapper  The element which contains the option's input object
 * @param   {Element} row_div         The element which contains the option itself (the DIV of the current row)
 * @param   {Element} container       The element which contains the row_div (option group container)
 */
akeeba.Configuration.GUI.renderOptionTypeEnum = function (current_id, defdata, controlWrapper, row_div, container)
{
    var editor       = document.createElement("select");
    editor.className = "akeeba-configuration-select-enum";
    editor.id        = current_id;
    editor.setAttribute("name", current_id);

    // Create and append options
    var enumvalues = defdata["enumvalues"].split("|");
    var enumkeys   = defdata["enumkeys"].split("|");

    for (counter = 0; counter < enumvalues.length; counter++)
    {
        var value = enumvalues[counter];

        var item_description = enumkeys[counter];
        var option           = document.createElement("option");
        option.value         = value;
        option.innerHTML     = item_description;

        if (value == defdata["default"])
        {
            option.setAttribute("selected", "selected");
        }

        editor.appendChild(option);
    }

    if (typeof defdata["onchange"] !== "undefined")
    {
        akeeba.System.addEventListener(editor, "change", function ()
        {
            var callback_onchange = defdata["onchange"];
            callback_onchange(editor);
        });
    }

    controlWrapper.appendChild(editor);
    row_div.appendChild(controlWrapper);
};

/**
 * Renders an option of type "string". A simple single-line, unvalidated text box.
 *
 * @param   {string}  current_id      The input name, e.g. var[something.or.another]
 * @param   {Object}  defdata         The option definition data
 * @param   {Element} controlWrapper  The element which contains the option's input object
 * @param   {Element} row_div         The element which contains the option itself (the DIV of the current row)
 * @param   {Element} container       The element which contains the row_div (option group container)
 */
akeeba.Configuration.GUI.renderOptionTypeString = function (current_id, defdata, controlWrapper, row_div, container)
{
    var editor       = document.createElement("input");
    editor.className = "akeeba-configuration-string";
    editor.setAttribute("type", "text");
    editor.id = current_id;
    editor.setAttribute("name", current_id);
    editor.setAttribute("size", 40);
    editor.value = defdata["default"];

    controlWrapper.appendChild(editor);
    row_div.appendChild(controlWrapper);
};

/**
 * Renders an option of type "password". A simple single-line, unvalidated password box.
 *
 * @param   {string}  current_id      The input name, e.g. var[something.or.another]
 * @param   {Object}  defdata         The option definition data
 * @param   {Element} controlWrapper  The element which contains the option's input object
 * @param   {Element} row_div         The element which contains the option itself (the DIV of the current row)
 * @param   {Element} container       The element which contains the row_div (option group container)
 */
akeeba.Configuration.GUI.renderOptionTypePassword = function (current_id, defdata, controlWrapper, row_div, container)
{
    akeeba.Configuration.passwordFields[current_id] = defdata["default"];

    var editor       = document.createElement("input");
    editor.className = "akeeba-configuration-password";
    editor.setAttribute("type", "password");
    editor.id = current_id;
    editor.setAttribute("name", current_id);
    editor.setAttribute("size", 40);
    editor.value = defdata["default"];
    editor.setAttribute("autocomplete", "off");

    controlWrapper.appendChild(editor);
    row_div.appendChild(controlWrapper);
};

/**
 * Renders an option of type "integer". Hidden form element with the real value.
 *
 * @param   {string}  current_id      The input name, e.g. var[something.or.another]
 * @param   {Object}  defdata         The option definition data
 * @param   {Element} controlWrapper  The element which contains the option's input object
 * @param   {Element} row_div         The element which contains the option itself (the DIV of the current row)
 * @param   {Element} container       The element which contains the row_div (option group container)
 */
akeeba.Configuration.GUI.renderOptionTypeInteger = function (current_id, defdata, controlWrapper, row_div, container)
{
    var config_key = current_id.substr(4, current_id.length - 5);

    // Hidden input field. Holds the actual value saved to the configuration.
    var elHiddenInput = document.createElement("input");
    elHiddenInput.id  = config_key;
    elHiddenInput.setAttribute("name", current_id);
    elHiddenInput.setAttribute("type", "hidden");
    elHiddenInput.value = defdata["default"];

    // Custom value input box
    var elCustomValue = document.createElement("input");

    elCustomValue.setAttribute("type", "text");
    elCustomValue.setAttribute("size", "10");
    elCustomValue.id            = config_key + "_custom";
    elCustomValue.style.display = "none";
    //elCustomValue.style.marginLeft = '6px';
    elCustomValue.className     = "akeeba-form-input-mini";

    akeeba.System.addEventListener(elCustomValue, "blur", function ()
    {
        var value = parseFloat(elCustomValue.value);
        value     = value * defdata["scale"];

        if (value < defdata["min"])
        {
            value = defdata["min"];
        }
        else if (value > defdata["max"])
        {
            value = defdata["max"];
        }

        elHiddenInput.value = value;

        var newValue = value / defdata["scale"];

        elCustomValue.value = newValue.toFixed(2);
    });

    // Select element with preset options
    var elDropdown = document.createElement("select");
    elDropdown.id  = config_key + "_dropdown";
    elDropdown.setAttribute("name", config_key + "_dropdown");
    elDropdown.className = "akeeba-form-input-small";

    // Create and append the preset options to the select element
    var enumvalues     = defdata["shortcuts"].split("|");
    var quantizer      = defdata["scale"];
    var isPresetOption = false;

    for (counter = 0; counter < enumvalues.length; counter++)
    {
        var value = enumvalues[counter];

        var item_description       = value / quantizer;
        var elDropdownOption       = document.createElement("option");
        elDropdownOption.value     = value;
        elDropdownOption.innerHTML = item_description.toFixed(2);

        if (value == defdata["default"])
        {
            elDropdownOption.setAttribute("selected", "selected");
            isPresetOption = true;
        }

        elDropdown.appendChild(elDropdownOption);
    }

    // Create one last option called "Custom"
    var option       = document.createElement("option");
    option.value     = -1;
    // TODO Translate this text
    option.innerHTML = "Custom...";

    if (!isPresetOption)
    {
        option.setAttribute("selected", "selected");
        elCustomValue.value         = (defdata["default"] / defdata["scale"]).toFixed(2);
        elCustomValue.style.display = "inline-block";
    }

    elDropdown.appendChild(option);

    // Add actions to the dropdown
    akeeba.System.addEventListener(elDropdown, "change", function ()
    {
        var value = elDropdown.value;

        if (value == -1)
        {
            elCustomValue.value         = (defdata["default"] / defdata["scale"]).toFixed(2);
            elCustomValue.style.display = "inline-block";
            akeeba.System.triggerEvent(elCustomValue, "focus");

            return;
        }

        elHiddenInput.value         = value;
        elCustomValue.style.display = "none";
    });

    // Label
    var uom = defdata["uom"];

    if ((typeof (uom) != "string") || empty(uom))
    {
        uom = "";

        controlWrapper.appendChild(elDropdown);
        controlWrapper.appendChild(elCustomValue);
    }
    else
    {
        var inputAppendWrapper       = document.createElement("div");
        inputAppendWrapper.className = "akeeba-input-group--small";

        var label         = document.createElement("span");
        //akeeba.System.addClass(label, 'add-on');
        label.textContent = " " + uom;

        inputAppendWrapper.appendChild(elDropdown);
        inputAppendWrapper.appendChild(elCustomValue);
        inputAppendWrapper.appendChild(label);
        controlWrapper.appendChild(inputAppendWrapper);
    }

    controlWrapper.appendChild(elHiddenInput);
    row_div.appendChild(controlWrapper);
};

/**
 * Renders an option of type "bool". A toggle button.
 *
 * @param   {string}  current_id      The input name, e.g. var[something.or.another]
 * @param   {Object}  defdata         The option definition data
 * @param   {Element} controlWrapper  The element which contains the option's input object
 * @param   {Element} row_div         The element which contains the option itself (the DIV of the current row)
 * @param   {Element} container       The element which contains the row_div (option group container)
 */
akeeba.Configuration.GUI.renderOptionTypeBool = function (current_id, defdata, controlWrapper, row_div, container)
{
    var wrap_div       = document.createElement("div");
    wrap_div.className = "akeeba-ui-checkbox";

    // Necessary hack: when the checkbox is unchecked, nothing gets submitted.
    // We need the hidden input to submit a zero value.
    var elInput = document.createElement("input");
    elInput.setAttribute("name", current_id);
    elInput.setAttribute("type", "hidden");
    elInput.value = 0;

    wrap_div.appendChild(elInput);

    // Create a checkbox
    var editor = document.createElement("input");
    editor.id  = current_id;
    editor.setAttribute("name", current_id);
    editor.setAttribute("type", "checkbox");
    editor.setAttribute("value", 1);

    if (defdata["default"] != 0)
    {
        editor.setAttribute("checked", "checked");
    }

    wrap_div.appendChild(editor);
    controlWrapper.appendChild(wrap_div);
    row_div.appendChild(controlWrapper);
};

/**
 * Renders an option of type "button". Button with a custom hook function.
 *
 * @param   {string}  current_id      The input name, e.g. var[something.or.another]
 * @param   {Object}  defdata         The option definition data
 * @param   {Element} controlWrapper  The element which contains the option's input object
 * @param   {Element} row_div         The element which contains the option itself (the DIV of the current row)
 * @param   {Element} container       The element which contains the row_div (option group container)
 */
akeeba.Configuration.GUI.renderOptionTypeButton = function (current_id, defdata, controlWrapper, row_div, container)
{
    // Create the button
    var label        = row_div.querySelector("label");
    var hook         = defdata["hook"];
    var labeltext    = label.innerHTML;
    var editor       = document.createElement("button");
    editor.id        = current_id;
    editor.innerHTML = labeltext;
    editor.className = "akeeba-btn--grey";
    label.innerHTML  = "&nbsp;";

    akeeba.System.addEventListener(editor, "click", function (e)
    {
        if (e.preventDefault)
        {
            e.preventDefault();
        }
        else
        {
            e.returnValue = false;
        }

        try
        {
            window[hook]();
        }
        catch (err)
        {
        }
    });

    controlWrapper.appendChild(editor);
    row_div.appendChild(controlWrapper);
};

/**
 * Renders an option of an unknown type (an extension is used).
 *
 * @param   {string}  current_id      The input name, e.g. var[something.or.another]
 * @param   {Object}  defdata         The option definition data+
 * @param   {Element} controlWrapper  The element which contains the option's input object
 * @param   {Element} row_div         The element which contains the option itself (the DIV of the current row)
 * @param   {Element} container       The element which contains the row_div (option group container)
 */
akeeba.Configuration.GUI.renderOptionTypeUnknown = function (current_id, defdata, controlWrapper, row_div, container)
{
    var config_key = current_id.substr(4, current_id.length - 5);

    var method = "akeeba_render_" + defdata["type"];
    var fn     = window[method];

    if (typeof fn == "function")
    {
        fn(config_key, defdata, label, row_div);
    }
    else
    {
        try
        {
            window[method](config_key, defdata, label, row_div);
        }
        catch (e)
        {

        }
    }
};

akeeba.Configuration.onChangeScriptType = function (selectElement)
{
    // Currently selected value
    var value             = selectElement.options[selectElement.selectedIndex].value;
    var possibleInstaller = (value === "joomla") ? "angie" : ("angie-" + value);

    // All possible installers
    var installerSelect   = document.getElementById("var[akeeba.advanced.embedded_installer]");
    var installerElements = installerSelect.children;

    for (var i = 0; i < installerElements.length; i++)
    {
        var element = installerElements[i];

        if (element.value === possibleInstaller)
        {
            installerSelect.value = possibleInstaller;

            return;
        }
    }
};

akeeba.Configuration.onFormSubmit = function (event)
{
    const adminForm = document.getElementById("adminForm");

    // Old browsers: submit the form with 100+ parameters.
    if (
        !adminForm || typeof window.FormData === "undefined"
        || typeof Object.fromEntries === "undefined" || typeof JSON.stringify === "undefined"
    )
    {
        return true;
    }

    let task = document.forms['adminForm']['task'].value;

    if (task !== 'apply' && task !== 'save' && task !== 'savenew')
    {
        return true;
    }

    // Newer browsers: submit as a single JSON document
    event.preventDefault();

    akeeba.Configuration.submitJsonForm(
        JSON.stringify(Object.fromEntries(new FormData(adminForm)))
    );
};

akeeba.Configuration.submitJsonForm = async function (json)
{
    // Set up our virtual form
    const formData = new FormData();
    formData.set('jsonForm', json);
    formData.set(Joomla.getOptions('csrf.token'), 1);
    formData.set('task', document.forms['adminForm']['task'].value);

    // Initialise the redirection detection code.
    let url = null;

    try {
        // Do an async POST to the profile config save endpoint
        const response = await fetch(
            document.forms['adminForm'].action,
            {
                method: "POST",
                body: formData,
            });

        // Redirections will be respected by using their URL as the ultimate redirection target.
        if (response.redirected)
        {
            url = response.url;
        }

        // Something went wrong. Reload the configuration page.
        if (!response.ok) {
            // noinspection ExceptionCaughtLocallyJS
            throw new Error('Error found in network response');
        }
    } catch (error) {
        location.reload();

        return;
    }

    // No redirection URL detected; reload the page.
    if (!url)
    {
        location.reload();

        return;
    }

    // We have a redirection URL; follow it.
    window.location = url;
};

// =====================================================================================================================
// Initialise hooks used by the engine definitions INI files
// =====================================================================================================================

akeeba_directftp_init_browser = function ()
{
    akeeba.Configuration.FtpBrowser.initialise(
        "engine.archiver.directftp.initial_directory",
        "engine.archiver.directftp"
    );
};

akeeba_postprocftp_init_browser = function ()
{
    akeeba.Configuration.FtpBrowser.initialise("engine.postproc.ftp.initial_directory", "engine.postproc.ftp");
};

akeeba_directsftp_init_browser = function ()
{
    akeeba.Configuration.SftpBrowser.initialise(
        "engine.archiver.directsftp.initial_directory",
        "engine.archiver.directsftp"
    );
};

akeeba_postprocsftp_init_browser = function ()
{
    akeeba.Configuration.FtpBrowser.initialise("engine.postproc.sftp.initial_directory", "engine.postproc.sftp");
};

directftp_test_connection = function ()
{
    akeeba.Configuration.FtpTest.testConnection("engine.archiver.directftp.ftp_test", "engine.archiver.directftp", 0);
};

postprocftp_test_connection = function ()
{
    akeeba.Configuration.FtpTest.testConnection("engine.postproc.ftp.ftp_test", "engine.postproc.ftp", 0);
};

directftpcurl_test_connection = function ()
{
    akeeba.Configuration.FtpTest.testConnection("engine.archiver.directftpcurl.ftp_test",
        "engine.archiver.directftpcurl", 1
    );
};

postprocftpcurl_test_connection = function ()
{
    akeeba.Configuration.FtpTest.testConnection("engine.postproc.ftpcurl.ftp_test", "engine.postproc.ftpcurl", 1);
};

directsftp_test_connection = function ()
{
    akeeba.Configuration.SftpTest.testConnection("engine.archiver.directsftp.sftp_test", "engine.archiver.directsftp",
        0
    );
};

postprocsftp_test_connection = function ()
{
    akeeba.Configuration.SftpTest.testConnection("engine.postproc.sftp.sftp_test", "engine.postproc.sftp", 0);
};

directsftpcurl_test_connection = function ()
{
    akeeba.Configuration.SftpTest.testConnection("engine.archiver.directsftpcurl.sftp_test",
        "engine.archiver.directsftpcurl", 1
    );
};

postprocsftpcurl_test_connection = function ()
{
    akeeba.Configuration.SftpTest.testConnection("engine.postproc.sftpcurl.sftp_test", "engine.postproc.sftpcurl", 1);
};

akconfig_dropbox_openoauth = function ()
{
    var url = akeeba.Configuration.URLs.dpeauthopen;

    if (url.indexOf("?") == -1)
    {
        url = url + "?";
    }
    else
    {
        url = url + "&";
    }

    window.open(url + "engine=dropbox", "akeeba_dropbox_window", "width=1010,height=500,opener");
};

akconfig_dropbox_gettoken = function ()
{
    akeeba.System.AjaxURL = akeeba.Configuration.URLs["dpecustomapi"];

    var data = {
        engine: "dropbox",
        method: "getauth"
    };

    akeeba.System.doAjax(
        data,
        function (res)
        {
            if (res["error"] != "")
            {
                alert("ERROR: Could not complete authentication; please retry");
            }
            else
            {
                document.getElementById("var[engine.postproc.dropbox.token]").value        = res.token.oauth_token;
                document.getElementById("var[engine.postproc.dropbox.token_secret]").value =
                    res.token.oauth_token_secret;
                document.getElementById("var[engine.postproc.dropbox.uid]").value          = res.token.uid;
                alert("Authentication successful!");
            }
        }, function (errorMessage)
        {
            alert("ERROR: Could not complete authentication; please retry" + "\n" + errorMessage);
        }, false, 15000
    );
};

akconfig_dropbox2_openoauth = function ()
{
    var url = akeeba.Configuration.URLs.dpeauthopen;

    if (url.indexOf("?") == -1)
    {
        url = url + "?";
    }
    else
    {
        url = url + "&";
    }

    window.open(url + "engine=dropbox2", "akeeba_dropbox2_window", "width=1010,height=500,opener");
};

akeeba_dropbox2_oauth_callback = function (data)
{
    // Update the tokens
    document.getElementById("var[engine.postproc.dropbox2.access_token]").value = data.access_token;
    document.getElementById("var[engine.postproc.dropbox2.refresh_token]").value = data.refresh_token;

    // Close the window
    myWindow = window.open("", "akeeba_dropbox2_window");
    myWindow.close();
};

akconfig_onedrive_openoauth = function ()
{
    var url = akeeba.Configuration.URLs.dpeauthopen;

    if (url.indexOf("?") == -1)
    {
        url = url + "?";
    }
    else
    {
        url = url + "&";
    }

    window.open(url + "engine=onedrive", "akeeba_onedrive_window", "width=1010,height=500,opener");
};

akeeba_onedrive_oauth_callback = function (data)
{
    // Update the tokens
    document.getElementById("var[engine.postproc.onedrive.access_token]").value  = data.access_token;
    document.getElementById("var[engine.postproc.onedrive.refresh_token]").value = data.refresh_token;

    // Close the window
    myWindow = window.open("", "akeeba_onedrive_window");
    myWindow.close();
};

akconfig_onedrivebusiness_openoauth = function ()
{
    var url = akeeba.Configuration.URLs.dpeauthopen;

    if (url.indexOf("?") == -1)
    {
        url = url + "?";
    }
    else
    {
        url = url + "&";
    }

    window.open(url + "engine=onedrivebusiness", "akeeba_onedrivebusiness_window", "width=1010,height=500,opener");
};

akeeba_onedrivebusiness_oauth_callback = function (data)
{
    // Update the tokens
    document.getElementById("var[engine.postproc.onedrivebusiness.access_token]").value  = data.access_token;
    document.getElementById("var[engine.postproc.onedrivebusiness.refresh_token]").value = data.refresh_token;

    // Close the window
    myWindow = window.open("", "akeeba_onedrivebusiness_window");
    myWindow.close();

    akeeba_onedrivebusiness_refreshdrives();
};

akeeba_onedrivebusiness_refreshdrives = function (params)
{
    if (document.getElementById('var[akeeba.advanced.postproc_engine]').value !== 'onedrivebusiness')
    {
        return;
    }

    params = params || {};

    if (typeof params["engine.postproc.onedrivebusiness.drive"] === "undefined")
    {
        params["engine.postproc.onedrivebusiness.drive"] = {
            "default": document.getElementById("var[engine.postproc.onedrivebusiness.drive]").value
        };
    }

    akeeba.System.AjaxURL = akeeba.Configuration.URLs["dpecustomapi"];

    var data = {
        engine: "onedrivebusiness",
        method: "getDrives",
        params: {
            "engine.postproc.onedrivebusiness.access_token":  document.getElementById(
                "var[engine.postproc.onedrivebusiness.access_token]").value,
            "engine.postproc.onedrivebusiness.refresh_token": document.getElementById(
                "var[engine.postproc.onedrivebusiness.refresh_token]").value
        }
    };

    akeeba.System.doAjax(
        data,
        function (res)
        {
            if (res.length === 0)
            {
                alert("ERROR: Could not retrieve list of OneDrive Drives.");
            }
            else
            {
                var dropDown       = document.getElementById("var[engine.postproc.onedrivebusiness.drive]");
                dropDown.innerHTML = "";

                for (var i = 0; i < res.length; i++)
                {
                    var elOption   = document.createElement("option");
                    elOption.value = res[i][0];
                    elOption.text  = res[i][1];

                    if (params["engine.postproc.onedrivebusiness.drive"]["default"] === elOption.value)
                    {
                        elOption.selected = true;
                    }

                    dropDown.appendChild(elOption);
                }
            }
        }, function (errorMessage)
        {
            alert("ERROR: Could not retrieve list of OneDrive Drives. Error: " + "\n" + errorMessage);
        }, false, 15000
    );
}

function akconfig_onedriveapp_openoauth()
{
    var url = akeeba.Configuration.URLs.dpeauthopen;

    if (url.indexOf("?") == -1)
    {
        url = url + "?";
    }
    else
    {
        url = url + "&";
    }

    window.open(url + "engine=onedriveapp", "akeeba_onedriveapp_window", "width=1010,height=500,opener");
}

function akeeba_onedriveapp_oauth_callback(data)
{
    // Update the tokens
    document.getElementById("var[engine.postproc.onedriveapp.access_token]").value  = data.access_token;
    document.getElementById("var[engine.postproc.onedriveapp.refresh_token]").value = data.refresh_token;

    // Close the window
    var myWindow = window.open("", "akeeba_onedriveapp_window");
    myWindow.close();
}

akconfig_googledrive_openoauth = function ()
{
    var url = akeeba.Configuration.URLs.dpeauthopen;

    if (url.indexOf("?") == -1)
    {
        url = url + "?";
    }
    else
    {
        url = url + "&";
    }

    window.open(url + "engine=googledrive", "akeeba_googledrive_window", "width=1010,height=500,opener");
};

akeeba_googledrive_oauth_callback = function (data)
{
    // Update the tokens
    document.getElementById("var[engine.postproc.googledrive.access_token]").value  = data.access_token;
    document.getElementById("var[engine.postproc.googledrive.refresh_token]").value = data.refresh_token;

    // Close the window
    myWindow = window.open("", "akeeba_googledrive_window");
    myWindow.close();

    // Refresh the list of drives
    akeeba_googledrive_refreshdrives();
};

akeeba_googledrive_refreshdrives = function (params)
{
    if (document.getElementById('var[akeeba.advanced.postproc_engine]').value !== 'googledrive')
    {
        return;
    }

    params = params || {};

    if (typeof params["engine.postproc.googledrive.team_drive"] === "undefined")
    {
        params["engine.postproc.googledrive.team_drive"] = {
            "default": document.getElementById("var[engine.postproc.googledrive.team_drive]").value
        };
    }

    akeeba.System.AjaxURL = akeeba.Configuration.URLs["dpecustomapi"];

    var data = {
        engine: "googledrive",
        method: "getDrives",
        params: {
            "engine.postproc.googledrive.access_token":  document.getElementById(
                "var[engine.postproc.googledrive.access_token]").value,
            "engine.postproc.googledrive.refresh_token": document.getElementById(
                "var[engine.postproc.googledrive.refresh_token]").value
        }
    };

    akeeba.System.doAjax(
        data,
        function (res)
        {
            if (res.length === 0)
            {
                alert("ERROR: Could not retrieve list of Google Drives.");
            }
            else
            {
                var dropDown       = document.getElementById("var[engine.postproc.googledrive.team_drive]");
                dropDown.innerHTML = "";

                for (var i = 0; i < res.length; i++)
                {
                    var elOption   = document.createElement("option");
                    elOption.value = res[i][0];
                    elOption.text  = res[i][1];

                    if (params["engine.postproc.googledrive.team_drive"]["default"] === elOption.value)
                    {
                        elOption.selected = true;
                    }

                    dropDown.appendChild(elOption);
                }
            }
        }, function (errorMessage)
        {
            alert("ERROR: Could not retrieve list of Google Drives. Error: " + "\n" + errorMessage);
        }, false, 15000
    );
};

akconfig_box_openoauth = function ()
{
    var url = akeeba.Configuration.URLs.dpeauthopen;

    if (url.indexOf("?") == -1)
    {
        url = url + "?";
    }
    else
    {
        url = url + "&";
    }

    window.open(url + "engine=box", "akeeba_box_window", "width=1010,height=500,opener");
};

akconfig_box_oauth_callback = function (data)
{
    // Update the tokens
    document.getElementById("var[engine.postproc.box.access_token]").value  = data.access_token;
    document.getElementById("var[engine.postproc.box.refresh_token]").value = data.refresh_token;

    // Close the window
    myWindow = window.open("", "akeeba_box_window");
    myWindow.close();
};

akeeba.Loader.add(['akeeba.UserInterfaceCommon', 'akeeba.System', 'akeeba.Ajax'], function ()
{
    // Get the configured URLs
    akeeba.Configuration.URLs = akeeba.System.getOptions("akeeba.Configuration.URLs", {});

    // Configuration page: we will be doing AJAX calls to the Data Processing Engine Custom API URL
    if (typeof akeeba.Configuration.URLs["dpecustomapi"] !== "undefined")
    {
        akeeba.System.params.AjaxURL = akeeba.Configuration.URLs["dpecustomapi"];
    }

    /**
     * The rest of the code only applies to the Configuration GUI.
     *
     * Therefore, if we have no Configuration GUI data (e.g. this file was included from a different view) we have to
     * return without trying to do anything else.
     */
    var guiData = akeeba.System.getOptions("akeeba.Configuration.GUIData", null);

    if (guiData === null)
    {
        return;
    }

    // Load the configuration UI data in a timeout to prevent Safari from auto-filling the password fields
    setTimeout(function ()
    {
        // Work around browsers which blatantly ignore autocomplete=off
        setTimeout(akeeba.Configuration.restoreDefaultPasswords, 1000);

        // Render the configuration UI in the timeout to prevent Safari from auto-filling the password fields
        akeeba.Configuration.parseConfigData(akeeba.System.getOptions("akeeba.Configuration.GUIData", {}));

        // Enable popovers. Must obviously run after we have the UI set up.
        akeeba.Configuration.enablePopoverFor(document.querySelectorAll("[rel=\"popover\"]"));

        // Reload drive lists if applicable
        akeeba_googledrive_refreshdrives(akeeba.Configuration.engines.postproc.googledrive.parameters);
        akeeba_onedrivebusiness_refreshdrives(akeeba.Configuration.engines.postproc.onedrivebusiness.parameters);

        // Post the form as a big JSON blob if possible
        const adminForm = document.getElementById('adminForm');

        if (adminForm) {
            adminForm.addEventListener('submit', akeeba.Configuration.onFormSubmit);
        }
    }, 10);
});