summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authortomasz1986 <twilczynski@naver.com>2023-09-25 21:17:57 +0200
committerGitHub <noreply@github.com>2023-09-25 21:17:57 +0200
commita44b31d173d8ab16065ced935535efc54582cfd6 (patch)
tree982adaa928461dca67d32ad8c5d756b49205f2ef
parent70065e6b139c5d01d15d3ffe4a796bf692a218f2 (diff)
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>
-rwxr-xr-xgui/default/syncthing/core/syncthingController.js221
1 files changed, 153 insertions, 68 deletions
diff --git a/gui/default/syncthing/core/syncthingController.js b/gui/default/syncthing/core/syncthingController.js
index 0a9a102412..efbdbba172 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 {