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 {