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 */
|
||||
@media (max-width: 991.98px) {
|
||||
@media (max-width: 991.98px) {
|
||||
/* Make navbar links larger and easier to tap */
|
||||
.navbar-nav .nav-link {
|
||||
padding: .75rem 1rem;
|
||||
font-size: 1.05rem;
|
||||
min-height: 44px; /* recommended minimum touch target */
|
||||
display: block;
|
||||
margin: .25rem 0;
|
||||
border-radius: .375rem;
|
||||
background-color: var(--bs-primary);
|
||||
color: #fff !important;
|
||||
text-align: center;
|
||||
box-shadow: 0 1px 0 rgba(0,0,0,0.05) inset;
|
||||
}
|
||||
.navbar-nav .nav-link {
|
||||
padding: .75rem 1rem;
|
||||
font-size: 1.05rem;
|
||||
min-height: 44px; /* recommended minimum touch target */
|
||||
display: block;
|
||||
margin: .25rem 0;
|
||||
border-radius: .375rem;
|
||||
background-color: var(--bs-primary);
|
||||
color: #fff !important;
|
||||
text-align: center;
|
||||
box-shadow: 0 1px 0 rgba(0,0,0,0.05) inset;
|
||||
}
|
||||
|
||||
/* Make links look like outline buttons for secondary links */
|
||||
.navbar-nav .nav-link.btn-outline {
|
||||
background-color: transparent;
|
||||
color: var(--bs-primary) !important;
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
}
|
||||
.navbar-nav .nav-link.btn-outline {
|
||||
background-color: transparent;
|
||||
color: var(--bs-primary) !important;
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
}
|
||||
|
||||
/* Make dropdown items full-width and larger */
|
||||
.navbar-nav .dropdown-menu .dropdown-item {
|
||||
padding: .75rem 1rem;
|
||||
font-size: 1.05rem;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
border-radius: .25rem;
|
||||
margin: .125rem 0;
|
||||
}
|
||||
.navbar-nav .dropdown-menu .dropdown-item {
|
||||
padding: .75rem 1rem;
|
||||
font-size: 1.05rem;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
border-radius: .25rem;
|
||||
margin: .125rem 0;
|
||||
}
|
||||
|
||||
/* When the navbar is collapsed, render dropdown menus as static stacked blocks
|
||||
so nested items are easy to tap (prevents absolute-position overlap issues). */
|
||||
.navbar-collapse .dropdown-menu {
|
||||
position: static !important;
|
||||
float: none !important;
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
margin-top: .25rem;
|
||||
}
|
||||
.navbar-collapse .dropdown-menu {
|
||||
position: static !important;
|
||||
float: none !important;
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
margin-top: .25rem;
|
||||
}
|
||||
|
||||
/* Ensure dropdown toggles are full-width when collapsed */
|
||||
.navbar-collapse .dropdown > .nav-link {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
display: block;
|
||||
}
|
||||
.navbar-collapse .dropdown > .nav-link {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
html, body {
|
||||
html, body {
|
||||
/* Slightly increase root font-size so rem-based components scale */
|
||||
font-size: 182%;
|
||||
line-height: 1.45;
|
||||
}
|
||||
font-size: 182%;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
/* Slightly larger headings for clear hierarchy */
|
||||
h1 { font-size: 1.8rem; }
|
||||
h2 { font-size: 1.4rem; }
|
||||
h3 { font-size: 1.15rem; }
|
||||
h1 { font-size: 1.8rem; }
|
||||
h2 { font-size: 1.4rem; }
|
||||
h3 { font-size: 1.15rem; }
|
||||
|
||||
/* 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 */
|
||||
.form-control, .btn { font-size: 1rem; padding: .6rem .75rem; }
|
||||
.form-control, .btn { font-size: 1rem; padding: .6rem .75rem; }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
{% block css %}
|
||||
@@ -224,7 +224,10 @@
|
||||
<a class="dropdown-item" href="{% url 'sbas:index' %}">SBAs</a>
|
||||
</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>
|
||||
</ul>
|
||||
</li>
|
||||
@@ -267,12 +270,12 @@
|
||||
<div class="col-md-12">
|
||||
<div id="htmx-error"></div>
|
||||
|
||||
<span
|
||||
id="cimar-login-needed"
|
||||
hx-get='{% url "cimar_status" %}'
|
||||
hx-trigger="cimar-login-needed from:body"
|
||||
>
|
||||
</span>
|
||||
<span
|
||||
id="cimar-login-needed"
|
||||
hx-get='{% url "cimar_status" %}'
|
||||
hx-trigger="cimar-login-needed from:body"
|
||||
>
|
||||
</span>
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
@@ -313,10 +316,10 @@
|
||||
});
|
||||
|
||||
</script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 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
|
||||
// `js-row-selectable` class or contains a checkbox named 'selection'.
|
||||
//var ancestor = elem.closest('div, section, main, article, form, body');
|
||||
@@ -336,151 +339,151 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
// ancestor = ancestor.parentElement;
|
||||
//}
|
||||
// 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"]')); });
|
||||
return any || null;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
// Helper to build id with optional token
|
||||
function suffixed(idBase, token) {
|
||||
return token ? idBase + '-' + token : idBase;
|
||||
}
|
||||
function suffixed(idBase, token) {
|
||||
return token ? idBase + '-' + token : idBase;
|
||||
}
|
||||
|
||||
// Ensure every selectable table has a row-selection control block. If the
|
||||
// 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.
|
||||
function ensureControlsForTable(table) {
|
||||
if (!table) return;
|
||||
function ensureControlsForTable(table) {
|
||||
if (!table) return;
|
||||
// detect an existing nearby toggle button
|
||||
var existing = table.previousElementSibling && table.previousElementSibling.querySelector && table.previousElementSibling.querySelector('[id^="toggle-row-selection"]');
|
||||
if (existing) return; // already present
|
||||
var existing = table.previousElementSibling && table.previousElementSibling.querySelector && table.previousElementSibling.querySelector('[id^="toggle-row-selection"]');
|
||||
if (existing) return; // already present
|
||||
|
||||
// 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');
|
||||
wrapper.className = 'row-selection-controls-wrapper';
|
||||
wrapper.innerHTML = '<div class="d-flex justify-content-between align-items-center mb-2">'
|
||||
+ ' <div>'
|
||||
+ ' <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;">'
|
||||
+ ' <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>'
|
||||
+ ' </div>'
|
||||
+ ' </div>'
|
||||
+ ' <div>'
|
||||
+ ' <small class="text-muted">Selected: <span id="selected-count-' + token + '">0</span></small>'
|
||||
+ ' </div>'
|
||||
+ '</div>';
|
||||
var wrapper = document.createElement('div');
|
||||
wrapper.className = 'row-selection-controls-wrapper';
|
||||
wrapper.innerHTML = '<div class="d-flex justify-content-between align-items-center mb-2">'
|
||||
+ ' <div>'
|
||||
+ ' <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;">'
|
||||
+ ' <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>'
|
||||
+ ' </div>'
|
||||
+ ' </div>'
|
||||
+ ' <div>'
|
||||
+ ' <small class="text-muted">Selected: <span id="selected-count-' + token + '">0</span></small>'
|
||||
+ ' </div>'
|
||||
+ '</div>';
|
||||
|
||||
// Insert the controls immediately before the table (or its responsive wrapper)
|
||||
var container = table.closest('.table-responsive') || table;
|
||||
container.parentNode.insertBefore(wrapper, container);
|
||||
}
|
||||
var container = table.closest('.table-responsive') || table;
|
||||
container.parentNode.insertBefore(wrapper, container);
|
||||
}
|
||||
|
||||
// Create controls for any table that looks selectable but lacks a control block
|
||||
Array.from(document.querySelectorAll('table')).forEach(function(t){
|
||||
if (t.querySelector && (t.querySelector('input[name="selection"]') || t.querySelector('input[type="checkbox"]'))) {
|
||||
ensureControlsForTable(t);
|
||||
}
|
||||
});
|
||||
Array.from(document.querySelectorAll('table')).forEach(function(t){
|
||||
if (t.querySelector && (t.querySelector('input[name="selection"]') || t.querySelector('input[type="checkbox"]'))) {
|
||||
ensureControlsForTable(t);
|
||||
}
|
||||
});
|
||||
|
||||
// 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)
|
||||
var token = '';
|
||||
if (toggleBtn.id !== 'toggle-row-selection') token = toggleBtn.id.replace('toggle-row-selection-', '');
|
||||
var token = '';
|
||||
if (toggleBtn.id !== 'toggle-row-selection') token = toggleBtn.id.replace('toggle-row-selection-', '');
|
||||
|
||||
var controlsEl = document.getElementById(suffixed('selection-controls', token));
|
||||
var selectAllBtn = document.getElementById(suffixed('select-all', token));
|
||||
var clearBtn = document.getElementById(suffixed('clear-selection', token));
|
||||
var selectedCountSpan = document.getElementById(suffixed('selected-count', token));
|
||||
var controlsEl = document.getElementById(suffixed('selection-controls', token));
|
||||
var selectAllBtn = document.getElementById(suffixed('select-all', token));
|
||||
var clearBtn = document.getElementById(suffixed('clear-selection', token));
|
||||
var selectedCountSpan = document.getElementById(suffixed('selected-count', token));
|
||||
|
||||
var table = findTableForControl(toggleBtn);
|
||||
if (!table) return; // nothing to do
|
||||
var table = findTableForControl(toggleBtn);
|
||||
if (!table) return; // nothing to do
|
||||
|
||||
var selectionEnabled = false;
|
||||
var selectionEnabled = false;
|
||||
|
||||
function findRowCheckboxes() {
|
||||
if (!table) return [];
|
||||
function findRowCheckboxes() {
|
||||
if (!table) return [];
|
||||
// prefer named selection inputs but fall back to any checkbox
|
||||
var checks = Array.from(table.querySelectorAll('input[type="checkbox"][name="selection"]'));
|
||||
if (!checks.length) checks = Array.from(table.querySelectorAll('input[type="checkbox"]'));
|
||||
return checks;
|
||||
}
|
||||
var checks = Array.from(table.querySelectorAll('input[type="checkbox"][name="selection"]'));
|
||||
if (!checks.length) checks = Array.from(table.querySelectorAll('input[type="checkbox"]'));
|
||||
return checks;
|
||||
}
|
||||
|
||||
function updateSelectedCount() {
|
||||
if (!selectedCountSpan) return;
|
||||
var checks = findRowCheckboxes();
|
||||
var count = checks.filter(function(c){ return c.checked; }).length;
|
||||
selectedCountSpan.textContent = count;
|
||||
checks.forEach(function(cb){ var tr = cb.closest('tr'); if (tr) tr.classList.toggle('table-active', cb.checked); });
|
||||
}
|
||||
function updateSelectedCount() {
|
||||
if (!selectedCountSpan) return;
|
||||
var checks = findRowCheckboxes();
|
||||
var count = checks.filter(function(c){ return c.checked; }).length;
|
||||
selectedCountSpan.textContent = count;
|
||||
checks.forEach(function(cb){ var tr = cb.closest('tr'); if (tr) tr.classList.toggle('table-active', cb.checked); });
|
||||
}
|
||||
|
||||
function setCheckboxesDisabled(disabled) {
|
||||
findRowCheckboxes().forEach(function(cb){ cb.disabled = disabled; });
|
||||
}
|
||||
function setCheckboxesDisabled(disabled) {
|
||||
findRowCheckboxes().forEach(function(cb){ cb.disabled = disabled; });
|
||||
}
|
||||
|
||||
function hideSelectionColumn() {
|
||||
if (!table) return;
|
||||
table.querySelectorAll('th, td').forEach(function(cell){ if (cell.querySelector && cell.querySelector('input[type="checkbox"][name="selection"]')) cell.style.display = 'none'; });
|
||||
}
|
||||
function hideSelectionColumn() {
|
||||
if (!table) return;
|
||||
table.querySelectorAll('th, td').forEach(function(cell){ if (cell.querySelector && cell.querySelector('input[type="checkbox"][name="selection"]')) cell.style.display = 'none'; });
|
||||
}
|
||||
|
||||
function showSelectionColumn() {
|
||||
if (!table) return;
|
||||
table.querySelectorAll('th, td').forEach(function(cell){ if (cell.querySelector && cell.querySelector('input[type="checkbox"][name="selection"]')) cell.style.display = ''; });
|
||||
}
|
||||
function showSelectionColumn() {
|
||||
if (!table) return;
|
||||
table.querySelectorAll('th, td').forEach(function(cell){ if (cell.querySelector && cell.querySelector('input[type="checkbox"][name="selection"]')) cell.style.display = ''; });
|
||||
}
|
||||
|
||||
// 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);
|
||||
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(); });
|
||||
}
|
||||
// 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);
|
||||
|
||||
if (clearBtn) {
|
||||
clearBtn.addEventListener('click', function(){ findRowCheckboxes().forEach(function(cb){ if (!cb.disabled) cb.checked = false; }); updateSelectedCount(); });
|
||||
}
|
||||
// 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);
|
||||
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
|
||||
updateSelectedCount();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
updateSelectedCount();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user