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


Project: http://git-wip-us.apache.org/repos/asf/guacamole-client/repo
Commit: http://git-wip-us.apache.org/repos/asf/guacamole-client/commit/229b0dee
Tree: http://git-wip-us.apache.org/repos/asf/guacamole-client/tree/229b0dee
Diff: http://git-wip-us.apache.org/repos/asf/guacamole-client/diff/229b0dee

Branch: refs/heads/master
Commit: 229b0dee4882352e7583c4f5872bee92158da712
Parents: 1cf16d1
Author: Michael Jumper <mjum...@apache.org>
Authored: Wed Jul 25 02:34:27 2018 -0700
Committer: Michael Jumper <mjum...@apache.org>
Committed: Wed Aug 8 09:00:06 2018 -0700

----------------------------------------------------------------------
 .../manage/directives/identifierSetEditor.js    | 267 +++++++++++++++++++
 .../app/manage/styles/related-objects.css       |  82 ++++++
 .../manage/templates/identifierSetEditor.html   |  46 ++++
 .../src/main/webapp/images/arrows/right.png     | Bin 0 -> 264 bytes
 guacamole/src/main/webapp/images/x-red.png      | Bin 0 -> 583 bytes
 5 files changed, 395 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/229b0dee/guacamole/src/main/webapp/app/manage/directives/identifierSetEditor.js
----------------------------------------------------------------------
diff --git 
a/guacamole/src/main/webapp/app/manage/directives/identifierSetEditor.js 
b/guacamole/src/main/webapp/app/manage/directives/identifierSetEditor.js
new file mode 100644
index 0000000..82f1109
--- /dev/null
+++ b/guacamole/src/main/webapp/app/manage/directives/identifierSetEditor.js
@@ -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;
+
+}]);

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/229b0dee/guacamole/src/main/webapp/app/manage/styles/related-objects.css
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/manage/styles/related-objects.css 
b/guacamole/src/main/webapp/app/manage/styles/related-objects.css
new file mode 100644
index 0000000..ddc85b1
--- /dev/null
+++ b/guacamole/src/main/webapp/app/manage/styles/related-objects.css
@@ -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;
+}

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/229b0dee/guacamole/src/main/webapp/app/manage/templates/identifierSetEditor.html
----------------------------------------------------------------------
diff --git 
a/guacamole/src/main/webapp/app/manage/templates/identifierSetEditor.html 
b/guacamole/src/main/webapp/app/manage/templates/identifierSetEditor.html
new file mode 100644
index 0000000..838decf
--- /dev/null
+++ b/guacamole/src/main/webapp/app/manage/templates/identifierSetEditor.html
@@ -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>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/229b0dee/guacamole/src/main/webapp/images/arrows/right.png
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/images/arrows/right.png 
b/guacamole/src/main/webapp/images/arrows/right.png
new file mode 100644
index 0000000..1b3483e
Binary files /dev/null and b/guacamole/src/main/webapp/images/arrows/right.png 
differ

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/229b0dee/guacamole/src/main/webapp/images/x-red.png
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/images/x-red.png 
b/guacamole/src/main/webapp/images/x-red.png
new file mode 100644
index 0000000..e5497f3
Binary files /dev/null and b/guacamole/src/main/webapp/images/x-red.png differ

Reply via email to