From patchwork Sat Apr 1 14:43:42 2023 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Leo-Andres Hofmann X-Patchwork-Id: 6756 Return-Path: Received: from mail01.ipfire.org (mail01.haj.ipfire.org [172.28.1.202]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature ECDSA (P-384) client-signature ECDSA (P-384)) (Client CN "mail01.haj.ipfire.org", Issuer "R3" (verified OK)) by web04.haj.ipfire.org (Postfix) with ESMTPS id 4Ppg0P49Pdz3x2l for ; Sat, 1 Apr 2023 14:44:21 +0000 (UTC) Received: from mail02.haj.ipfire.org (mail02.haj.ipfire.org [172.28.1.201]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature ECDSA (P-384) client-signature ECDSA (P-384)) (Client CN "mail02.haj.ipfire.org", Issuer "R3" (verified OK)) by mail01.ipfire.org (Postfix) with ESMTPS id 4Ppg0P01YRz2wv; Sat, 1 Apr 2023 14:44:21 +0000 (UTC) Received: from mail02.haj.ipfire.org (localhost [127.0.0.1]) by mail02.haj.ipfire.org (Postfix) with ESMTP id 4Ppg0N6cJ4z2yNd; Sat, 1 Apr 2023 14:44:20 +0000 (UTC) Received: from mail01.ipfire.org (mail01.haj.ipfire.org [172.28.1.202]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature ECDSA (P-384) client-signature ECDSA (P-384)) (Client CN "mail01.haj.ipfire.org", Issuer "R3" (verified OK)) by mail02.haj.ipfire.org (Postfix) with ESMTPS id 4Ppg0N3cm0z2xkd for ; Sat, 1 Apr 2023 14:44:20 +0000 (UTC) Received: from arche.uberspace.de (arche.uberspace.de [185.26.156.147]) (using TLSv1.2 with cipher ECDHE-ECDSA-AES256-GCM-SHA384 (256/256 bits)) (Client did not present a certificate) by mail01.ipfire.org (Postfix) with ESMTPS id 4Ppg0M6nzvzhR for ; Sat, 1 Apr 2023 14:44:19 +0000 (UTC) Authentication-Results: mail01.ipfire.org; dkim=pass header.d=leo-andres.de header.s=uberspace header.b=B+J6ai1t; spf=pass (mail01.ipfire.org: domain of hofmann@leo-andres.de designates 185.26.156.147 as permitted sender) smtp.mailfrom=hofmann@leo-andres.de; dmarc=pass (policy=quarantine) header.from=leo-andres.de ARC-Seal: i=1; s=202003rsa; d=lists.ipfire.org; t=1680360260; a=rsa-sha256; cv=none; b=RnEayqr2jIV+3vNEdJPABFd6rOktI40klxamn+0R0GGUZfuklJ+3TLZ+VEg4w0WG6MCKL2 LDr/QQtrhyieiNqrhs2ubgBddqwintyh0Q96/lhGhgDukGlfx1hwiltIEogitF/auRcKpo +i5fUO+FQacm/NbbvFUY10e4RTAs2Gly8nd+fiq92pyCVvbHR01ONrDyWyC/1j++6HVxIP 6hbnRGH7/t5ye5XVX86/yNZtvgcCATav6oRg4u0MXwwiLR3YveE9ZEomCpPSjMLHI/uWVN 2biAS2B0KNvm8VR1Fu9IcucFfLy1uf7fVUtA2q3XwWyaxWgRR3QYA30hCH/1lQ== ARC-Authentication-Results: i=1; mail01.ipfire.org; dkim=pass header.d=leo-andres.de header.s=uberspace header.b=B+J6ai1t; spf=pass (mail01.ipfire.org: domain of hofmann@leo-andres.de designates 185.26.156.147 as permitted sender) smtp.mailfrom=hofmann@leo-andres.de; dmarc=pass (policy=quarantine) header.from=leo-andres.de ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=lists.ipfire.org; s=202003rsa; t=1680360260; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references:dkim-signature; bh=8Kax0OJAwR5nPLW4WPerYj2QDljq/yZJ6QHNcoUxvf0=; b=c5i2Pr/6c67jQtmiv0NpOsD3ZcIdVVnjgYjRihV/WS7H+AfKmqi0s1GrOWQ23peIt4TZFF Acsb9s0pG5Cq8f/vRpraXABlfiQJBgChCUOOayr0PC8bypGnRh1C8/GvEkxGjv0sPu0P5o BrEUSYilffRfhobpFPo4EJI1rHhB23UCYZBkAtlfaBkp6JjFPOMlkaw+MzpOhmokevhliI 7l0L3ETiGjj/Ty1PKm3sW/kcRfjFX1MbNUDQaNUJQdlt6HKjoo8Ff0YT/LGzm2bZ54EPym nYcpJEvWphp5UqeB40KzXbLuM7Rwy/SGRJgL82ZzrEzbrN+iRvr5ll9V3BdEQQ== Received: (qmail 16989 invoked by uid 990); 1 Apr 2023 14:44:19 -0000 From: Leo-Andres Hofmann To: development@lists.ipfire.org Subject: [PATCH 3/4] WUI: Implement form confirmation dialog Date: Sat, 1 Apr 2023 16:43:42 +0200 Message-Id: <20230401144343.1483-3-hofmann@leo-andres.de> X-Mailer: git-send-email 2.37.1.windows.1 In-Reply-To: <20230401144343.1483-1-hofmann@leo-andres.de> References: <20230401144343.1483-1-hofmann@leo-andres.de> MIME-Version: 1.0 X-Rspamd-Bar: ----- X-Rspamd-Report: R_MISSING_CHARSET(0.5) MIME_GOOD(-0.1) REPLY(-4) MID_CONTAINS_FROM(1) BAYES_HAM(-3) X-Rspamd-Score: -5.6 Received: from unknown (HELO unkown) (::1) by arche.uberspace.de (Haraka/3.0.1) with ESMTPSA; Sat, 01 Apr 2023 16:44:19 +0200 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=leo-andres.de; s=uberspace; h=from; bh=i8kAbb/rbuYBRkcGtFQQcsbh5LA/xkFdqTHzMSKky50=; b=B+J6ai1tuXu6TorUaDwcEWcQmTfCdJl4rp1EAgx3Dpdj/qvVMM1uxa0OwFJbMIpZlW+6d7ux1T KYUuYhmKS0PcLLeNxSUn/qdfOkSd3PFJyCV6B2IXPlaW9Ew2npbSsp3J2zsm8X6rFZnadvY12nB0 B0NJmGRY9/TQZGzzBbZ7jAL4H9WbzBbhYAygV1dpMVo2iSmDXJY4dHaPKrr3Csv1lkUl+u4io1By 9TprEfxcVVNtiRA96YRVgGuOcArfmHNCYNxtudRbCmpRkFE/WykeKmKJZSGBMdaASlQiwMgrvBL+ 5OYc/XFAi2csvC/7B0ybrzVVAg3V3pTztciZVXBLhKatWd+v/vyN5R395wjv8LRziKb/hOQakWEa 4t7RIswwz8tAL/Sd9b6axnHkOGuoQ2ArReTplTPsIU9c9i6NjN3Dpo25spLCVczldATCbPGQRVA+ zLaDZrr7gOgBcEYuZ+gFc9U4HQiaJKRC2EDUsh0p7FTqTker503n/fY9eN4cntLIS2SzuoEZvyGM SKrxMGgnPkOssRzFIxFzky/NM9mBS2JcnXMCFC9e+jdIbfxSenEcbeDfOHx8lKw15SgswQepba7H TK/KgSqfI5h1UlgpMwbOc8WmzEjPlAnE5TCjM0xpjyRwFGbbl2oMMCaqJ+cpkbU1sCP0u7nX7Nwt c= X-Rspamd-Action: no action X-Rspamd-Server: mail01.haj.ipfire.org X-Rspamd-Queue-Id: 4Ppg0M6nzvzhR X-Spamd-Result: default: False [-5.90 / 11.00]; REPLY(-4.00)[]; BAYES_HAM(-3.00)[99.99%]; MID_CONTAINS_FROM(1.00)[]; R_MISSING_CHARSET(0.50)[]; MX_INVALID(0.50)[]; DMARC_POLICY_ALLOW(-0.50)[leo-andres.de,quarantine]; R_DKIM_ALLOW(-0.20)[leo-andres.de:s=uberspace]; R_SPF_ALLOW(-0.20)[+ip4:185.26.156.0/24:c]; ONCE_RECEIVED(0.10)[]; MIME_GOOD(-0.10)[text/plain]; IP_REPUTATION_HAM(-0.00)[asn: 205766(0.00), country: DE(-0.00), ip: 185.26.156.147(0.00)]; RCVD_VIA_SMTP_AUTH(0.00)[]; ARC_NA(0.00)[]; TO_MATCH_ENVRCPT_SOME(0.00)[]; FROM_HAS_DN(0.00)[]; ASN(0.00)[asn:205766, ipnet:185.26.156.0/24, country:DE]; RCVD_COUNT_ONE(0.00)[1]; FROM_EQ_ENVFROM(0.00)[]; ARC_SIGNED(0.00)[lists.ipfire.org:s=202003rsa:i=1]; TO_DN_SOME(0.00)[]; MIME_TRACE(0.00)[0:+]; DKIM_REPUTATION(0.00)[0]; RCPT_COUNT_TWO(0.00)[2]; DKIM_TRACE(0.00)[leo-andres.de:+]; RCVD_IN_DNSWL_NONE(0.00)[185.26.156.147:from]; RCVD_TLS_ALL(0.00)[] X-BeenThere: development@lists.ipfire.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: IPFire development talk List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: development-bounces@lists.ipfire.org Sender: "Development" This patch adds a modal confirmation dialog for HTML forms. The dialog can be invoked by setting the "data-confirm-message" attribute on the form. Default texts can be overridden with the "data-confirm-title" and "data-confirm-subheading" attributes. Signed-off-by: Leo-Andres Hofmann --- html/html/include/wui.js | 6 ++ html/html/include/wui_core.mjs | 77 ++++++++++++++- html/html/include/wui_dialogs.mjs | 97 +++++++++++++++++++ html/html/themes/ipfire/include/css/style.css | 75 ++++++++++++++ html/html/themes/ipfire/include/functions.pl | 28 ++++++ langs/en/cgi-bin/en.pl | 4 + 6 files changed, 286 insertions(+), 1 deletion(-) create mode 100644 html/html/include/wui_dialogs.mjs diff --git a/html/html/include/wui.js b/html/html/include/wui.js index e65924e29..e219f5ca4 100644 --- a/html/html/include/wui.js +++ b/html/html/include/wui.js @@ -23,6 +23,7 @@ import {WUIcore_i18n as WUI_i18n} from "./wui_core.mjs"; +import {WUIdialog_confirm as WUI_confirm} from "./wui_dialogs.mjs"; import {WUImodule_rrdimage as WUI_rrdimage} from "./wui_rrdimage.mjs"; //--- WUI main class --- @@ -33,11 +34,16 @@ class WUImain { this.i18n = new WUI_i18n(); //- Modules - + // Dialogs + this.dialogs = {}; + this.dialogs.confirm = new WUI_confirm(this.i18n); + // RRDtool graph images this.rrdimage = new WUI_rrdimage(this.i18n); //- Defaults - // These modules are available on every page: + this.dialogs.confirm.enabled = true; this.rrdimage.enabled = true; } } diff --git a/html/html/include/wui_core.mjs b/html/html/include/wui_core.mjs index b7b729396..ab5338f12 100644 --- a/html/html/include/wui_core.mjs +++ b/html/html/include/wui_core.mjs @@ -27,7 +27,7 @@ export class WUIcore_moduleBase { //- Private properties - #enabled; // Activation state, disabled by default #readyState; // Loading state similar to Document.readyState - #namespace; // Namespace derived from the class name (without "WUImod_" prefix) + #namespace; // Namespace derived from the class name (without "WUImodule_"/"WUIdialog_" prefix) //- Class constructor - constructor(translations) { @@ -101,6 +101,81 @@ export class WUIcore_moduleBase { } } +//--- Modal dialog template --- +// Make sure that overridden functions are still executed with super()! +// Text fields and buttons are identified by their data-... attributes: +// data-textbox="foo", data-action="bar". See setText for reference. +// Events should only be managed by the modules, there is no public interface by design. +export class WUIcore_dialogBase extends WUIcore_moduleBase { + //- Private properties - + #modalId; // Element ID of the overlay div box with dialog window + + //- Class constructor - + constructor(translations) { + super(translations); + + //- Protected properties - + // jQuery object, reference to dialog div box + this._$modal = $(); + } + + // DOMContentLoaded/jQuery.ready event handler + _handleDOMReady() { + super._handleDOMReady(); + + // modalId was set before, but the document was not ready yet + if(this.modalId && (! this.hasModal)) { + this._$modal = $(`#${this.modalId}`).first(); + } + } + + // Element ID of dialog overlay div box + // This element must be hidden by default and render a modal dialog when activated. + set modalId(id) { + this.#modalId = id; + + // Delay attaching element until DOM is ready + if(this.readyState === "complete") { + this._$modal = $(`#${id}`).first(); + } + } + get modalId() { + return this.#modalId; + } + + // Check if modal dialog element has been attached + get hasModal() { + return (this._$modal.length === 1); + } + + // Show/hide modal overlay by setting CSS "display" property + // The top modal element should always be a simple overlay, so that display=block does not disturb the layout. + showModal() { + this._$modal.css("display", "block"); + } + hideModal() { + this._$modal.css("display", "none"); + } + get isVisible() { + return (this._$modal.css("display") === "block"); + } + + // Set text field content. Fields are identified by their data-textbox attribute: + // -> field "message" + setText(field, value) { + this._$modal.find(`[data-textbox="${field}"]`).text(value); + } + + //### Protected properties ### + + // Get a dialog window button as a jQuery object. Buttons are identified by their data-action attribute: + // -> action "submit" + // Button actions must be unique within the modal window. Events are managed internally only, do not add custom handlers. + _$getButton(action) { + return this._$modal.find(`button[data-action="${action}"]`).first(); + } +} + //--- Simple translation strings helper --- export class WUIcore_i18n { //- Private properties - diff --git a/html/html/include/wui_dialogs.mjs b/html/html/include/wui_dialogs.mjs new file mode 100644 index 000000000..a2b3bdbb4 --- /dev/null +++ b/html/html/include/wui_dialogs.mjs @@ -0,0 +1,97 @@ +/*############################################################################# +# # +# IPFire.org - A linux based firewall # +# Copyright (C) 2007-2023 IPFire Team # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +#############################################################################*/ + +// IPFire Web User Interface - JavaScript module + +import {WUIcore_dialogBase as WUI_dialog} from "./wui_core.mjs"; + +//--- Form submit confirm dialog --- +// Text fields: data-textbox "title" "subheading" "message" +// Buttons: data-action "submit" "cancel" "close" +export class WUIdialog_confirm extends WUI_dialog { + + // DOMContentLoaded/jQuery.ready event handler + _handleDOMReady() { + super._handleDOMReady(); + + // Process all forms with confirmation request, attach submit events + $("form[data-confirm-message]").each((i, formElem) => { + const $form = $(formElem); + $form.on(`submit.${this.namespace}`, {"form": $form, "message": $form.data("confirmMessage")}, this.#handleFormSubmit.bind(this)); + }); + } + + // Form with confirmation "submit" event handler + async #handleFormSubmit(event) { + event.preventDefault(); + + const $form = event.data["form"]; + this.prepareModal(event.data["message"], $form.data("confirmTitle"), $form.data("confirmSubheading")); + + // Show the dialog and wait for user interaction + try { + const response = await this.requestAsync(); + if(response === true) { + // Trigger native HTML submit() method, since it does not raise another submit event that would cause a loop + event.currentTarget.submit(); + } + } catch(error) { + // User closed the window, do nothing + } + } + + // Show modal confirmation request dialog + // Returns a promise that resolves true/false upon user interaction and rejects when the window is closed. + requestAsync() { + // Attach promise to submit/cancel button "click" events + const whenConfirm = new Promise((resolve, reject) => { + this._$getButton("submit").on(`click.${this.namespace}`, () => { resolve(true); }); + this._$getButton("cancel").on(`click.${this.namespace}`, () => { resolve(false); }); + + this._$getButton("close").on(`click.${this.namespace}`, () => { reject(new Error("dialog window closed")); }); + }).finally(() => { + // Always hide the window and detach all button events after any click, + // so that the dialog can be used multiple times without side effects. + this.hideModal(); + this.#clearButtonEvents(); + }); + + // Show dialog, default action is "close" + this.showModal(); + this._$getButton("close").focus(); + + return whenConfirm; + } + + // Prepare dialog window: set texts, but leave hidden and without events + prepareModal(message, title = undefined, subheading = undefined) { + this.hideModal(); + this.#clearButtonEvents(); + + this.setText("message", message); + this.setText("title", title ?? this._i18n("title")); + this.setText("subheading", subheading ?? this._i18n("subheading")); + } + + // Remove all active events from dialog window buttons + #clearButtonEvents() { + this._$modal.find(`button[data-action]`).off(`.${this.namespace}`); + } +} diff --git a/html/html/themes/ipfire/include/css/style.css b/html/html/themes/ipfire/include/css/style.css index 96d0519f5..f4ef1769c 100644 --- a/html/html/themes/ipfire/include/css/style.css +++ b/html/html/themes/ipfire/include/css/style.css @@ -377,3 +377,78 @@ div.rrdimage > img { max-width: 100%; min-height: 290px; } + +/* Modal dialog box */ + +div.dialog-overlay { + display: none; + z-index: 99; + + left: 0; + top: 0; + width: 100%; + height: 100%; + position: fixed; + overflow: auto; + + background-color: rgba(0, 0, 0, 0.5); + animation: fadeIn 0.5s; +} +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +div.dialog-window { + top: 15%; + width: 550px; + margin: auto; + padding: 0; + position: relative; + + border: 1px solid black; + background: #fff url('../../images/n2.gif') 0px 0px repeat-x; + border-radius: 3px; + box-shadow: 3px 3px 15px rgba(0, 0, 0, 0.7); +} + +.dialog-window .section { + box-sizing: border-box; + padding: 1em 1.5em; + text-align: left; +} +.dialog-window .section:not(:last-child) { + border-bottom: 1px solid silver; +} + +.dialog-window .header > h3 { + display: inline-block; + color: #66000F; + font-size: 1.6em; +} +.dialog-window .header > button[data-action="close"] { + border: none; + background: none; + font-size: 1.2em; + font-weight: bold; + cursor: pointer; +} + +.dialog-window .content { + line-height: 150%; +} + +.dialog-window .header, +.dialog-window .controls { + display:flex; + flex-direction: row; + align-items: center; + justify-content: space-between; +} +.dialog-window .controls > button { + padding: 0.3em; + min-width: 35%; +} +.dialog-window .controls > button[data-action="submit"] { + font-weight: bold; +} diff --git a/html/html/themes/ipfire/include/functions.pl b/html/html/themes/ipfire/include/functions.pl index bc66d7fdb..784b2f398 100644 --- a/html/html/themes/ipfire/include/functions.pl +++ b/html/html/themes/ipfire/include/functions.pl @@ -217,6 +217,34 @@ print <$system_release + + + + END diff --git a/langs/en/cgi-bin/en.pl b/langs/en/cgi-bin/en.pl index 729516538..df9deda53 100644 --- a/langs/en/cgi-bin/en.pl +++ b/langs/en/cgi-bin/en.pl @@ -84,6 +84,10 @@ 'ConnSched up' => 'Up', 'ConnSched weekdays' => 'Days of the week:', 'Daily' => 'Daily', +'dialog confirm title' => 'Permanently save changes?', +'dialog confirm subheading' => 'The following action cannot be undone:', +'dialog confirm submit' => 'Yes, continue', +'dialog confirm cancel' => 'No, cancel', 'Disabled' => 'Disabled', 'Edit an existing route' => 'Edit an existing route', 'Enter TOS' => 'Activate or deactivate TOS-bits
and then press Save.',