From a44b31d173d8ab16065ced935535efc54582cfd6 Mon Sep 17 00:00:00 2001 From: tomasz1986 Date: Mon, 25 Sep 2023 21:17:57 +0200 Subject: [PATCH] gui: Fix body padding infinitely increasing due to overlapping modals (ref #9063) (#9078) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Opening and hiding multiple modals at the same time as well as opening a modal before fully hiding the previous one can lead to the body padding infinitely increasing by the scrollbar width each time, with the only way to fix it being refreshing the GUI. Therefore, always try to ensure to open and hide multiple modals one by one, and also that the previous modal has fully been hidden before proceeding to open the next one. The most common case when this problem happens is when saving config changes which displays a GUI blocking modal that overlaps, e.g. with folder or device modals that have not been hidden yet. Ref: https://github.com/twbs/bootstrap/issues/3902#issuecomment-1547187799 Signed-off-by: Tomasz WilczyƄski --- .../syncthing/core/syncthingController.js | 221 ++++++++++++------ 1 file changed, 153 insertions(+), 68 deletions(-) diff --git a/gui/default/syncthing/core/syncthingController.js b/gui/default/syncthing/core/syncthingController.js index 0a9a10241..efbdbba17 100755 --- a/gui/default/syncthing/core/syncthingController.js +++ b/gui/default/syncthing/core/syncthingController.js @@ -138,7 +138,7 @@ angular.module('syncthing.core') $scope.reportData = data; if ($scope.system && $scope.config.options.urAccepted > -1 && $scope.config.options.urSeen < $scope.system.urVersionMax && $scope.config.options.urAccepted < $scope.system.urVersionMax) { // Usage reporting format has changed, prompt the user to re-accept. - $('#ur').modal(); + showModal('#ur'); } }).error($scope.emitHTTPError); @@ -150,9 +150,9 @@ angular.module('syncthing.core') online = true; restarting = false; - $('#networkError').modal('hide'); - $('#restarting').modal('hide'); - $('#shutdown').modal('hide'); + hideModal('#networkError'); + hideModal('#restarting'); + hideModal('#shutdown'); }).catch($scope.emitHTTPError); }); @@ -164,7 +164,7 @@ angular.module('syncthing.core') console.log('UIOffline'); online = false; if (!restarting) { - $('#networkError').modal(); + showModal('#networkError'); } }); @@ -186,10 +186,10 @@ angular.module('syncthing.core') } else if (arg.status >= 400 && arg.status <= 599 && arg.status != 501) { // A genuine HTTP error. 501/NotImplemented is considered intentional // and not an error which we need to act upon. - $('#networkError').modal('hide'); - $('#restarting').modal('hide'); - $('#shutdown').modal('hide'); - $('#httpError').modal(); + hideModal('#networkError'); + hideModal('#restarting'); + hideModal('#shutdown'); + showModal('#httpError'); } } }); @@ -325,7 +325,7 @@ angular.module('syncthing.core') document.cookie = "firstVisit=" + Date.now() + ";max-age=" + 30 * 24 * 3600; } else { if (+firstVisit < Date.now() - 4 * 3600 * 1000) { - $('#ur').modal(); + showModal('#ur'); } } } @@ -1331,7 +1331,7 @@ angular.module('syncthing.core') $scope.showDeviceIdentification = function (deviceCfg) { $scope.currentDevice = deviceCfg; - $('#idqr').modal(); + showModal('#idqr'); }; $scope.setDevicePause = function (device, pause) { @@ -1362,7 +1362,7 @@ angular.module('syncthing.core') params.heading = $translate.instant("Listener Status"); } $scope.connectivityStatusParams = params; - $('#connectivity-status').modal(); + showModal('#connectivity-status'); }; $scope.showDiscoveryStatus = function () { @@ -1377,7 +1377,7 @@ angular.module('syncthing.core') params.heading = $translate.instant("Discovery Status"); } $scope.connectivityStatusParams = params; - $('#connectivity-status').modal(); + showModal('#connectivity-status'); }; $scope.logging = { @@ -1401,7 +1401,7 @@ angular.module('syncthing.core') $scope.logging.timer = $timeout($scope.logging.fetch); var textArea = $('#logViewerText'); textArea.on("scroll", $scope.logging.onScroll); - $('#logViewer').modal().one('shown.bs.modal', function () { + $('#logViewer').one('shown.bs.modal', function () { // Scroll to bottom. textArea.scrollTop(textArea[0].scrollHeight); }).one('hidden.bs.modal', function () { @@ -1410,6 +1410,7 @@ angular.module('syncthing.core') $scope.logging.timer = null; $scope.logging.entries = []; }); + showModal('#logViewer'); }, onFacilityChange: function (facility) { var enabled = $scope.logging.facilities[facility].enabled; @@ -1477,13 +1478,14 @@ angular.module('syncthing.core') }, show: function () { $scope.about.refreshPaths(); - $('#about').modal("show"); + showModal('#about'); }, }; $scope.discardChangedSettings = function () { - $("#discard-changes-confirmation").modal("hide"); - $("#settings").off("hide.bs.modal").modal("hide"); + hideModal('#discard-changes-confirmation'); + $('#settings').off('hide.bs.modal') + hideModal('#settings'); }; $scope.showSettings = function () { @@ -1500,9 +1502,9 @@ angular.module('syncthing.core') $scope.tmpGUI = angular.copy($scope.config.gui); $scope.tmpRemoteIgnoredDevices = angular.copy($scope.config.remoteIgnoredDevices); $scope.tmpDevices = angular.copy($scope.config.devices); - $('#settings').modal("show"); - $("#settings a[href='#settings-general']").tab("show"); - $("#settings").on('hide.bs.modal', function (event) { + $('#settings').one('shown.bs.modal', function () { + $("#settings a[href='#settings-general']").tab("show"); + }).on('hide.bs.modal', function (event) { if ($scope.settingsModified()) { event.preventDefault(); $("#discard-changes-confirmation").modal("show"); @@ -1510,12 +1512,17 @@ angular.module('syncthing.core') $("#settings").off("hide.bs.modal"); } }); + showModal('#settings'); }; $scope.saveConfig = function () { - // Only block the UI when there is a significant delay. + // Use "$scope.saveConfig().then" when hiding modals after saving + // changes, or otherwise the background modal will be hidden before + // the #savingChanges modal, causing the right body margin increase + // bug (see https://github.com/syncthing/syncthing/pull/9078). var timeout = setTimeout(function () { - $('#savingChanges').modal('show'); + // Only block the UI when there is a significant delay. + showModal('#savingChanges'); }, 200); var cfg = JSON.stringify($scope.config); var opts = { @@ -1527,7 +1534,7 @@ angular.module('syncthing.core') console.log('saveConfig', $scope.config); refreshConfig(); clearTimeout(timeout); - $('#savingChanges').modal('hide'); + hideModal('#savingChanges'); }).catch($scope.emitHTTPError); }; @@ -1611,22 +1618,27 @@ angular.module('syncthing.core') $scope.saveConfig().then(function () { if (themeChanged) { document.location.reload(true); + } else { + $('#settings').off('hide.bs.modal') + hideModal('#settings'); } }); + } else { + $('#settings').off('hide.bs.modal') + hideModal('#settings'); } - - $("#settings").off("hide.bs.modal").modal("hide"); }; $scope.saveAdvanced = function () { $scope.config = $scope.advancedConfig; - $scope.saveConfig(); - $('#advanced').modal("hide"); + $scope.saveConfig().then(function () { + hideModal('#advanced'); + }); }; $scope.restart = function () { restarting = true; - $('#restarting').modal(); + showModal('#restarting'); $http.post(urlbase + '/system/restart'); $scope.configInSync = true; @@ -1648,21 +1660,21 @@ angular.module('syncthing.core') $scope.upgrade = function () { restarting = true; - $('#upgrade').modal('hide'); - $('#majorUpgrade').modal('hide'); - $('#upgrading').modal(); + hideModal('#upgrade'); + hideModal('#majorUpgrade'); + showModal('#upgrading'); $http.post(urlbase + '/system/upgrade').success(function () { - $('#restarting').modal(); - $('#upgrading').modal('hide'); + hideModal('#upgrading'); + showModal('#restarting'); }).error(function () { - $('#upgrading').modal('hide'); + hideModal('#upgrading'); }); }; $scope.shutdown = function () { restarting = true; $http.post(urlbase + '/system/shutdown').success(function () { - $('#shutdown').modal(); + showModal('#shutdown'); }).error($scope.emitHTTPError); $scope.configInSync = true; }; @@ -1670,7 +1682,7 @@ angular.module('syncthing.core') function editDeviceModal() { $scope.currentDevice._addressesStr = $scope.currentDevice.addresses.join(', '); $scope.deviceEditor.$setPristine(); - $('#editDevice').modal(); + showModal('#editDevice'); } $scope.editDeviceModalTitle = function() { @@ -1794,7 +1806,6 @@ angular.module('syncthing.core') }; $scope.deleteDevice = function () { - $('#editDevice').modal('hide'); if ($scope.currentDevice._editing != "existing") { return; } @@ -1809,11 +1820,12 @@ angular.module('syncthing.core') }); } - $scope.saveConfig(); + $scope.saveConfig().then(function () { + hideModal('#editDevice'); + }); }; $scope.saveDevice = function () { - $('#editDevice').modal('hide'); $scope.currentDevice.addresses = $scope.currentDevice._addressesStr.split(',').map(function (x) { return x.trim(); }); @@ -1825,7 +1837,9 @@ angular.module('syncthing.core') } delete $scope.currentSharing; $scope.currentDevice = {}; - $scope.saveConfig(); + $scope.saveConfig().then(function () { + hideModal('#editDevice'); + }); }; function setDeviceConfig() { @@ -2054,7 +2068,7 @@ angular.module('syncthing.core') }; $scope.globalChanges = function () { - $('#globalChanges').modal(); + showModal('#globalChanges'); }; function editFolderModal(initialTab) { @@ -2066,7 +2080,7 @@ angular.module('syncthing.core') initialTab = "#folder-general"; } $('.nav-tabs a[href="' + initialTab + '"]').tab('show'); - $('#editFolder').modal().one('shown.bs.tab', function (e) { + $('#editFolder').one('shown.bs.tab', function (e) { if (e.target.attributes.href.value === "#folder-ignores") { $('#folder-ignores textarea').focus(); } @@ -2082,6 +2096,7 @@ angular.module('syncthing.core') $scope.ignores = {}; }); }); + showModal('#editFolder'); }; $scope.editFolderModalTitle = function() { @@ -2309,7 +2324,7 @@ angular.module('syncthing.core') // On modal being hidden without clicking save, the defaults will be saved. $scope.ignores.saved = true; saveFolderAddIgnores($scope.currentFolder.id); - hideFolderModal(); + hideModal('#editFolder'); return; } @@ -2362,10 +2377,11 @@ angular.module('syncthing.core') delete folderCfg._guiVersioning; if ($scope.currentFolder._editing == "defaults") { - hideFolderModal(); $scope.config.defaults.ignores.lines = ignoresArray(); $scope.config.defaults.folder = folderCfg; - $scope.saveConfig(); + $scope.saveConfig().then(function () { + hideModal('#editFolder'); + }); return; } @@ -2377,16 +2393,18 @@ angular.module('syncthing.core') $scope.config.folders = folderList($scope.folders); if ($scope.currentFolder._editing == "existing") { - hideFolderModal(); saveFolderIgnoresExisting(); - $scope.saveConfig(); + $scope.saveConfig().then(function () { + hideModal('#editFolder'); + }); return; } // No ignores to be set on the new folder, save directly. if (!$scope.currentFolder._addIgnores) { - hideFolderModal(); - $scope.saveConfig(); + $scope.saveConfig().then(function () { + hideModal('#editFolder'); + }); return; } @@ -2533,7 +2551,6 @@ angular.module('syncthing.core') }; $scope.deleteFolder = function (id) { - hideFolderModal(); if ($scope.currentFolder._editing != "existing") { return; } @@ -2543,13 +2560,11 @@ angular.module('syncthing.core') $scope.config.folders = folderList($scope.folders); recalcLocalStateTotal(); - $scope.saveConfig(); + $scope.saveConfig().then(function () { + hideModal('#editFolder'); + }); }; - function hideFolderModal() { - $('#editFolder').modal('hide'); - } - function resetRestoreVersions() { $scope.restoreVersions = { folder: null, @@ -2615,7 +2630,7 @@ angular.module('syncthing.core') $http.post(urlbase + '/folder/versions?folder=' + encodeURIComponent($scope.restoreVersions.folder), selections).success(function (data) { if (Object.keys(data).length == 0) { - $('#restoreVersions').modal('hide'); + hideModal('#restoreVersions'); } else { $scope.restoreVersions.errors = data; } @@ -2626,12 +2641,13 @@ angular.module('syncthing.core') var closed = false; var modalShown = $q.defer(); - $('#restoreVersions').modal().one('hidden.bs.modal', function () { + $('#restoreVersions').one('hidden.bs.modal', function () { closed = true; resetRestoreVersions(); }).one('shown.bs.modal', function () { modalShown.resolve(); }); + showModal('#restoreVersions'); var dataReceived = $http.get(urlbase + '/folder/versions?folder=' + encodeURIComponent($scope.restoreVersions.folder)) .success(function (data) { @@ -2814,8 +2830,9 @@ angular.module('syncthing.core') $scope.acceptUR = function () { $scope.config.options.urAccepted = $scope.system.urVersionMax; $scope.config.options.urSeen = $scope.system.urVersionMax; - $scope.saveConfig(); - $('#ur').modal('hide'); + $scope.saveConfig().then(function () { + hideModal('#ur'); + }); }; $scope.declineUR = function () { @@ -2823,17 +2840,19 @@ angular.module('syncthing.core') $scope.config.options.urAccepted = -1; } $scope.config.options.urSeen = $scope.system.urVersionMax; - $scope.saveConfig(); - $('#ur').modal('hide'); + $scope.saveConfig().then(function () { + hideModal('#ur'); + }); }; $scope.showNeed = function (folder) { $scope.neededFolder = folder; $scope.refreshNeed(1, 10); - $('#needed').modal().one('hidden.bs.modal', function () { + $('#needed').one('hidden.bs.modal', function () { $scope.needed = undefined; $scope.neededFolder = ''; }); + showModal('#needed'); }; $scope.showRemoteNeed = function (device) { @@ -2847,9 +2866,10 @@ angular.module('syncthing.core') $scope.remoteNeedFolders.push(folder); $scope.refreshRemoteNeed(folder, 1, 10); }); - $('#remoteNeed').modal().one('hidden.bs.modal', function () { + $('#remoteNeed').one('hidden.bs.modal', function () { resetRemoteNeed(); }); + showModal('#remoteNeed'); }; $scope.downloadProgressEnabled = function() { @@ -2862,9 +2882,10 @@ angular.module('syncthing.core') $scope.showFailed = function (folder) { $scope.failed.folder = folder; $scope.failed = $scope.refreshFailed(1, 10); - $('#failed').modal().one('hidden.bs.modal', function () { + $('#failed').one('hidden.bs.modal', function () { $scope.failed = {}; }); + showModal('#failed'); }; $scope.hasFailedFiles = function (folder) { @@ -2878,11 +2899,12 @@ angular.module('syncthing.core') $scope.localChangedFolder = folder; $scope.localChangedType = folderType; $scope.localChanged = $scope.refreshLocalChanged(1, 10); - $('#localChanged').modal().one('hidden.bs.modal', function () { + $('#localChanged').one('hidden.bs.modal', function () { $scope.localChanged = {}; $scope.localChangedFolder = undefined; $scope.localChangedType = undefined; }); + showModal('#localChanged'); }; $scope.hasReceiveOnlyChanged = function (folderCfg) { @@ -2922,7 +2944,7 @@ angular.module('syncthing.core') break; } $scope.revertOverrideParams = params; - $('#revert-override-confirmation').modal('show'); + showModal('#revert-override-confirmation'); }; $scope.advanced = function () { @@ -2935,7 +2957,7 @@ angular.module('syncthing.core') } return $scope.advancedConfig.defaults.ignores.lines.join('\n'); }; - $('#advanced').modal('show'); + showModal('#advanced'); }; $scope.showReportPreview = function () { @@ -3239,7 +3261,7 @@ angular.module('syncthing.core') } $scope.shareDeviceIdParams = params; - $('#share-device-id-dialog').modal('show'); + showModal('#share-device-id-dialog'); }; $scope.shareDeviceId = function () { @@ -3397,6 +3419,69 @@ angular.module('syncthing.core') return n.match !== ""; }); }; + + // The showModal and hideModal functions are a bandaid for a Bootstrap + // bug (see https://github.com/twbs/bootstrap/issues/3902) that causes + // multiple consecutively shown or hidden modals to overlap which leads + // to the right body margin in HTML increasing in size infinitely. These + // custom functions make sure that the previous modal has either been + // fully shown or hidden before showing or hiding a new one. Note that + // modals still need to be manipulated in the order of their appearance, + // i.e. the foreground first, the background later, or the body margin + // addition bug will occur. + + var previousModalState = ''; + var previousModalID = ''; + + function showModal(modalID) { + if (($(modalID).data('bs.modal') || {}).isShown) { + return; + } + showHideModal(modalID, 'show'); + }; + + function hideModal(modalID) { + if (!($(modalID).data('bs.modal') || {}).isShown) { + return; + } + showHideModal(modalID, 'hide'); + }; + + function showHideModal(modalID, modalState) { + var modalAction = ''; + var modalEvent = ''; + + switch (modalState) { + case 'show': + modalAction = showModal; + modalEvent = 'shown.bs.modal'; + break; + case 'hide': + modalAction = hideModal; + modalEvent = 'hidden.bs.modal'; + break; + } + + switch (previousModalState) { + case 'show': + $(previousModalID).one('shown.bs.modal', function () { + modalAction(modalID); + }); + break; + case 'hide': + $(previousModalID).one('hidden.bs.modal', function () { + modalAction(modalID); + }); + break; + default: + previousModalState = modalState; + previousModalID = modalID; + $(modalID).one(modalEvent, function () { + previousModalState = ''; + previousModalID = ''; + }).modal(modalState); + } + }; }) .directive('shareTemplate', function () { return {