GUACAMOLE-220: Implement generic editor directive for manipulating sets of identifiers.

This commit is contained in:
Michael Jumper
2018-07-25 02:34:27 -07:00
parent 1cf16d1dc6
commit 229b0dee48
5 changed files with 395 additions and 0 deletions

View File

@@ -0,0 +1,267 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive for manipulating a set of objects sharing some common relation
* and represented by an array of their identifiers. The specific objects
* added or removed are tracked within a separate pair of arrays of
* identifiers.
*/
angular.module('manage').directive('identifierSetEditor', ['$injector',
function identifierSetEditor($injector) {
var directive = {
// Element only
restrict: 'E',
replace: true,
scope: {
/**
* The translation key of the text which should be displayed within
* the main header of the identifier set editor.
*
* @type String
*/
header : '@',
/**
* The translation key of the text which should be displayed if no
* identifiers are currently present within the set.
*
* @type String
*/
emptyPlaceholder : '@',
/**
* The translation key of the text which should be displayed if no
* identifiers are available to be added within the set.
*
* @type String
*/
unavailablePlaceholder : '@',
/**
* All identifiers which are available to be added to or removed
* from the identifier set being edited.
*
* @type String[]
*/
identifiersAvailable : '=',
/**
* The current state of the identifier set being manipulated. This
* array will be modified as changes are made through this
* identifier set editor.
*
* @type String[]
*/
identifiers : '=',
/**
* The set of identifiers that have been added, relative to the
* initial state of the identifier set being manipulated.
*
* @type String[]
*/
identifiersAdded : '=',
/**
* The set of identifiers that have been removed, relative to the
* initial state of the identifier set being manipulated.
*
* @type String[]
*/
identifiersRemoved : '='
},
templateUrl: 'app/manage/templates/identifierSetEditor.html'
};
directive.controller = ['$scope', function identifierSetEditorController($scope) {
/**
* Whether the full list of available identifiers should be displayed.
* Initially, only an abbreviated list of identifiers currently present
* is shown.
*
* @type Boolean
*/
$scope.expanded = false;
/**
* Map of identifiers to boolean flags indicating whether that
* identifier is currently present (true) or absent (false). If an
* identifier is absent, it may also be absent from this map.
*
* @type Object.<String, Boolean>
*/
$scope.identifierFlags = {};
/**
* Adds the given identifier to the given sorted array of identifiers,
* preserving the sorted order of the array. If the identifier is
* already present, no change is made to the array. The given array
* must already be sorted in ascending order.
*
* @param {String[]} arr
* The sorted array of identifiers to add the given identifier to.
*
* @param {String} identifier
* The identifier to add to the given array.
*/
var addIdentifier = function addIdentifier(arr, identifier) {
// Determine location that the identifier should be added to
// maintain sorted order
var index = _.sortedIndex(arr, identifier);
// Do not add if already present
if (arr[index] === identifier)
return;
// Insert identifier at determined location
arr.splice(index, 0, identifier);
};
/**
* Removes the given identifier from the given sorted array of
* identifiers, preserving the sorted order of the array. If the
* identifier is already absent, no change is made to the array. The
* given array must already be sorted in ascending order.
*
* @param {String[]} arr
* The sorted array of identifiers to remove the given identifier
* from.
*
* @param {String} identifier
* The identifier to remove from the given array.
*
* @returns {Boolean}
* true if the identifier was present in the given array and has
* been removed, false otherwise.
*/
var removeIdentifier = function removeIdentifier(arr, identifier) {
// Search for identifier in sorted array
var index = _.sortedIndexOf(arr, identifier);
// Nothing to do if already absent
if (index === -1)
return false;
// Remove identifier
arr.splice(index, 1);
return true;
};
// Keep identifierFlags up to date when identifiers array is replaced
// or initially assigned
$scope.$watch('identifiers', function identifiersChanged(identifiers) {
// Maintain identifiers in sorted order so additions and removals
// can be made more efficiently
if (identifiers)
identifiers.sort();
// Convert array of identifiers into set of boolean
// presence/absence flags
$scope.identifierFlags = {};
angular.forEach(identifiers, function storeIdentifierFlag(identifier) {
$scope.identifierFlags[identifier] = true;
});
});
/**
* Notifies the controller that a change has been made to the flag
* denoting presence/absence of a particular identifier within the
* <code>identifierFlags</code> map. The <code>identifiers</code>,
* <code>identifiersAdded</code>, and <code>identifiersRemoved</code>
* arrays are updated accordingly.
*
* @param {String} identifier
* The identifier which has been added or removed through modifying
* its boolean flag within <code>identifierFlags</code>.
*/
$scope.identifierChanged = function identifierChanged(identifier) {
// Determine status of modified identifier
var present = !!$scope.identifierFlags[identifier];
// Add/remove identifier from added/removed sets depending on
// change in flag state
if (present) {
addIdentifier($scope.identifiers, identifier);
if (!removeIdentifier($scope.identifiersRemoved, identifier))
addIdentifier($scope.identifiersAdded, identifier);
}
else {
removeIdentifier($scope.identifiers, identifier);
if (!removeIdentifier($scope.identifiersAdded, identifier))
addIdentifier($scope.identifiersRemoved, identifier);
}
};
/**
* Removes the given identifier, updating <code>identifierFlags</code>,
* <code>identifiers</code>, <code>identifiersAdded</code>, and
* <code>identifiersRemoved</code> accordingly.
*
* @param {String} identifier
* The identifier to remove.
*/
$scope.removeIdentifier = function removeIdentifier(identifier) {
$scope.identifierFlags[identifier] = false;
$scope.identifierChanged(identifier);
};
/**
* Shows the full list of available identifiers. If the full list is
* already shown, this function has no effect.
*/
$scope.expand = function expand() {
$scope.expanded = true;
};
/**
* Hides the full list of available identifiers. If the full list is
* already hidden, this function has no effect.
*/
$scope.collapse = function collapse() {
$scope.expanded = false;
};
}];
return directive;
}]);

View File

@@ -0,0 +1,82 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
.related-objects .abbreviated-related-objects {
display: table;
margin: 1em 0;
}
.related-objects .abbreviated-related-objects ul {
display: table-cell;
vertical-align: top;
}
.related-objects .abbreviated-related-objects ul,
.related-objects .all-related-objects ul {
padding: 0;
list-style: none;
}
.related-objects .abbreviated-related-objects ul li {
display: inline-block;
margin: 0.25em;
padding: 0.25em;
border: 1px solid silver;
background: #F5F5F5;
-moz-border-radius: 0.25em;
-webkit-border-radius: 0.25em;
-khtml-border-radius: 0.25em;
border-radius: 0.25em;
}
.related-objects .abbreviated-related-objects ul li img.remove {
max-height: 0.75em;
max-width: 0.75em;
margin: 0 0.25em;
}
.related-objects .abbreviated-related-objects ul li .identifier {
margin: 0 0.25em;
}
.related-objects .abbreviated-related-objects img.expand,
.related-objects .abbreviated-related-objects img.collapse {
display: table-cell;
max-height: 1.5em;
max-width: 1.5em;
margin: 0.375em 0;
}
.related-objects .all-related-objects {
border-top: 1px solid silver;
}
.related-objects .abbreviated-related-objects p.no-related-objects,
.related-objects .all-related-objects p.no-objects-available {
font-style: italic;
opacity: 0.5;
}
.related-objects .abbreviated-related-objects p.no-related-objects {
display: table-cell;
vertical-align: middle;
}

View File

@@ -0,0 +1,46 @@
<div class="related-objects">
<div class="header">
<h2>{{ header | translate }}</h2>
<div class="filter">
<input class="search-string" type="text"
placeholder="{{ 'SETTINGS_USERS.FIELD_PLACEHOLDER_FILTER' | translate }}"
ng-model="filterString"/>
</div>
</div>
<div class="section">
<!-- Abbreviated list of only the currently selected objects -->
<div class="abbreviated-related-objects">
<img src="images/arrows/right.png" alt="Expand" class="expand" ng-hide="expanded" ng-click="expand()"/>
<img src="images/arrows/down.png" alt="Collapse" class="collapse" ng-show="expanded" ng-click="collapse()"/>
<p ng-hide="identifiers.length" class="no-related-objects">{{ emptyPlaceholder | translate }}</p>
<ul>
<li ng-repeat="identifier in identifiers | filter: filterString">
<label><img src="images/x-red.png" alt="Remove" class="remove"
ng-click="removeIdentifier(identifier)"/><span class="identifier">{{ identifier }}</span>
</label>
</li>
</ul>
</div>
<!-- Exhaustive, paginated list of all objects -->
<div class="all-related-objects" ng-show="expanded">
<p ng-hide="identifiersAvailablePage.length" class="no-objects-available">{{ unavailablePlaceholder | translate }}</p>
<ul>
<li ng-repeat="identifier in identifiersAvailablePage">
<label><input type="checkbox"
ng-model="identifierFlags[identifier]"
ng-change="identifierChanged(identifier)"/>
<span class="identifier">{{ identifier }}</span>
</label>
</li>
</ul>
<!-- Pager controls for user list -->
<guac-pager page="identifiersAvailablePage" page-size="25"
items="identifiersAvailable | orderBy | filter: filterString"></guac-pager>
</div>
</div>
</div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 583 B