Skip to content

Commit 27c44e4

Browse files
authored
Add multi-selection using shift key in the table list views (#117)
Signed-off-by: tdruez <tdruez@nexb.com>
1 parent f7d990a commit 27c44e4

7 files changed

Lines changed: 132 additions & 75 deletions

File tree

component_catalog/templates/component_catalog/base_component_package_list.html

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
{% load i18n %}
33

44
{% block top-right-buttons %}
5-
<button class="btn btn-outline-dark disabled" id="download-aboutcode-files" href="{% if opts.model_name == 'package' %}{% url 'component_catalog:package_multi_about_files' %}{% elif opts.model_name == 'component' %}{% url 'component_catalog:component_multi_about_files' %}{% endif %}" data-bs-toggle="tooltip">
6-
<i class="fas fa-download"></i> AboutCode
7-
</button>
5+
<span class="d-inline-block" tabindex="0" data-bs-toggle="tooltip">
6+
<button class="btn btn-outline-dark disabled" id="download-aboutcode-files" href="{% if opts.model_name == 'package' %}{% url 'component_catalog:package_multi_about_files' %}{% elif opts.model_name == 'component' %}{% url 'component_catalog:component_multi_about_files' %}{% endif %}">
7+
<i class="fas fa-download"></i> AboutCode
8+
</button>
9+
</span>
810
{% if form or add_to_component_form %}
911
<div class="btn-group">
1012
<div class="dropdown" data-bs-toggle="tooltip" title="{% trans 'Select objects first' %}">
@@ -47,16 +49,17 @@
4749
<script>
4850
$(document).ready(function () {
4951
let download_aboutcode_btn = $('#download-aboutcode-files');
52+
let download_aboutcode_wrapper = download_aboutcode_btn.parent();
5053

5154
let handle_button_display = function() {
52-
let count = $('main input[type="checkbox"]:checked').length;
55+
let count = $('table input[type="checkbox"]:checked').length;
5356
if (count >= 1) {
5457
download_aboutcode_btn.removeClass('disabled');
55-
download_aboutcode_btn.attr('data-bs-title', 'Download AboutCode files');
58+
download_aboutcode_wrapper.attr('data-bs-original-title', 'Download AboutCode files');
5659
}
5760
else {
5861
download_aboutcode_btn.addClass('disabled');
59-
download_aboutcode_btn.attr('data-bs-title', 'Select objects first');
62+
download_aboutcode_wrapper.attr('data-bs-original-title', 'Select objects first');
6063
}
6164
};
6265

component_catalog/templates/component_catalog/includes/add_to.js.html

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
<script>
22
$(document).ready(function () {
33
let add_to_btn = $('#add-to-btn');
4+
let add_to_btn_wrapper = add_to_btn.parent();
45

56
let handle_button_display = function () {
67
let count = $('main input[type="checkbox"]:checked').length;
78
if (count >= 1) {
89
add_to_btn.removeClass('disabled');
9-
add_to_btn.parent().attr('data-bs-title', '');
10+
add_to_btn_wrapper.attr('data-bs-original-title', '');
1011
}
1112
else {
1213
add_to_btn.addClass('disabled');
13-
add_to_btn.parent().attr('data-bs-title', 'Select objects first');
14+
add_to_btn_wrapper.attr('data-bs-original-title', 'Select objects first');
1415
}
1516
};
1617

dejacode/static/css/dejacode_bootstrap.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ table.products-table .column-owner {
341341
min-width: 75px;
342342
}
343343
table.products-table .column-productinventoryitem_count {
344-
max-width:80px;
344+
width: 100px;
345345
}
346346

347347
/* -- Package List -- */

dejacode/static/js/dejacode_main.js

Lines changed: 90 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,92 @@
88
#
99
*/
1010

11+
function setupTooltips() {
12+
// Enables all tooltips
13+
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
14+
const tooltips = Array.from(tooltipTriggerList).map(element => {
15+
return new bootstrap.Tooltip(element, {
16+
container: 'body'
17+
});
18+
});
19+
}
20+
21+
function setupPopovers() {
22+
// Enables all popovers
23+
const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]');
24+
const popovers = Array.from(popoverTriggerList).map(element => {
25+
return new bootstrap.Popover(element, {
26+
container: 'body',
27+
html: true
28+
});
29+
});
30+
}
31+
32+
function setupSelectionCheckboxes() {
33+
const selectAllCheckbox = document.getElementById("checkbox-select-all");
34+
const rowCheckboxes = document.querySelectorAll("table#object-list-table tbody input[type='checkbox']");
35+
let lastChecked; // Store the last checked checkbox
36+
37+
if (!rowCheckboxes) return;
38+
39+
// Select-all checkboxes
40+
if (selectAllCheckbox) {
41+
selectAllCheckbox.addEventListener("click", function() {
42+
rowCheckboxes.forEach(function(checkbox) {
43+
checkbox.checked = selectAllCheckbox.checked;
44+
});
45+
});
46+
}
47+
48+
// Add a click event listener to each row checkbox to handle individual selections
49+
rowCheckboxes.forEach((checkbox) => {
50+
checkbox.addEventListener("click", function (event) {
51+
if (event.shiftKey && lastChecked) {
52+
// Determine the index of the clicked checkbox
53+
const currentCheckboxIndex = Array.from(rowCheckboxes).indexOf(checkbox);
54+
const lastCheckedIndex = Array.from(rowCheckboxes).indexOf(lastChecked);
55+
56+
// Determine the range of checkboxes to check/uncheck
57+
const startIndex = Math.min(currentCheckboxIndex, lastCheckedIndex);
58+
const endIndex = Math.max(currentCheckboxIndex, lastCheckedIndex);
59+
60+
// Toggle the checkboxes within the range
61+
for (let i = startIndex; i <= endIndex; i++) {
62+
rowCheckboxes[i].checked = checkbox.checked;
63+
}
64+
}
65+
66+
// Update the last checked checkbox
67+
lastChecked = checkbox;
68+
69+
// Check if all row checkboxes are checked and update the "Select All" checkbox accordingly
70+
if (selectAllCheckbox) {
71+
selectAllCheckbox.checked = Array.from(rowCheckboxes).every((cb) => cb.checked);
72+
}
73+
74+
});
75+
});
76+
}
77+
78+
function setupBackToTop() {
79+
// Get the back-to-top button element
80+
const backToTopButton = document.getElementById('back-to-top');
81+
82+
// Add a scroll event listener
83+
window.addEventListener('scroll', function () {
84+
if (window.scrollY >= 250) { // Page is scrolled more than 250px
85+
backToTopButton.style.display = 'block';
86+
} else {
87+
backToTopButton.style.display = 'none';
88+
}
89+
});
90+
91+
// Add a click event listener to scroll back to the top
92+
backToTopButton.addEventListener('click', function () {
93+
window.scrollTo(0, 0);
94+
});
95+
}
96+
1197
document.addEventListener('DOMContentLoaded', () => {
1298
NEXB = {};
1399
NEXB.client_data = JSON.parse(document.getElementById("client_data").textContent);
@@ -37,23 +123,6 @@ document.addEventListener('DOMContentLoaded', () => {
37123
document.body.appendChild(overlay);
38124
}
39125

40-
// Enables all tooltips
41-
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
42-
const tooltips = Array.from(tooltipTriggerList).map(element => {
43-
return new bootstrap.Tooltip(element, {
44-
container: 'body'
45-
});
46-
});
47-
48-
// Enables all popovers
49-
const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]');
50-
const popovers = Array.from(popoverTriggerList).map(element => {
51-
return new bootstrap.Popover(element, {
52-
container: 'body',
53-
html: true
54-
});
55-
});
56-
57126
// Search selection in the header
58127
$('#search-selector-list a').click(function(event) {
59128
event.preventDefault();
@@ -62,21 +131,9 @@ document.addEventListener('DOMContentLoaded', () => {
62131
$('#search-input').focus();
63132
});
64133

65-
// Get the back-to-top button element
66-
const backToTopButton = document.getElementById('back-to-top');
67-
68-
// Add a scroll event listener
69-
window.addEventListener('scroll', function () {
70-
if (window.scrollY >= 250) { // Page is scrolled more than 250px
71-
backToTopButton.style.display = 'block';
72-
} else {
73-
backToTopButton.style.display = 'none';
74-
}
75-
});
76-
77-
// Add a click event listener to scroll back to the top
78-
backToTopButton.addEventListener('click', function () {
79-
window.scrollTo(0, 0);
80-
});
134+
setupTooltips();
135+
setupPopovers();
136+
setupSelectionCheckboxes();
137+
setupBackToTop();
81138

82139
});

dje/templates/object_list_base.html

Lines changed: 22 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -96,34 +96,28 @@
9696
$('.h-link').tooltip({placement: 'bottom', title: 'Hierarchy view', container: 'body'});
9797
$('.r-link').tooltip({placement: 'bottom', title: 'Request view', container: 'body'});
9898

99-
/* Select all checkboxes */
100-
let checkbox_select_all = $("thead th #checkbox-select-all");
101-
checkbox_select_all.click(function() {
102-
let checkboxes = $("table#object-list-table tbody td input[type='checkbox']");
103-
checkboxes.prop("checked", $(this).prop("checked"));
104-
// Fire the `change` event on the first item to trigger the `handle_button_display`
105-
// There's no need to trigger that event for each checkbox in this "select all" action
106-
checkboxes.first().change();
107-
});
108-
109-
// Left and Right keys navigation
99+
/* Left and Right keys navigation */
110100
{% if is_paginated %}
111-
$(document).keydown(function(e) {
112-
// Do not trigger the navigation if an <input> or <textarea> currently has the focus
113-
var any_input_has_focus = (function() {return ($("input,textarea").is(":focus"))});
114-
{% if page_obj.has_previous %}
115-
if (e.keyCode == 37 && !any_input_has_focus()) {
116-
e.preventDefault();
117-
window.location.href = window.location.href.replace( /[\?#].*|$/, "?{{ previous_url }}" );
118-
}
119-
{% endif %}
120-
{% if page_obj.has_next %}
121-
if (e.keyCode == 39 && !any_input_has_focus()) {
122-
e.preventDefault();
123-
window.location.href = window.location.href.replace( /[\?#].*|$/, "?{{ next_url }}" );
124-
}
125-
{% endif %}
126-
});
101+
document.addEventListener("keydown", function(e) {
102+
// Do not trigger the navigation if an <input> or <textarea> currently has the focus
103+
var anyInputHasFocus = function() {
104+
return document.querySelector("input:focus, textarea:focus") !== null;
105+
};
106+
107+
{% if page_obj.has_previous %}
108+
if (e.keyCode === 37 && !anyInputHasFocus()) {
109+
e.preventDefault();
110+
window.location.href = window.location.href.replace(/[\?#].*|$/, "?{{ previous_url }}");
111+
}
112+
{% endif %}
113+
114+
{% if page_obj.has_next %}
115+
if (e.keyCode === 39 && !anyInputHasFocus()) {
116+
e.preventDefault();
117+
window.location.href = window.location.href.replace(/[\?#].*|$/, "?{{ next_url }}");
118+
}
119+
{% endif %}
120+
});
127121
{% endif %}
128122

129123
$('select.bootstrap-select-filter')
@@ -162,7 +156,7 @@
162156
});
163157
}
164158

165-
/// If there are no active search/filters, add 'all=true' to the parameters
159+
// If there are no active search/filters, add 'all=true' to the parameters
166160
if (params.toString() === "") {
167161
params.set('all', 'true');
168162
}

product_portfolio/templates/product_portfolio/product_list.html

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33

44
{% block top-right-buttons %}
55
<div class="btn-group">
6-
<button id="compare_button" href="/products/compare/" class="btn btn-outline-dark disabled" data-bs-toggle="tooltip"><i class="far fa-clone"></i> {% trans "Compare" %}</button>
6+
<span class="d-inline-block" tabindex="0" data-bs-toggle="tooltip">
7+
<button id="compare_button" href="/products/compare/" class="btn btn-outline-dark disabled"><i class="far fa-clone"></i> {% trans "Compare" %}</button>
8+
</span>
79
</div>
810
{{ block.super }}
911
{% endblock %}
@@ -13,16 +15,17 @@
1315
<script>
1416
$(document).ready(function () {
1517
let compare_button = $('#compare_button');
18+
let compare_button_wrapper = compare_button.parent();
1619

1720
let handle_compare_button_display = function () {
1821
let count = $('tbody input[type="checkbox"]:checked').length;
1922
if (count === 2) {
2023
compare_button.removeClass('disabled');
21-
compare_button.attr('data-bs-title', '');
24+
compare_button_wrapper.attr('data-bs-original-title', '');
2225
}
2326
else {
2427
compare_button.addClass('disabled');
25-
compare_button.attr('data-bs-title', 'Select first two products to compare, then click this button');
28+
compare_button_wrapper.attr('data-bs-original-title', 'Select first two products to compare, then click this button');
2629
}
2730
};
2831

product_portfolio/tests/test_views.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1054,8 +1054,7 @@ def test_product_portfolio_list_view_compare_button(self):
10541054
url = resolve_url("product_portfolio:product_list")
10551055
response = self.client.get(url)
10561056
expected = """
1057-
<button id="compare_button" href="/products/compare/"
1058-
class="btn btn-outline-dark disabled" data-bs-toggle="tooltip">
1057+
<button id="compare_button" href="/products/compare/" class="btn btn-outline-dark disabled">
10591058
<i class="far fa-clone"></i> Compare
10601059
</button>
10611060
"""

0 commit comments

Comments
 (0)