Refactor navbar styles and improve dropdown accessibility for better mobile usability
This commit is contained in:
+173
-170
@@ -110,76 +110,76 @@
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Improve navbar tap targets and mobile dropdown behaviour */
|
/* Improve navbar tap targets and mobile dropdown behaviour */
|
||||||
@media (max-width: 991.98px) {
|
@media (max-width: 991.98px) {
|
||||||
/* Make navbar links larger and easier to tap */
|
/* Make navbar links larger and easier to tap */
|
||||||
.navbar-nav .nav-link {
|
.navbar-nav .nav-link {
|
||||||
padding: .75rem 1rem;
|
padding: .75rem 1rem;
|
||||||
font-size: 1.05rem;
|
font-size: 1.05rem;
|
||||||
min-height: 44px; /* recommended minimum touch target */
|
min-height: 44px; /* recommended minimum touch target */
|
||||||
display: block;
|
display: block;
|
||||||
margin: .25rem 0;
|
margin: .25rem 0;
|
||||||
border-radius: .375rem;
|
border-radius: .375rem;
|
||||||
background-color: var(--bs-primary);
|
background-color: var(--bs-primary);
|
||||||
color: #fff !important;
|
color: #fff !important;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
box-shadow: 0 1px 0 rgba(0,0,0,0.05) inset;
|
box-shadow: 0 1px 0 rgba(0,0,0,0.05) inset;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Make links look like outline buttons for secondary links */
|
/* Make links look like outline buttons for secondary links */
|
||||||
.navbar-nav .nav-link.btn-outline {
|
.navbar-nav .nav-link.btn-outline {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
color: var(--bs-primary) !important;
|
color: var(--bs-primary) !important;
|
||||||
border: 1px solid rgba(255,255,255,0.08);
|
border: 1px solid rgba(255,255,255,0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Make dropdown items full-width and larger */
|
/* Make dropdown items full-width and larger */
|
||||||
.navbar-nav .dropdown-menu .dropdown-item {
|
.navbar-nav .dropdown-menu .dropdown-item {
|
||||||
padding: .75rem 1rem;
|
padding: .75rem 1rem;
|
||||||
font-size: 1.05rem;
|
font-size: 1.05rem;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
border-radius: .25rem;
|
border-radius: .25rem;
|
||||||
margin: .125rem 0;
|
margin: .125rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* When the navbar is collapsed, render dropdown menus as static stacked blocks
|
/* When the navbar is collapsed, render dropdown menus as static stacked blocks
|
||||||
so nested items are easy to tap (prevents absolute-position overlap issues). */
|
so nested items are easy to tap (prevents absolute-position overlap issues). */
|
||||||
.navbar-collapse .dropdown-menu {
|
.navbar-collapse .dropdown-menu {
|
||||||
position: static !important;
|
position: static !important;
|
||||||
float: none !important;
|
float: none !important;
|
||||||
display: block !important;
|
display: block !important;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
margin-top: .25rem;
|
margin-top: .25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure dropdown toggles are full-width when collapsed */
|
/* Ensure dropdown toggles are full-width when collapsed */
|
||||||
.navbar-collapse .dropdown > .nav-link {
|
.navbar-collapse .dropdown > .nav-link {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
/* Slightly increase root font-size so rem-based components scale */
|
/* Slightly increase root font-size so rem-based components scale */
|
||||||
font-size: 182%;
|
font-size: 182%;
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Slightly larger headings for clear hierarchy */
|
/* Slightly larger headings for clear hierarchy */
|
||||||
h1 { font-size: 1.8rem; }
|
h1 { font-size: 1.8rem; }
|
||||||
h2 { font-size: 1.4rem; }
|
h2 { font-size: 1.4rem; }
|
||||||
h3 { font-size: 1.15rem; }
|
h3 { font-size: 1.15rem; }
|
||||||
|
|
||||||
/* Improve spacing for paragraphs and lists */
|
/* Improve spacing for paragraphs and lists */
|
||||||
p, li { font-size: 1rem; }
|
p, li { font-size: 1rem; }
|
||||||
|
|
||||||
/* Make form controls and buttons a bit larger for touch */
|
/* Make form controls and buttons a bit larger for touch */
|
||||||
.form-control, .btn { font-size: 1rem; padding: .6rem .75rem; }
|
.form-control, .btn { font-size: 1rem; padding: .6rem .75rem; }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
{% block css %}
|
{% block css %}
|
||||||
@@ -224,7 +224,10 @@
|
|||||||
<a class="dropdown-item" href="{% url 'sbas:index' %}">SBAs</a>
|
<a class="dropdown-item" href="{% url 'sbas:index' %}">SBAs</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item" href="{% url 'generic:examcollection_list' %}">Exam Collection</a>
|
<a class="dropdown-item d-flex justify-content-between align-items-center" href="{% url 'generic:examcollection_list' %}">
|
||||||
|
<span><i class="bi bi-collection me-1" aria-hidden="true"></i> Exam Collections</span>
|
||||||
|
<span class="badge bg-secondary ms-2">Manage</span>
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
@@ -267,12 +270,12 @@
|
|||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div id="htmx-error"></div>
|
<div id="htmx-error"></div>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
id="cimar-login-needed"
|
id="cimar-login-needed"
|
||||||
hx-get='{% url "cimar_status" %}'
|
hx-get='{% url "cimar_status" %}'
|
||||||
hx-trigger="cimar-login-needed from:body"
|
hx-trigger="cimar-login-needed from:body"
|
||||||
>
|
>
|
||||||
</span>
|
</span>
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
@@ -313,10 +316,10 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Wire up all row-selection control blocks to the nearest .js-row-selectable table.
|
// Wire up all row-selection control blocks to the nearest .js-row-selectable table.
|
||||||
function findTableForControl(elem) {
|
function findTableForControl(elem) {
|
||||||
// Walk up ancestors and look for a table that either has the
|
// Walk up ancestors and look for a table that either has the
|
||||||
// `js-row-selectable` class or contains a checkbox named 'selection'.
|
// `js-row-selectable` class or contains a checkbox named 'selection'.
|
||||||
//var ancestor = elem.closest('div, section, main, article, form, body');
|
//var ancestor = elem.closest('div, section, main, article, form, body');
|
||||||
@@ -336,151 +339,151 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
// ancestor = ancestor.parentElement;
|
// ancestor = ancestor.parentElement;
|
||||||
//}
|
//}
|
||||||
// fallback to any table on the page that looks like it has selection
|
// fallback to any table on the page that looks like it has selection
|
||||||
var any = document.querySelector('table.js-row-selectable') //|| Array.from(document.querySelectorAll('table')).find(function(t){ return t.querySelector && (t.querySelector('input[name="selection"]') || t.querySelector('input[type="checkbox"]')); });
|
var any = document.querySelector('table.js-row-selectable') //|| Array.from(document.querySelectorAll('table')).find(function(t){ return t.querySelector && (t.querySelector('input[name="selection"]') || t.querySelector('input[type="checkbox"]')); });
|
||||||
return any || null;
|
return any || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to build id with optional token
|
// Helper to build id with optional token
|
||||||
function suffixed(idBase, token) {
|
function suffixed(idBase, token) {
|
||||||
return token ? idBase + '-' + token : idBase;
|
return token ? idBase + '-' + token : idBase;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure every selectable table has a row-selection control block. If the
|
// Ensure every selectable table has a row-selection control block. If the
|
||||||
// page author didn't render the controls, create them automatically and
|
// page author didn't render the controls, create them automatically and
|
||||||
// insert them before the table so the behavior is available out-of-the-box.
|
// insert them before the table so the behavior is available out-of-the-box.
|
||||||
function ensureControlsForTable(table) {
|
function ensureControlsForTable(table) {
|
||||||
if (!table) return;
|
if (!table) return;
|
||||||
// detect an existing nearby toggle button
|
// detect an existing nearby toggle button
|
||||||
var existing = table.previousElementSibling && table.previousElementSibling.querySelector && table.previousElementSibling.querySelector('[id^="toggle-row-selection"]');
|
var existing = table.previousElementSibling && table.previousElementSibling.querySelector && table.previousElementSibling.querySelector('[id^="toggle-row-selection"]');
|
||||||
if (existing) return; // already present
|
if (existing) return; // already present
|
||||||
|
|
||||||
// generate a short token for unique IDs
|
// generate a short token for unique IDs
|
||||||
var token = (Date.now().toString(16) + Math.floor(Math.random()*0xffff).toString(16));
|
var token = (Date.now().toString(16) + Math.floor(Math.random()*0xffff).toString(16));
|
||||||
|
|
||||||
var wrapper = document.createElement('div');
|
var wrapper = document.createElement('div');
|
||||||
wrapper.className = 'row-selection-controls-wrapper';
|
wrapper.className = 'row-selection-controls-wrapper';
|
||||||
wrapper.innerHTML = '<div class="d-flex justify-content-between align-items-center mb-2">'
|
wrapper.innerHTML = '<div class="d-flex justify-content-between align-items-center mb-2">'
|
||||||
+ ' <div>'
|
+ ' <div>'
|
||||||
+ ' <button class="btn btn-outline-secondary btn-sm" id="toggle-row-selection-' + token + '" type="button">Enable row selection</button>'
|
+ ' <button class="btn btn-outline-secondary btn-sm" id="toggle-row-selection-' + token + '" type="button">Enable row selection</button>'
|
||||||
+ ' <div id="selection-controls-' + token + '" class="btn-group ms-2" role="group" style="display:none;">'
|
+ ' <div id="selection-controls-' + token + '" class="btn-group ms-2" role="group" style="display:none;">'
|
||||||
+ ' <button class="btn btn-sm btn-outline-primary" id="select-all-' + token + '">Select all</button>'
|
+ ' <button class="btn btn-sm btn-outline-primary" id="select-all-' + token + '">Select all</button>'
|
||||||
+ ' <button class="btn btn-sm btn-outline-secondary" id="clear-selection-' + token + '">Clear</button>'
|
+ ' <button class="btn btn-sm btn-outline-secondary" id="clear-selection-' + token + '">Clear</button>'
|
||||||
+ ' </div>'
|
+ ' </div>'
|
||||||
+ ' </div>'
|
+ ' </div>'
|
||||||
+ ' <div>'
|
+ ' <div>'
|
||||||
+ ' <small class="text-muted">Selected: <span id="selected-count-' + token + '">0</span></small>'
|
+ ' <small class="text-muted">Selected: <span id="selected-count-' + token + '">0</span></small>'
|
||||||
+ ' </div>'
|
+ ' </div>'
|
||||||
+ '</div>';
|
+ '</div>';
|
||||||
|
|
||||||
// Insert the controls immediately before the table (or its responsive wrapper)
|
// Insert the controls immediately before the table (or its responsive wrapper)
|
||||||
var container = table.closest('.table-responsive') || table;
|
var container = table.closest('.table-responsive') || table;
|
||||||
container.parentNode.insertBefore(wrapper, container);
|
container.parentNode.insertBefore(wrapper, container);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create controls for any table that looks selectable but lacks a control block
|
// Create controls for any table that looks selectable but lacks a control block
|
||||||
Array.from(document.querySelectorAll('table')).forEach(function(t){
|
Array.from(document.querySelectorAll('table')).forEach(function(t){
|
||||||
if (t.querySelector && (t.querySelector('input[name="selection"]') || t.querySelector('input[type="checkbox"]'))) {
|
if (t.querySelector && (t.querySelector('input[name="selection"]') || t.querySelector('input[type="checkbox"]'))) {
|
||||||
ensureControlsForTable(t);
|
ensureControlsForTable(t);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// For every toggle button (supports both unsuffixed and suffixed IDs)
|
// For every toggle button (supports both unsuffixed and suffixed IDs)
|
||||||
document.querySelectorAll('button[id^="toggle-row-selection"]').forEach(function(toggleBtn) {
|
document.querySelectorAll('button[id^="toggle-row-selection"]').forEach(function(toggleBtn) {
|
||||||
// derive token (empty for unsuffixed)
|
// derive token (empty for unsuffixed)
|
||||||
var token = '';
|
var token = '';
|
||||||
if (toggleBtn.id !== 'toggle-row-selection') token = toggleBtn.id.replace('toggle-row-selection-', '');
|
if (toggleBtn.id !== 'toggle-row-selection') token = toggleBtn.id.replace('toggle-row-selection-', '');
|
||||||
|
|
||||||
var controlsEl = document.getElementById(suffixed('selection-controls', token));
|
var controlsEl = document.getElementById(suffixed('selection-controls', token));
|
||||||
var selectAllBtn = document.getElementById(suffixed('select-all', token));
|
var selectAllBtn = document.getElementById(suffixed('select-all', token));
|
||||||
var clearBtn = document.getElementById(suffixed('clear-selection', token));
|
var clearBtn = document.getElementById(suffixed('clear-selection', token));
|
||||||
var selectedCountSpan = document.getElementById(suffixed('selected-count', token));
|
var selectedCountSpan = document.getElementById(suffixed('selected-count', token));
|
||||||
|
|
||||||
var table = findTableForControl(toggleBtn);
|
var table = findTableForControl(toggleBtn);
|
||||||
if (!table) return; // nothing to do
|
if (!table) return; // nothing to do
|
||||||
|
|
||||||
var selectionEnabled = false;
|
var selectionEnabled = false;
|
||||||
|
|
||||||
function findRowCheckboxes() {
|
function findRowCheckboxes() {
|
||||||
if (!table) return [];
|
if (!table) return [];
|
||||||
// prefer named selection inputs but fall back to any checkbox
|
// prefer named selection inputs but fall back to any checkbox
|
||||||
var checks = Array.from(table.querySelectorAll('input[type="checkbox"][name="selection"]'));
|
var checks = Array.from(table.querySelectorAll('input[type="checkbox"][name="selection"]'));
|
||||||
if (!checks.length) checks = Array.from(table.querySelectorAll('input[type="checkbox"]'));
|
if (!checks.length) checks = Array.from(table.querySelectorAll('input[type="checkbox"]'));
|
||||||
return checks;
|
return checks;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSelectedCount() {
|
function updateSelectedCount() {
|
||||||
if (!selectedCountSpan) return;
|
if (!selectedCountSpan) return;
|
||||||
var checks = findRowCheckboxes();
|
var checks = findRowCheckboxes();
|
||||||
var count = checks.filter(function(c){ return c.checked; }).length;
|
var count = checks.filter(function(c){ return c.checked; }).length;
|
||||||
selectedCountSpan.textContent = count;
|
selectedCountSpan.textContent = count;
|
||||||
checks.forEach(function(cb){ var tr = cb.closest('tr'); if (tr) tr.classList.toggle('table-active', cb.checked); });
|
checks.forEach(function(cb){ var tr = cb.closest('tr'); if (tr) tr.classList.toggle('table-active', cb.checked); });
|
||||||
}
|
}
|
||||||
|
|
||||||
function setCheckboxesDisabled(disabled) {
|
function setCheckboxesDisabled(disabled) {
|
||||||
findRowCheckboxes().forEach(function(cb){ cb.disabled = disabled; });
|
findRowCheckboxes().forEach(function(cb){ cb.disabled = disabled; });
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideSelectionColumn() {
|
function hideSelectionColumn() {
|
||||||
if (!table) return;
|
if (!table) return;
|
||||||
table.querySelectorAll('th, td').forEach(function(cell){ if (cell.querySelector && cell.querySelector('input[type="checkbox"][name="selection"]')) cell.style.display = 'none'; });
|
table.querySelectorAll('th, td').forEach(function(cell){ if (cell.querySelector && cell.querySelector('input[type="checkbox"][name="selection"]')) cell.style.display = 'none'; });
|
||||||
}
|
}
|
||||||
|
|
||||||
function showSelectionColumn() {
|
function showSelectionColumn() {
|
||||||
if (!table) return;
|
if (!table) return;
|
||||||
table.querySelectorAll('th, td').forEach(function(cell){ if (cell.querySelector && cell.querySelector('input[type="checkbox"][name="selection"]')) cell.style.display = ''; });
|
table.querySelectorAll('th, td').forEach(function(cell){ if (cell.querySelector && cell.querySelector('input[type="checkbox"][name="selection"]')) cell.style.display = ''; });
|
||||||
}
|
}
|
||||||
|
|
||||||
// initialise state
|
// initialise state
|
||||||
setCheckboxesDisabled(true);
|
|
||||||
hideSelectionColumn();
|
|
||||||
|
|
||||||
// row click toggling (attach to table to avoid global handlers)
|
|
||||||
table.addEventListener('click', function(e){
|
|
||||||
if (!selectionEnabled) return;
|
|
||||||
var tag = (e.target.tagName || '').toLowerCase();
|
|
||||||
if (['input','a','button','select','textarea','label'].indexOf(tag) !== -1) return;
|
|
||||||
var tr = e.target.closest('tr');
|
|
||||||
if (!tr || !table.contains(tr)) return;
|
|
||||||
var cb = tr.querySelector('input[type="checkbox"][name="selection"]') || tr.querySelector('input[type="checkbox"]');
|
|
||||||
if (cb && !cb.disabled) { cb.checked = !cb.checked; tr.classList.toggle('table-active', cb.checked); updateSelectedCount(); }
|
|
||||||
}, true);
|
|
||||||
|
|
||||||
// listen for changes within the table to keep counts in sync
|
|
||||||
table.addEventListener('change', function(e){ if (e.target && e.target.matches && e.target.matches('input[type="checkbox"]')) updateSelectedCount(); }, true);
|
|
||||||
|
|
||||||
// wire controls
|
|
||||||
if (toggleBtn) {
|
|
||||||
toggleBtn.addEventListener('click', function() {
|
|
||||||
selectionEnabled = !selectionEnabled;
|
|
||||||
if (selectionEnabled) {
|
|
||||||
toggleBtn.textContent = 'Disable row selection';
|
|
||||||
if (controlsEl) controlsEl.style.display = '';
|
|
||||||
setCheckboxesDisabled(false);
|
|
||||||
showSelectionColumn();
|
|
||||||
} else {
|
|
||||||
toggleBtn.textContent = 'Enable row selection';
|
|
||||||
if (controlsEl) controlsEl.style.display = 'none';
|
|
||||||
setCheckboxesDisabled(true);
|
setCheckboxesDisabled(true);
|
||||||
hideSelectionColumn();
|
hideSelectionColumn();
|
||||||
findRowCheckboxes().forEach(function(cb){ cb.checked = false; });
|
|
||||||
updateSelectedCount();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectAllBtn) {
|
// row click toggling (attach to table to avoid global handlers)
|
||||||
selectAllBtn.addEventListener('click', function(){ findRowCheckboxes().forEach(function(cb){ if (!cb.disabled) cb.checked = true; }); updateSelectedCount(); });
|
table.addEventListener('click', function(e){
|
||||||
}
|
if (!selectionEnabled) return;
|
||||||
|
var tag = (e.target.tagName || '').toLowerCase();
|
||||||
|
if (['input','a','button','select','textarea','label'].indexOf(tag) !== -1) return;
|
||||||
|
var tr = e.target.closest('tr');
|
||||||
|
if (!tr || !table.contains(tr)) return;
|
||||||
|
var cb = tr.querySelector('input[type="checkbox"][name="selection"]') || tr.querySelector('input[type="checkbox"]');
|
||||||
|
if (cb && !cb.disabled) { cb.checked = !cb.checked; tr.classList.toggle('table-active', cb.checked); updateSelectedCount(); }
|
||||||
|
}, true);
|
||||||
|
|
||||||
if (clearBtn) {
|
// listen for changes within the table to keep counts in sync
|
||||||
clearBtn.addEventListener('click', function(){ findRowCheckboxes().forEach(function(cb){ if (!cb.disabled) cb.checked = false; }); updateSelectedCount(); });
|
table.addEventListener('change', function(e){ if (e.target && e.target.matches && e.target.matches('input[type="checkbox"]')) updateSelectedCount(); }, true);
|
||||||
}
|
|
||||||
|
// wire controls
|
||||||
|
if (toggleBtn) {
|
||||||
|
toggleBtn.addEventListener('click', function() {
|
||||||
|
selectionEnabled = !selectionEnabled;
|
||||||
|
if (selectionEnabled) {
|
||||||
|
toggleBtn.textContent = 'Disable row selection';
|
||||||
|
if (controlsEl) controlsEl.style.display = '';
|
||||||
|
setCheckboxesDisabled(false);
|
||||||
|
showSelectionColumn();
|
||||||
|
} else {
|
||||||
|
toggleBtn.textContent = 'Enable row selection';
|
||||||
|
if (controlsEl) controlsEl.style.display = 'none';
|
||||||
|
setCheckboxesDisabled(true);
|
||||||
|
hideSelectionColumn();
|
||||||
|
findRowCheckboxes().forEach(function(cb){ cb.checked = false; });
|
||||||
|
updateSelectedCount();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectAllBtn) {
|
||||||
|
selectAllBtn.addEventListener('click', function(){ findRowCheckboxes().forEach(function(cb){ if (!cb.disabled) cb.checked = true; }); updateSelectedCount(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clearBtn) {
|
||||||
|
clearBtn.addEventListener('click', function(){ findRowCheckboxes().forEach(function(cb){ if (!cb.disabled) cb.checked = false; }); updateSelectedCount(); });
|
||||||
|
}
|
||||||
|
|
||||||
// initial count
|
// initial count
|
||||||
updateSelectedCount();
|
updateSelectedCount();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user