gui: Fix body padding infinitely increasing due to overlapping modals (ref #9063) (#9078)

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 <twilczynski@naver.com>
This commit is contained in:
tomasz1986 2023-09-25 21:17:57 +02:00 committed by GitHub
parent 70065e6b13
commit a44b31d173
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 153 additions and 68 deletions

View File

@ -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 {