diff --git a/generic/tables.py b/generic/tables.py
index a8745ecc..7b7f383c 100644
--- a/generic/tables.py
+++ b/generic/tables.py
@@ -18,8 +18,6 @@ from django.contrib.auth.models import User
# column (input name="selection") and table attrs that client-side
# scripts can use to find selectable tables.
class SelectionTable(tables.Table):
- # Provide a default selection checkbox column. Subclasses that wish to
- # customise can override this attribute.
selection = tables.CheckBoxColumn(
accessor="pk",
orderable=False,
@@ -32,16 +30,9 @@ class SelectionTable(tables.Table):
)
class Meta:
- # Defaults: add js-row-selectable class so client-side code can
- # locate tables that support row selection.
attrs = {"class": "table js-row-selectable", "data-selection-name": "selection"}
def __init__(self, *args, **kwargs):
- # Ensure selection column is always placed last in the column order
- # so that table actions/controls appear to the right. We attempt to
- # reorder columns after initialization; if django-tables2 provides
- # `reorder_columns`, use it, otherwise fall back to setting
- # `self.sequence`.
super().__init__(*args, **kwargs)
try:
cols = list(self.columns.names())
@@ -49,19 +40,12 @@ class SelectionTable(tables.Table):
cols.remove('selection')
cols.append('selection')
try:
- # Preferred API when available
self.reorder_columns(cols)
except Exception:
- # Fallback: set sequence attribute
self.sequence = tuple(cols)
except Exception:
- # Defensive: don't break table rendering if introspection fails
pass
- # Expose a per-instance selection token and ensure the table has the
- # `js-row-selectable` class so client-side scripts (or the inline
- # initializer below) can find the correct table without extra markup
- # in templates.
try:
token = format(id(self), 'x')
attrs = getattr(self, 'attrs', None) or {}
@@ -71,77 +55,100 @@ class SelectionTable(tables.Table):
attrs['data-selection-token'] = token
self.attrs = attrs
except Exception:
- # non-fatal
pass
- # By default render the row selection controls above the table so
- # templates don't need to include `{{ table.row_selection_controls }}`.
- try:
- # `row_selection_controls` is already marked safe HTML.
- self.caption = self.row_selection_controls
- except Exception:
- pass
+ self.caption = self.row_selection_controls
@property
def row_selection_controls(self):
- """Return HTML for the row-selection control bar.
+ token = (self.attrs or {}).get('data-selection-token', format(id(self), 'x'))
- Usage: render in template with `{{ table.row_selection_controls|safe }}`
+ html = (
+ f"
"
+ f"
"
+ f" "
+ f"
"
+ f" "
+ f" "
+ f"
"
+ f"
"
+ f"
"
+ f" Selected: 0"
+ f"
"
+ f"
"
+ )
- The control IDs are suffixed with a small per-instance token to
- avoid collisions when multiple tables are present on the same page.
- """
- token = (self.attrs or {}).get('data-selection-token', format(id(self), 'x'))
+ script = """
+"
- ).format(t=token)
+ var selectionEnabled = false;
+ var lastChecked = null;
- return mark_safe(html + script)
+ function findRowCheckboxes() { 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 setCheckboxesDisabled(disabled) { findRowCheckboxes().forEach(function(cb){ cb.disabled = disabled; }); lastChecked = null; }
+ function hideSelectionColumn() { table.querySelectorAll('th, td').forEach(function(cell){ if (cell.querySelector && cell.querySelector('input[type="checkbox"][name="selection"]')) cell.style.display = 'none'; }); }
+ function showSelectionColumn() { table.querySelectorAll('th, td').forEach(function(cell){ if (cell.querySelector && cell.querySelector('input[type="checkbox"][name="selection"]')) cell.style.display = ''; }); }
+
+ setCheckboxesDisabled(true); hideSelectionColumn();
+
+ table.addEventListener('click', function(e){
+ if (!selectionEnabled) return;
+ var tag = (e.target.tagName || '').toLowerCase();
+ if (['a','button','select','textarea','label'].indexOf(tag) !== -1) return;
+ if (e.target && e.target.matches && e.target.matches('input[type="checkbox"]')){
+ var cb = e.target;
+ if (e.shiftKey && lastChecked && lastChecked !== cb){
+ var checks = findRowCheckboxes();
+ var start = checks.indexOf(lastChecked);
+ var end = checks.indexOf(cb);
+ if (start > -1 && end > -1){
+ if (start > end){ var tmp = start; start = end; end = tmp; }
+ for (var i = start; i <= end; i++){ if (!checks[i].disabled) checks[i].checked = cb.checked; }
+ updateSelectedCount();
+ }
+ }
+ lastChecked = cb;
+ 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(); lastChecked = cb; }
+ }, true);
+
+ table.addEventListener('change', function(e){ if (e.target && e.target.matches && e.target.matches('input[type="checkbox"]')) updateSelectedCount(); }, true);
+
+ 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(); lastChecked = null; } });
+
+ if (selectAllBtn) selectAllBtn.addEventListener('click', function(){ findRowCheckboxes().forEach(function(cb){ if (!cb.disabled) cb.checked = true; }); updateSelectedCount(); lastChecked = null; });
+ if (clearBtn) clearBtn.addEventListener('click', function(){ findRowCheckboxes().forEach(function(cb){ if (!cb.disabled) cb.checked = false; }); updateSelectedCount(); lastChecked = null; });
+
+ updateSelectedCount();
+ };
+})();
+
+"""
+
+ # Replace the simple placeholder with the runtime token. Using
+ # a plain triple-quoted string avoids f-string brace parsing errors
+ # from the JavaScript code which contains many `{`/`}` characters.
+ script = script.replace('__TOKEN__', token)
+
+ return mark_safe(html + script)
class SingleImageColumn(tables.Column):
def render(self, value):
diff --git a/rad.code-workspace b/rad.code-workspace
index feb2c722..c0f24d9d 100644
--- a/rad.code-workspace
+++ b/rad.code-workspace
@@ -15,6 +15,10 @@
"/^source \\.venv/bin/activate\\.fish && python manage\\.py check$/": {
"approve": true,
"matchCommandLine": true
+ },
+ "/^python -m py_compile /home/ross/penracourses/rad/generic/tables\\.py$/": {
+ "approve": true,
+ "matchCommandLine": true
}
}
}
diff --git a/templates/base.html b/templates/base.html
index bf743ebb..2b0f0459 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -348,6 +348,16 @@
// Selection wiring has been moved into per-table initializers
// emitted by `SelectionTable.row_selection_controls`.
+ try {
+ if (window.__selection_inits) {
+ Object.keys(window.__selection_inits).forEach(function(k){
+ try { window.__selection_inits[k](); } catch (e) { console.error('selection init failed', k, e); }
+ try { delete window.__selection_inits[k]; } catch (e) {}
+ });
+ }
+ } catch (err) {
+ console.error('Error running selection initializers', err);
+ }
});