/** * Copyright (c) 2016 Weitian LI * MIT license * * Web UI of "fg21sim" * Configuration form manipulations */ "use strict"; /** * Set custom error messages for the form fields that failed the server-side * validations. * * NOTE: * Add "error" class for easier manipulations, e.g., clearFormConfigErrors(). * * References: Constraint Validation API * * @param {String} name - The name of filed name * @param {String} error - The custom error message to be set for the field */ var setFormConfigErrorSingle = function (name, error) { var selector; if (name === "userconfig") { selector = "input[name=configfile]"; } else { selector = "input[name='" + name + "']"; } $(selector).each(function () { // Reset the error message this.setCustomValidity(error); // Also add the "error" class for easier use later $(this).addClass("error"); }); }; /** * Clear the custom error states marked on the form fields that failed the * server-side validations. * * NOTE: The form fields marked custom errors has the "error" class. * * References: Constraint Validation API */ var clearFormConfigErrors = function () { $("input.error").each(function () { // Remove the dynamically added "error" class $(this).removeClass("error"); // Reset the error message this.setCustomValidity(""); }); }; /** * Reset the configuration form to its defaults as written in the HTML. * * Credit: http://stackoverflow.com/a/6364313 */ var resetFormConfigs = function () { $("#conf-form")[0].reset(); // Clear previously marked errors clearFormConfigErrors(); }; /** * Get the value of one single form field by specifying the name. * * @param {String} name - The name of filed name * * @returns value - value of the configuration option field * + `null` if the field is empty (no value) * + `undefined` if the field does not exists */ var getFormConfigSingle = function (name) { var value = undefined; if (! name) { console.error("Invalid name:", name); } else if (name === "userconfig") { value = joinPath($("input[name=workdir]").val(), $("input[name=configfile]").val()); } else { var selector = "input[name='" + name + "']"; var target = $(selector); if (target.length) { if (target.is(":radio")) { value = target.filter(":checked").val(); } else if (target.is(":checkbox") && target.data("type") === "boolean") { // Convert the checkbox value into boolean value = target.prop("checked"); } else if (target.is(":checkbox")) { // Get values of checked checkboxes into array // Credit: https://stackoverflow.com/a/16171146/4856091 value = target.filter(":checked").map( function () { return $(this).val(); }).get(); } else if (target.is(":text") && target.data("type") === "array") { // Convert comma-separated string back to Array value = target.val().split(/\s*,\s*/); if (value.length === 1 && value[0] === "") { value = []; // Empty Array } } else { value = target.val(); } // NOTE: convert "" (empty string) back to `null` if (value === "") { value = null; } } else { value = undefined; console.error("No such element:", selector); } } return value; }; /** * Collect all the current configurations and values from the form. * * @returns {Object} key-value pairs of the form configurations */ var getFormConfigAll = function () { var names = $("#conf-form").find("input[name]").map( function () { return $(this).attr("name"); }).get(); names = $.unique(names); var data = {}; names.forEach(function (name) { data[name] = getFormConfigSingle(name); }); // Do not forget the "userconfig" data["userconfig"] = getFormConfigSingle("userconfig"); // Delete unwanted items ["workdir", "configfile", "_xsrf"].forEach(function (name) { delete data[name]; }); console.log("Collected form configurations data:", data); return data; }; /** * Set the value of one single form field according to the given * name and value. * * NOTE: * - Do NOT trigger the "change" event, which leads to recursive * request/response between the client and server. * - Trigger the "Enter keypress" event, to update the related contents, * e.g., the pixel resolution note w.r.t. "common/nside" * * @param {String} name - The name of filed name * @param {String|Number|Array} value - The value to be set for the field */ var setFormConfigSingle = function (name, value) { if (name === "userconfig") { if (value) { // Split the absolute path to "workdir" and "configfile" var workdir = dirname(value); var configfile = basename(value); $("input[name=workdir]").val(workdir); $("input[name=configfile]").val(configfile); } else { $("input[name=workdir]").val(""); $("input[name=configfile]").val(""); } } else { var selector = "input[name='" + name + "']"; var target = $(selector); if (target.length) { if (target.is(":radio")) { target.val([value]); // Use Array in "val()" } else if (target.is(":checkbox") && target.data("type") == "boolean") { // Convert the checkbox value into boolean target.prop("checked", value); } else if (target.is(":checkbox")) { // The received value is already an Array target.val(value); } else if (target.is(":text") && target.data("type") == "array") { // Convert array of values into a string value = value.join(", "); target.val(value); } else { target.val(value); } // Manually trigger the "Enter keypress" event to update related contents target.trigger($.Event("keypress", {which: 13})); } else { console.error("No such element:", selector); } } }; /** * Set the configuration form to the supplied data, and mark out the fields * with error states as specified in the given errors. * * @param {Object} data - The input configurations data, key-value pairs. * @param {Object} errors - The config options with invalid values. */ var setFormConfigs = function (data, errors) { // Set the values of form field to the input configurations data $.each(data, function (name, value) { if (value === null) { value = ""; // Default to empty string } var val_old = getFormConfigSingle(name); if (typeof val_old !== "undefined" && val_old !== value) { setFormConfigSingle(name, value); console.log("Set input '" + name + "' to:", value, " <-", val_old); } }); // Clear previously marked errors clearFormConfigErrors(); // Mark custom errors on fields with invalid values validated by the server $.each(errors, function (name, error) { setFormConfigErrorSingle(name, error); }); }; /** * Update the configuration form status indicator: "#conf-status" * * NOTE: * Also store the current validity status in a custom data attribute: * `validity`, which has a boolean value. */ var updateFormConfigStatus = function () { var target = $("#conf-status"); var recheck_icon = $("#conf-recheck"); var invalid = $("#conf-form").find("input[name]:invalid"); if (invalid.length) { // Exists invalid configurations console.warn("Found", invalid.length, "invalid configurations!"); recheck_icon.show(); target.removeClass("label-default label-success") .addClass("label-warning"); target.find(".icon").removeClass("fa-question-circle fa-check-circle") .addClass("fa-warning"); target.find(".text").text("Invalid!"); target.data("validity", false); } else { // All valid // console.info("Great, all configurations are valid :)"); recheck_icon.hide(); target.removeClass("label-default label-warning") .addClass("label-success"); target.find(".icon").removeClass("fa-question-circle fa-warning") .addClass("fa-check-circle"); target.find(".text").text("OK"); target.data("validity", true); } }; /** * Get the configurations from the server and update the client form * to the newly received values. * * NOTE: * The configurations are not validated on the server, therefore, * there is no validation error returned. * For the validation, see function `validateServerConfigs()`. * * @param {String} url - The URL that handles the "configs" AJAX requests. * @param {Array} [keys=null] - List of keys whose values will be requested. * If `null` then request all configurations. */ var getServerConfigs = function (url, keys) { keys = typeof keys !== "undefined" ? keys : null; return $.getJSONUncached( url, {action: "get", keys: JSON.stringify(keys)}, function (response) { setFormConfigs(response.data, {}); }); }; /** * Validate the server-side configurations to get the validation errors, * and mark the corresponding form fields to be invalid with details. */ var validateServerConfigs = function (url) { return $.getJSONUncached( url, {action: "validate"}, function (response) { setFormConfigs({}, response.errors); }); }; /** * Reset the server-side configurations to the defaults, then sync back to * the client-side form configurations. */ var resetConfigs = function (url) { $.postJSON(url, {action: "reset"}) .done(function () { // Server-side configurations already reset resetFormConfigs(); // Sync server-side configurations back to the client getServerConfigs(url) .then(function () { return validateServerConfigs(url); }) .done(function () { // Update the configuration status label updateFormConfigStatus(); // Popup a modal notification showModal({ icon: "check-circle", contents: "Reset and synchronized the configurations." }); }); }) .fail(function (jqxhr) { showModal({ icon: "times-circle", contents: "Failed to reset the configurations!", code: jqxhr.status, reason: jqxhr.statusText }); }); }; /** * Set the server-side configurations using the sent data from the client. * * NOTE: * The supplied configuration data are validated on the server side, and * the validation errors are sent back. * However, the whole configurations is NOT checked, therefore, function * `validateServerConfigs()` should be used if necessary. * * @param {Object} [data={}] - Group of key-value pairs that to be sent to * the server to update the configurations there. */ var setServerConfigs = function (url, data) { data = typeof data !== "undefined" ? data : {}; return $.postJSON( url, {action: "set", data: data}, function (response) { setFormConfigs(response.data, response.errors); }) .fail(function (jqxhr) { showModal({ icon: "times-circle", contents: "Failed to update/set the configuration data!", code: jqxhr.status, reason: jqxhr.statusText }); }); }; /** * Request the server to load/merge the configurations from the specified * user configuration file. * * @param {Object} userconfig - Absolute path to the user config file on the * server. If not specified, then determine from * the form fields "workdir" and "configfile". */ var loadServerConfigFile = function (url, userconfig) { if (! userconfig) { userconfig = getFormConfigSingle("userconfig"); } return $.postJSON(url, {action: "load", userconfig: userconfig}) .fail(function (jqxhr) { showModal({ icon: "times-circle", contents: "Failed to load the user configuration file!", code: jqxhr.status, reason: jqxhr.statusText }); }); }; /** * Request the server to save current configurations to the supplied output * file. * * @param {Boolean} [clobber=false] - Whether overwrite the existing file. */ var saveServerConfigFile = function (url, clobber) { clobber = typeof clobber !== "undefined" ? clobber : false; var userconfig = getFormConfigSingle("userconfig"); var data = { action: "save", outfile: userconfig, clobber: clobber }; return $.postJSON(url, data) .done(function () { var modalData = {}; if ($("#conf-status").data("validity")) { // Form configurations is valid :) modalData.icon = "check-circle"; modalData.contents = "Configurations saved to file."; } else { // Configurations is currently invalid! modalData.icon = "warning"; modalData.contents = ("Configurations saved to file. " + "But there exist some invalid values!"); } showModal(modalData); }) .fail(function (jqxhr) { showModal({ icon: "times-circle", contents: "Failed to save the configurations!", code: jqxhr.status, reason: jqxhr.statusText }); }); }; /** * Check whether the specified file already exists on the server? */ var existsServerFile = function (url, filepath, callback) { var data = { action: "exists", filepath: JSON.stringify(filepath) }; return $.getJSONUncached(url, data, callback) .fail(function (jqxhr) { showModal({ icon: "times-circle", contents: ("Failed to check the existence " + "of the user configuration file!"), code: jqxhr.status, reason: jqxhr.statusText }); }); }; /** * Handle the received message of type "configs" pushed through the WebSocket */ var handleWebSocketMsgConfigs = function (msg) { if (msg.action === "push") { // Pushed configurations (with validations) of current state on the server setFormConfigs(msg.data, msg.errors); updateFormConfigStatus(); } else { console.warn("WebSocket: received message:", msg); } }; $(document).ready(function () { // URL to handle the "configs" AJAX requests var ajax_url = "/ajax/configs"; // Re-check/validate the whole form configurations $("#conf-recheck").on("click", function () { var data = getFormConfigAll(); setServerConfigs(ajax_url, data) .then(function () { return validateServerConfigs(ajax_url); }) .done(function () { updateFormConfigStatus(); }); }); // Reset both server-side and client-side configurations to the defaults $("#reset-defaults").on("click", function () { showModal({ icon: "warning", contents: "Are you sure to reset the configurations?", buttons: [ { text: "Cancel", click: function () { $.modal.close(); } }, { text: "Reset!", "class": "button-warning", // NOTE: "class" is a preserved keyword click: function () { $.modal.close(); resetConfigs(ajax_url); } } ] }); }); // Load the configurations from the specified user configuration file $("#load-configfile").on("click", function () { var userconfig = getFormConfigSingle("userconfig"); resetFormConfigs(); loadServerConfigFile(ajax_url, userconfig) .then(function () { return getServerConfigs(ajax_url); }) .then(function () { return validateServerConfigs(ajax_url); }) .done(function () { // Update the configuration status label updateFormConfigStatus(); // Popup a modal notification showModal({ icon: "check-circle", contents: "Loaded the configurations from file." }); }); }); // Save the current configurations to file $("#save-configfile").on("click", function () { var userconfig = getFormConfigSingle("userconfig"); existsServerFile(ajax_url, userconfig, function (response) { if (response.data.exists) { // The specified configuration file already exists // Confirm to overwrite showModal({ icon: "warning", contents: "Configuration file already exists! Overwrite?", buttons: [ { text: "Cancel", rel: "modal:close", click: function () { $.modal.close(); } }, { text: "Overwrite!", "class": "button-warning", rel: "modal:close", click: function () { $.modal.close(); saveServerConfigFile(ajax_url, true); } } ] }); } else { saveServerConfigFile(ajax_url, false); } }); }); // Sync changed field to server, validate and update form $("#conf-form input").on("change", function (e) { var name = e.target.name; var value = getFormConfigSingle(name); // Synchronize the changed form configuration to the server // NOTE: // Use the "computed property names" available in ECMAScript 6 // (IE 11 not support this!) // var data = {[name]: value}; var data = {}; data[name] = value; setServerConfigs(ajax_url, data) .then(function () { return validateServerConfigs(ajax_url); }) .done(function () { updateFormConfigStatus(); }); }); // Update the resolution note for field "common/nside" when press "Enter" $("#conf-form input[name='common/nside']").on("keypress", function (e) { var nside, resolution; if (e.which === 13) { nside = parseInt($(this).val(), 10); // Update the resolution note (unit: arcmin) resolution = Math.sqrt(3/Math.PI) * 3600 / nside; $(this).closest(".form-group").find(".note > .value") .text(resolution.toFixed(2)); } }); // Update the maximum multiple "common/lmax" when "common/nside" changed $("#conf-form input[name='common/nside']").on("change", function (e) { var nside, lmax; // Update the resolution note $(this).trigger($.Event("keypress", {which: 13})); nside = parseInt($(this).val(), 10); if (isFinite(nside)) { lmax = 3 * nside - 1; $("#conf-form input[name='common/lmax']").val(lmax).trigger("change"); } }); });