From 308a22fd2fb755506e9002446807d22422a1d53a Mon Sep 17 00:00:00 2001 From: Matthias Jentsch Date: Thu, 10 Oct 2013 15:49:12 +0200 Subject: [PATCH] Add support for multi-selection Add classes to handle multi-row selection using the CTRL-Key and to create the link for the selected query. refs #3788 --- public/js/icinga/components/mainDetailGrid.js | 54 ++++- public/js/icinga/selection/multiSelection.js | 224 ++++++++++++++++++ public/js/icinga/selection/selectable.js | 83 +++++++ 3 files changed, 353 insertions(+), 8 deletions(-) create mode 100644 public/js/icinga/selection/multiSelection.js create mode 100644 public/js/icinga/selection/selectable.js diff --git a/public/js/icinga/components/mainDetailGrid.js b/public/js/icinga/components/mainDetailGrid.js index da82324135..4886207f71 100644 --- a/public/js/icinga/components/mainDetailGrid.js +++ b/public/js/icinga/components/mainDetailGrid.js @@ -25,8 +25,17 @@ * @author Icinga Development Team */ // {{{ICINGA_LICENSE_HEADER}}} -define(['components/app/container', 'jquery', 'logging', 'URIjs/URI', 'URIjs/URITemplate'], -function(Container, $, logger, URI) { +define( + [ + 'components/app/container', + 'jquery', + 'logging', + 'icinga/selection/selectable', + 'icinga/selection/multiSelection', + 'URIjs/URI', + 'URIjs/URITemplate' + ], +function(Container, $, logger, Selectable, TableMultiSelection, URI) { "use strict"; /** @@ -65,6 +74,13 @@ function(Container, $, logger, URI) { */ var controlForms; + /** + * Handles multi-selection + * + * @type {TableMultiSelection} + */ + var selection; + /** * Detect and select control forms for this table and return them * @@ -131,7 +147,7 @@ function(Container, $, logger, URI) { if (a.length) { // test if the URL is on the current server, if not open it directly - if (true || Container.isExternalLink(a.attr('href'))) { + if (Container.isExternalLink(a.attr('href'))) { return true; } } else if ($.inArray('input', nodeNames) > -1 || $.inArray('button', nodeNames) > -1) { @@ -141,17 +157,35 @@ function(Container, $, logger, URI) { } } - Container.getDetailContainer().replaceDomFromUrl($('a', this).attr('href')); - if (!ev.ctrlKey && !ev.metaKey) { - $('tr', $(this).parent()).removeClass('active'); + var selectable = new Selectable(this); + if (ev.ctrlKey || ev.metaKey) { + selection.toggle(selectable); + } else if (ev.shiftKey) { + // select range ? + selection.add(selectable); + } else { + selection.clear(); + selection.add(selectable); } - $(this).addClass('active'); + // TODO: Detail target + var url = URI($('a', this).attr('href')); + var segments = url.segment(); + if (selection.size() === 0) { + // don't open anything + url.search('?'); + } else if (selection.size() > 1 && segments.length > 3) { + // open detail view for multiple objects + segments[2] = 'multi'; + url.pathname(segments.join('/')); + url.search('?'); + url.setSearch(selection.toQuery()); + } + Container.getDetailContainer().replaceDomFromUrl(url); return false; }); }; - /** * Register submit handler for the form controls (sorting, filtering, etc). Reloading happens in the * current container @@ -214,6 +248,10 @@ function(Container, $, logger, URI) { this.container.removeDefaultLoadIndicator(); controlForms = determineControlForms(); contentNode = determineContentTable(); + selection = new TableMultiSelection( + contentNode, + Container.getDetailContainer().getContainerHref() + ); this.registerControls(); this.registerTableLinks(); this.registerHistoryChanges(); diff --git a/public/js/icinga/selection/multiSelection.js b/public/js/icinga/selection/multiSelection.js new file mode 100644 index 0000000000..b490b5f598 --- /dev/null +++ b/public/js/icinga/selection/multiSelection.js @@ -0,0 +1,224 @@ +// {{{ICINGA_LICENSE_HEADER}}} +/** + * This file is part of Icinga 2 Web. + * + * Icinga 2 Web - Head for multiple monitoring backends. + * Copyright (C) 2013 Icinga Development Team + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * @copyright 2013 Icinga Development Team + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} +define( + ['jquery', 'URIjs/URI', 'icinga/selection/selectable'], +function($, URI, Selectable) { + "use strict"; + + /** + * Handle the multi-selection of table rows and generate the query string + * that can be used to open the selected items. + * + * NOTE: After each site reload, the state (the current selection) of this object will be + * restored automatically. The selectable items are determined by finding all TR elements + * in the targeted table. The active selection is determined by checking the query elements + * of the given url. + * + * @param {HtmlElement} The table that contains the selectable rows. + * + * @param {Object} The query that contains the selected rows. + */ + return function MultiSelection(table, detailUrl) { + var self = this; + /** + * Contains all selected selectables + * + * @type {Object} + */ + var selection = {}; + + /** + * If the selectable was already added, remove it, otherwise add it. + * + * @param {Selectable} The selectable to use. + */ + this.toggle = function(selectable) { + if (selection[selectable.getId()]) { + self.remove(selectable); + } else { + self.add(selectable); + } + }; + + /** + * Add the selectable to the current selection. + * + * @param {Selectable} The selectable to use. + */ + this.add = function(selectable) { + selectable.setActive(true); + selection[selectable.getId()] = selectable; + }; + + /** + * Remove the selectable from the current selection. + * + * @param {Selectable} The selectable to use. + */ + this.remove = function(selectable) { + selectable.setActive(false); + delete selection[selectable.getId()]; + }; + + /** + * Clear the current selection + */ + this.clear = function() { + $.each(selection, function(index, selectable){ + selectable.setActive(false); + }); + selection = {}; + }; + + /** + * Convert the current selection to its query-representation. + * + * @returns {String} The query + */ + this.toQuery = function() { + var query = {}; + var i = 0; + $.each(selection, function(id, selectable) { + $.each(selectable.getQuery(), function(key, value) { + query[key + '[' + i + ']'] = value; + }); + i++; + }); + return query; + }; + + this.size = function() { + var size = 0; + $.each(selection, function(){ size++; }); + return size; + }; + + /** + * Fetch the selections from a query containing multiple selections + */ + var selectionFromMultiQuery = function(query) { + var i = 0; + var selections = []; + alert('query ' + JSON.stringify(query)); + $.each(query, function(key, value) { + // Fetch the index from the key + var id = key.match(/\[([0-9]+)\]/); + alert(id); + if (id) { + alert('extracted id ' + id[1]); + // Remove the index from the key + key = key.replace(/\[[0-9]+\]/,''); + key = encodeURIComponent(key); + value = encodeURIComponent(value); + // Add it to the array representing this index + if (id[1] !== i) { + // begin a new index + selections[i] = [ key + '=' + value ]; + i = id[1]; + } else { + selections[i].push(key + '=' + value); + } + } + }); + return selections; + }; + + /** + * Fetch the selections from a default query. + */ + var selectionFromQuery = function(query) { + var selection = []; + $.each(query, function(key, value){ + key = encodeURIComponent(key); + value = encodeURIComponent(value); + selection.push(key + '=' + value); + }); + return [ selection ]; + }; + + /** + * Restore the selected ids from the given URL. + * + * @param {URI} The used URL + * + * @returns {Array} The selected ids + */ + var restoreSelectionStateUrl = function(url) { + if (!url) { + return []; + } + if (!url.query) { + url = new URI(url); + } + var segments = url.segment(); + var parts; + if (segments.length > 2 && segments[1] === 'Multi') { + alert('from multiselection'); + parts = selectionFromMultiQuery(url.query(true)); + } else { + parts = selectionFromQuery(url.query(true)); + } + return $.map(parts, function(part) { + part.sort(function(a, b){ + a = a.toUpperCase(); + b = b.toUpperCase(); + return (a < b ? -1 : (a > b) ? 1 : 0); + }); + return part.join('&'); + }); + }; + + /** + * Create the selectables from the given table-Html + */ + var createSelectables = function(table) { + var selectables = {}; + $(table).find('tr').each(function(i, row) { + var selectable = new Selectable(row); + selectables[selectable.getId()] = selectable; + }); + return selectables; + }; + + /** + * Restore the selectables from the given table and the given url + */ + var restoreSelection = function() { + var selectables = createSelectables(table); + var selected = restoreSelectionStateUrl(detailUrl); + var selection = {}; + $.each(selected, function(i, selectionId) { + if (selectables[selectionId]) { + selection[selectionId] = selectables[selectionId]; + } + }); + return selection; + }; + + selection = restoreSelection(); + }; +}); diff --git a/public/js/icinga/selection/selectable.js b/public/js/icinga/selection/selectable.js new file mode 100644 index 0000000000..098e59d324 --- /dev/null +++ b/public/js/icinga/selection/selectable.js @@ -0,0 +1,83 @@ +// {{{ICINGA_LICENSE_HEADER}}} +/** + * This file is part of Icinga 2 Web. + * + * Icinga 2 Web - Head for multiple monitoring backends. + * Copyright (C) 2013 Icinga Development Team + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * @copyright 2013 Icinga Development Team + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} +define(['jquery', 'URIjs/URI'], function($, URI) { + "use strict"; + + /** + * Wrapper around a selectable table row. Searches the first href and to identify the associated + * query and use this query as an Identifier. + * + * @param {HtmlElement} The table row. + */ + return function Selectable(tableRow) { + + /** + * The href that is called when this row clicked. + * + * @type {*} + */ + var href = URI($(tableRow).find('a').first().attr('href')); + + /* + * Sort queries alphabetically to ensure non-ambiguous ids. + */ + var query = href.query(); + var parts = query.split('&'); + parts.sort(function(a, b){ + a = a.toUpperCase(); + b = b.toUpperCase(); + return (a < b ? -1 : (a > b) ? 1 : 0); + }); + href.query(parts.join('&')); + + /** + * Return an ID for this selectable. + * + * @returns {String} The id. + */ + this.getId = function () { + return href.query(); + }; + + /** + * Return the query object associated with this selectable. + * + * @returns {String} The id. + */ + this.getQuery = function() { + return href.query(true); + }; + + this.setActive = function(value) { + if (value) { + $(tableRow).addClass('active'); + } else { + $(tableRow).removeClass('active'); + } + }; + }; +}); \ No newline at end of file