Use bootstrap grid instead of column-count:3 for aligning checkboxes (fixes #2029)

Upgrading to bootstrap 3.3.5 meant that checkboxes inside a div with
column-count:3 set would be unclickable in Chrome: in fact, the entire
div appears to sit on top of its contents, making interaction impossible.

This affected both the 'show folder with these devices' and 'these devices
can access this folder' sections of the UI.

I'm not sure what the the underlying cause is, but moving to Bootstrap's
grid system appears work around the issue. Devices/folders have to be
explicitly split out into rows, otherwise the final element appears offset.

To do this grouping by row, a new filter (groupFilter) has been added, which
turns an input of e.g. [1, 2, 3, 4, 5] with a groupSize of 3 into
[[1, 2, 3], [4, 5]]. However altering the collection in this way throws
Angular into an infinite watch loop, terminating in infdig. m59peacemaker's
pmkr.filterStabilize (MIT) was added to work around this issue.

This also has the nice side-effect of wrapping the list of devices/folders
when the screen width decreases.

See also:
 - #2027 (bootstrap update which triggered this issue)
 - #1121 (last time it happened)
This commit is contained in:
Antony Male 2015-07-05 17:08:16 +01:00
parent 57a5d13c47
commit 7023d3ca2b
4 changed files with 97 additions and 10 deletions

View File

@ -601,11 +601,13 @@
<div class="form-group">
<label translate for="folders">Share Folders With Device</label>
<p translate class="help-block">Select the folders to share with this device.</p>
<div class="three-columns">
<div class="checkbox" ng-repeat="folder in folderList()">
<label>
<input type="checkbox" ng-model="currentDevice.selectedFolders[folder.id]"> {{folder.id}}
</label>
<div class="container-fluid">
<div class="row" ng-repeat="folderGroup in folderList() | group:3">
<div class="checkbox col-md-4" ng-repeat="folder in folderGroup">
<label>
<input type="checkbox" ng-model="currentDevice.selectedFolders[folder.id]"> {{folder.id}}
</label>
</div>
</div>
</div>
</div>
@ -766,11 +768,13 @@
<div class="form-group">
<label translate for="devices">Share With Devices</label>
<p translate class="help-block">Select the devices to share this folder with.</p>
<div class="three-columns">
<div class="checkbox" ng-repeat="device in otherDevices()">
<label>
<input type="checkbox" ng-model="currentFolder.selectedDevices[device.deviceID]"> {{deviceName(device)}}
</label>
<div class="container-fluid">
<div class="row" ng-repeat="deviceGroup in otherDevices() | group:3">
<div class="checkbox col-md-4" ng-repeat="device in deviceGroup">
<label>
<input type="checkbox" ng-model="currentFolder.selectedDevices[device.deviceID]"> {{deviceName(device)}}
</label>
</div>
</div>
</div>
</div>
@ -1259,9 +1263,12 @@
<script src="scripts/syncthing/core/filters/basenameFilter.js"></script>
<script src="scripts/syncthing/core/filters/binaryFilter.js"></script>
<script src="scripts/syncthing/core/filters/durationFilter.js"></script>
<script src="scripts/syncthing/core/filters/groupFilter.js"></script>
<script src="scripts/syncthing/core/filters/naturalFilter.js"></script>
<script src="scripts/syncthing/core/filters/lastErrorComponentFilter.js"></script>
<script src="scripts/syncthing/core/services/filterStabilize.js"></script>
<script src="scripts/syncthing/core/services/localeService.js"></script>
<script src="scripts/syncthing/core/services/memoize.js"></script>
<script src="assets/lang/valid-langs.js"></script>
<script src="assets/lang/prettyprint.js"></script>

View File

@ -0,0 +1,25 @@
/**
* Groups input in chunks of the specified size
*
* E.g. [1, 2, 3, 4, 5] with groupSize = 3 => [[1, 2, 3], [4, 5]]
* Uses pmkr.memoize to avoid infdig, see 'Johnny Hauser's "Filter Stablize" Solution'
* here: http://sobrepere.com/blog/2014/10/14/creating-groupby-filter-angularjs/
*/
angular.module('syncthing.core')
.filter('group', [
'pmkr.filterStabilize',
function (stabilize) {
return stabilize(function(items, groupSize) {
var groups = [];
var inner;
for (var i = 0; i < items.length; i++) {
if (i % groupSize === 0) {
inner = [];
groups.push(inner);
}
inner.push(items[i]);
}
return groups;
});
}
]);

View File

@ -0,0 +1,27 @@
/**
* m59peacemaker's filterStabilize
*
* See https://github.com/m59peacemaker/angular-pmkr-components/tree/master/src/services/filterStabilize
* Released under the MIT license
*/
angular.module('syncthing.core')
.factory('pmkr.filterStabilize', [
'pmkr.memoize',
function(memoize) {
function service(fn) {
function filter() {
var args = [].slice.call(arguments);
// always pass a copy of the args so that the original input can't be modified
args = angular.copy(args);
// return the `fn` return value or input reference (makes `fn` return optional)
var filtered = fn.apply(this, args) || args[0];
return filtered;
}
var memoized = memoize(filter);
return memoized;
}
return service;
}
]);

View File

@ -0,0 +1,28 @@
/**
* m59peacemaker's memoize
*
* See https://github.com/m59peacemaker/angular-pmkr-components/tree/master/src/services/memoize
* Released under the MIT license
*/
angular.module('syncthing.core')
.factory('pmkr.memoize', [
function() {
function service() {
return memoizeFactory.apply(this, arguments);
}
function memoizeFactory(fn) {
var cache = {};
function memoized() {
var args = [].slice.call(arguments);
var key = JSON.stringify(args);
if (cache.hasOwnProperty(key)) {
return cache[key];
}
cache[key] = fn.apply(this, arguments);
return cache[key];
}
return memoized;
}
return service;
}
]);