From 09a9fd087686e97155043daa45a72f940e8af4d3 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 26 Feb 2015 17:02:57 -0800 Subject: [PATCH] GUAC-1099: Implement pagination directive (guacPager), borrowing from old 0.9.3 pager code and styles. --- .../src/main/webapp/app/index/styles/ui.css | 61 +--- .../webapp/app/pager/directives/guacPager.js | 303 ++++++++++++++++++ .../src/main/webapp/app/pager/pagerModule.js | 26 ++ .../main/webapp/app/pager/styles/pager.css | 90 ++++++ .../webapp/app/pager/templates/guacPager.html | 46 +++ 5 files changed, 466 insertions(+), 60 deletions(-) create mode 100644 guacamole/src/main/webapp/app/pager/directives/guacPager.js create mode 100644 guacamole/src/main/webapp/app/pager/pagerModule.js create mode 100644 guacamole/src/main/webapp/app/pager/styles/pager.css create mode 100644 guacamole/src/main/webapp/app/pager/templates/guacPager.html diff --git a/guacamole/src/main/webapp/app/index/styles/ui.css b/guacamole/src/main/webapp/app/index/styles/ui.css index bba2c9970..451360667 100644 --- a/guacamole/src/main/webapp/app/index/styles/ui.css +++ b/guacamole/src/main/webapp/app/index/styles/ui.css @@ -346,66 +346,7 @@ div.logout-panel { padding-right: 1em; } -.first-page, -.prev-page, -.set-page, -.next-page, -.last-page { - cursor: pointer; - vertical-align: middle; -} - -.first-page.disabled, -.prev-page.disabled, -.set-page.disabled, -.next-page.disabled, -.last-page.disabled { - cursor: auto; - opacity: 0.25; -} - -.set-page, -.more-pages { - display: inline-block; - padding: 0.25em; - text-align: center; - min-width: 1.25em; -} - -.set-page { - text-decoration: underline; -} - -.set-page.current { - cursor: auto; - text-decoration: none; - font-weight: bold; - background: rgba(0, 0, 0, 0.1); - border: 1px solid rgba(0, 0, 0, 0.1); - -moz-border-radius: 0.2em; - -webkit-border-radius: 0.2em; - -khtml-border-radius: 0.2em; - border-radius: 0.2em; -} - -.icon.first-page { - background-image: url('images/action-icons/guac-first-page.png'); -} - -.icon.prev-page { - background-image: url('images/action-icons/guac-prev-page.png'); -} - -.icon.next-page { - background-image: url('images/action-icons/guac-next-page.png'); -} - -.icon.last-page { - background-image: url('images/action-icons/guac-last-page.png'); -} - -.buttons, -.list-pager-buttons { +.buttons { text-align: center; margin: 1em; } diff --git a/guacamole/src/main/webapp/app/pager/directives/guacPager.js b/guacamole/src/main/webapp/app/pager/directives/guacPager.js new file mode 100644 index 000000000..8074f1362 --- /dev/null +++ b/guacamole/src/main/webapp/app/pager/directives/guacPager.js @@ -0,0 +1,303 @@ +/* + * Copyright (C) 2015 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * A directive which provides pagination controls, along with a paginated + * subset of the elements of some given array. + */ +angular.module('pager').directive('guacPager', [function guacPager() { + + return { + restrict: 'E', + replace: true, + scope: { + + /** + * The property to which a subset of the provided array will be + * assigned. + * + * @type Array + */ + page : '=', + + /** + * The maximum number of items per page. + * + * @type Number + */ + pageSize : '&', + + /** + * The maximum number of page choices to provide, regardless of the + * total number of pages. + * + * @type Number + */ + pageCount : '&', + + /** + * An array objects to paginate. Subsets of this array will be + * exposed as pages. + * + * @type Array + */ + items : '&' + + }, + + templateUrl: 'app/pager/templates/guacPager.html', + controller: ['$scope', function guacPagerController($scope) { + + /** + * The default size of a page, if not provided via the pageSize + * attribute. + * + * @type Number + */ + var DEFAULT_PAGE_SIZE = 10; + + /** + * The default maximum number of page choices to provide, if a + * value is not providede via the pageCount attribute. + * + * @type Number + */ + var DEFAULT_PAGE_COUNT = 11; + + /** + * An array of arrays, where the Nth array contains the contents of + * the Nth page. + * + * @type Array[] + */ + var pages = []; + + /** + * The number of the first selectable page. + * + * @type Number; + */ + $scope.firstPage = 1; + + /** + * The number of the page immediately before the currently-selected + * page. + * + * @type Number; + */ + $scope.previousPage = 1; + + /** + * The number of the currently-selected page. + * + * @type Number; + */ + $scope.currentPage = 1; + + /** + * The number of the page immediately after the currently-selected + * page. + * + * @type Number; + */ + $scope.nextPage = 1; + + /** + * The number of the last selectable page. + * + * @type Number; + */ + $scope.lastPage = 1; + + /** + * An array of relevant page numbers that the user may want to jump + * to directly. + * + * @type Number[] + */ + $scope.pageNumbers = []; + + /** + * Updates the displayed page number choices. + */ + var updatePageNumbers = function updatePageNumbers() { + + // Get page count + var pageCount = $scope.pageCount() || DEFAULT_PAGE_COUNT; + + // Determine start/end of page window + var windowStart = $scope.currentPage - (pageCount - 1) / 2; + var windowEnd = windowStart + pageCount - 1; + + // Shift window as necessary if it extends beyond the first page + if (windowStart < $scope.firstPage) { + windowEnd = Math.min($scope.lastPage, windowEnd - windowStart + $scope.firstPage); + windowStart = $scope.firstPage; + } + + // Shift window as necessary if it extends beyond the last page + else if (windowEnd > $scope.lastPage) { + windowStart = Math.max(1, windowStart - windowEnd + $scope.lastPage); + windowEnd = $scope.lastPage; + } + + // Generate list of relevant page numbers + $scope.pageNumbers = []; + for (var pageNumber = windowStart; pageNumber <= windowEnd; pageNumber++) + $scope.pageNumbers.push(pageNumber); + + }; + + /** + * Iterates through the bound items array, splitting it into pages + * based on the current page size. + */ + var updatePages = function updatePages() { + + // Get current items and page size + var items = $scope.items(); + var pageSize = $scope.pageSize() || DEFAULT_PAGE_SIZE; + + // Clear current pages + pages = []; + + // Only split into pages if items actually exist + if (items) { + + // Split into pages of pageSize items each + for (var i = 0; i < items.length; i += pageSize) + pages.push(items.slice(i, i + pageSize)); + + } + + // Update minimum and maximum values + $scope.firstPage = 1; + $scope.lastPage = pages.length; + + // Select an appropriate page + var adjustedCurrentPage = Math.min($scope.lastPage, Math.max($scope.firstPage, $scope.currentPage)); + $scope.selectPage(adjustedCurrentPage); + + }; + + /** + * Selects the page having the given number, assigning that page to + * the property bound to the page attribute. If no such page + * exists, the property will be set to undefined instead. Valid + * page numbers begin at 1. + * + * @param {Number} page + * The number of the page to select. Valid page numbers begin + * at 1. + */ + $scope.selectPage = function selectPage(page) { + + // Select the chosen page + $scope.currentPage = page; + $scope.page = pages[page-1]; + + // Update next/previous page numbers + $scope.nextPage = Math.min($scope.lastPage, $scope.currentPage + 1); + $scope.previousPage = Math.max($scope.firstPage, $scope.currentPage - 1); + + // Update which page numbers are shown + updatePageNumbers(); + + }; + + /** + * Returns whether the given page number can be legally selected + * via selectPage(), resulting in a different page being shown. + * + * @param {Number} page + * The page number to check. + * + * @returns {Boolean} + * true if the page having the given number can be selected, + * false otherwise. + */ + $scope.canSelectPage = function canSelectPage(page) { + return page !== $scope.currentPage + && page >= $scope.firstPage + && page <= $scope.lastPage; + }; + + /** + * Returns whether the page having the given number is currently + * selected. + * + * @param {Number} page + * The page number to check. + * + * @returns {Boolean} + * true if the page having the given number is currently + * selected, false otherwise. + */ + $scope.isSelected = function isSelected(page) { + return page === $scope.currentPage; + }; + + /** + * Returns whether pages exist before the first page listed in the + * pageNumbers array. + * + * @returns {Boolean} + * true if pages exist before the first page listed in the + * pageNumbers array, false otherwise. + */ + $scope.hasMorePagesBefore = function hasMorePagesBefore() { + var firstPageNumber = $scope.pageNumbers[0] + return firstPageNumber !== $scope.firstPage; + }; + + /** + * Returns whether pages exist after the last page listed in the + * pageNumbers array. + * + * @returns {Boolean} + * true if pages exist after the last page listed in the + * pageNumbers array, false otherwise. + */ + $scope.hasMorePagesAfter = function hasMorePagesAfter() { + var lastPageNumber = $scope.pageNumbers[$scope.pageNumbers.length - 1]; + return lastPageNumber !== $scope.lastPage; + }; + + // Update available pages when available items are changed + $scope.$watchCollection($scope.items, function itemsChanged() { + updatePages(); + }); + + // Update available pages when page size is changed + $scope.$watch($scope.pageSize, function pageSizeChanged() { + updatePages(); + }); + + // Update available page numbers when page count is changed + $scope.$watch($scope.pageCount, function pageCountChanged() { + updatePageNumbers(); + }); + + }] + + }; +}]); diff --git a/guacamole/src/main/webapp/app/pager/pagerModule.js b/guacamole/src/main/webapp/app/pager/pagerModule.js new file mode 100644 index 000000000..026a64286 --- /dev/null +++ b/guacamole/src/main/webapp/app/pager/pagerModule.js @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2015 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * Module for displaying the contents of a list, split into multiple pages. + */ +angular.module('pager', []); diff --git a/guacamole/src/main/webapp/app/pager/styles/pager.css b/guacamole/src/main/webapp/app/pager/styles/pager.css new file mode 100644 index 000000000..bb0b9229c --- /dev/null +++ b/guacamole/src/main/webapp/app/pager/styles/pager.css @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2015 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +.pager { + text-align: center; + margin: 1em; +} + +.pager .page-numbers { + display: inline-block; + margin: 0; + padding: 0; +} + +.pager .first-page, +.pager .prev-page, +.pager .set-page, +.pager .next-page, +.pager .last-page { + cursor: pointer; + vertical-align: middle; +} + +.pager .first-page.disabled, +.pager .prev-page.disabled, +.pager .set-page.disabled, +.pager .next-page.disabled, +.pager .last-page.disabled { + cursor: auto; + opacity: 0.25; +} + +.pager .set-page, +.pager .more-pages { + display: inline-block; + padding: 0.25em; + text-align: center; + min-width: 1.25em; +} + +.pager .set-page { + text-decoration: underline; +} + +.pager .set-page.current { + cursor: auto; + text-decoration: none; + font-weight: bold; + background: rgba(0, 0, 0, 0.1); + border: 1px solid rgba(0, 0, 0, 0.1); + -moz-border-radius: 0.2em; + -webkit-border-radius: 0.2em; + -khtml-border-radius: 0.2em; + border-radius: 0.2em; +} + +.pager .icon.first-page { + background-image: url('images/action-icons/guac-first-page.png'); +} + +.pager .icon.prev-page { + background-image: url('images/action-icons/guac-prev-page.png'); +} + +.pager .icon.next-page { + background-image: url('images/action-icons/guac-next-page.png'); +} + +.pager .icon.last-page { + background-image: url('images/action-icons/guac-last-page.png'); +} diff --git a/guacamole/src/main/webapp/app/pager/templates/guacPager.html b/guacamole/src/main/webapp/app/pager/templates/guacPager.html new file mode 100644 index 000000000..0ebe152b5 --- /dev/null +++ b/guacamole/src/main/webapp/app/pager/templates/guacPager.html @@ -0,0 +1,46 @@ +
+ + + +
+
+ + +
...
+ + +
    +
  • {{pageNumber}}
  • +
+ + +
...
+ + +
+
+ +