The script below shows how to add the following features to the columns of the listview table.
- Hide one or more columns
- Freeze one or more columns
- Reorder the columns
All changes made are saved in the user's session, that is, when a new session is created, the changes will be lost.
In the static directory, which is configured in the django settings STATICFILES_DIRS, create the file changelist_organizer_columns.js inside /admin/js. For example, if your STATICFILES_DIRS is mapped to the statics directory, the file would look like this /statics/admin.js/changelist_organizer_columns.js.
// changelist_organizer_columns.js
// The original code for the snippet is here: https://gist.github.com/duducp/0c5b32f7936b924b39c10e5983de1199#file-changelist-organizer-columns-md
// Wait for the DOM to be fully loaded before running the script
document.addEventListener('DOMContentLoaded', function() {
const table = document.getElementById('result_list');
if (!table) return;
// --- SessionStorage Key ---
const STORAGE_KEY = 'changelist_columns_' + location.pathname;
// --- Helpers for default state (reset only) ---
let defaultState = null;
function getDefaultState() {
const ths = Array.from(table.querySelectorAll('thead th'));
return ths.map((th, idx) => ({
idx,
text: th.textContent.trim(),
pinned: th.classList.contains('pinned-column'),
visible: th.style.display !== 'none'
}));
}
function saveDefaultState() {
if (!defaultState) {
defaultState = getDefaultState();
}
}
function restoreState(state) {
if (!state) return;
// Map current ths and cells by their text content
const ths = Array.from(table.querySelectorAll('thead th'));
const tr = table.querySelector('thead tr');
const thByText = {};
ths.forEach(th => {
thByText[th.textContent.trim()] = th;
});
// Remove all ths
ths.forEach(th => tr.removeChild(th));
// Add in new order by matching text
state.forEach(col => {
if (thByText[col.text]) tr.appendChild(thByText[col.text]);
});
// Reorder tbody
const rows = table.querySelectorAll('tbody tr');
rows.forEach(row => {
const cells = Array.from(row.children);
// Build a map from header text to cell
const cellByText = {};
cells.forEach((cell, i) => {
if (ths[i]) {
cellByText[ths[i].textContent.trim()] = cell;
}
});
// Remove all cells
while (row.firstChild) row.removeChild(row.firstChild);
state.forEach(col => {
if (cellByText[col.text]) row.appendChild(cellByText[col.text]);
});
});
// Set visibility and pin
const newThs = Array.from(table.querySelectorAll('thead th'));
state.forEach((col, i) => {
const th = newThs[i];
if (!th) return;
if (col.visible) showColumn(i);
else hideColumn(i);
if (col.pinned) pinColumn(i);
else unpinColumn(i);
});
// After restoring, save to sessionStorage
saveTableState();
}
// --- Table State Persistence ---
function getTableState() {
const ths = Array.from(table.querySelectorAll('thead th'));
return ths.map((th, idx) => ({
idx,
text: th.textContent.trim(),
pinned: th.classList.contains('pinned-column'),
visible: th.style.display !== 'none'
}));
}
function saveTableState() {
try {
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(getTableState()));
} catch (e) {}
}
function loadTableState() {
try {
const data = sessionStorage.getItem(STORAGE_KEY);
if (data) return JSON.parse(data);
} catch (e) {}
return null;
}
function clearTableState() {
try {
sessionStorage.removeItem(STORAGE_KEY);
} catch (e) {}
}
// --- Hide/Show Column Logic ---
function hideColumn(idx) {
const headerCells = table.querySelectorAll('thead th');
if (headerCells[idx]) headerCells[idx].style.display = 'none';
const rows = table.querySelectorAll('tbody tr');
rows.forEach(row => {
if (row.children[idx]) row.children[idx].style.display = 'none';
});
saveTableState();
}
function showColumn(idx) {
const headerCells = table.querySelectorAll('thead th');
if (headerCells[idx]) headerCells[idx].style.display = '';
const rows = table.querySelectorAll('tbody tr');
rows.forEach(row => {
if (row.children[idx]) row.children[idx].style.display = '';
});
saveTableState();
}
// --- Add Hide Button and Pin Button to Columns ---
function addHideButtons() {
const headerCells = table.querySelectorAll('thead th');
headerCells.forEach((th, idx) => {
// Do not add hide/pin button to the first column if it has class 'action-checkbox-column'
if (idx === 0 && th.classList.contains('action-checkbox-column')) return;
if (th.querySelector('.hide-col-btn')) return;
th.style.whiteSpace = 'nowrap';
th.style.verticalAlign = 'middle';
// Wrap existing content in a span if not already
let contentWrap = th.querySelector('.th-content-wrap');
if (!contentWrap) {
contentWrap = document.createElement('span');
contentWrap.className = 'th-content-wrap';
contentWrap.style.display = 'flex';
contentWrap.style.alignItems = 'center';
contentWrap.style.justifyContent = 'space-between';
contentWrap.style.width = '100%';
while (th.firstChild) {
contentWrap.appendChild(th.firstChild);
}
th.appendChild(contentWrap);
} else {
contentWrap.style.display = 'flex';
contentWrap.style.alignItems = 'center';
contentWrap.style.justifyContent = 'space-between';
contentWrap.style.width = '100%';
}
// Hide button styled like Django sort options
const hideBtn = document.createElement('a');
hideBtn.href = '#';
hideBtn.title = 'Hide column';
hideBtn.className = 'hide-col-btn sorthide';
hideBtn.style.cssText = `
margin-left: 6px;
font-size: 13px;
color: var(--primary, #ba2121);
text-decoration: none;
vertical-align: middle;
display: flex;
align-items: center;
justify-content: center;
height: 20px;
width: 20px;
border-radius: 2px;
`;
hideBtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 12 12" style="vertical-align:middle;" xmlns="http://www.w3.org/2000/svg"><rect x="2" y="5" width="8" height="2" rx="1" fill="currentColor"/></svg>';
hideBtn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
const currentThs = Array.from(table.querySelectorAll('thead th'));
const realIdx = currentThs.indexOf(th);
hideColumn(realIdx);
});
// Pin button styled like Django sort options
const pinBtn = document.createElement('a');
pinBtn.href = '#';
pinBtn.title = th.classList.contains('pinned-column') ? 'Unpin column' : 'Pin column';
pinBtn.className = 'pin-col-btn sortpin';
pinBtn.style.cssText = `
margin-left: 4px;
font-size: 13px;
color: var(--primary, #3273dc);
text-decoration: none;
vertical-align: middle;
display: flex;
align-items: center;
justify-content: center;
height: 20px;
width: 20px;
border-radius: 2px;
`;
pinBtn.innerHTML = th.classList.contains('pinned-column')
? '<svg width="12" height="12" viewBox="0 0 12 12" style="vertical-align:middle;" xmlns="http://www.w3.org/2000/svg"><path d="M3 9l6-6M9 3v6H3" stroke="currentColor" stroke-width="2" fill="none"/></svg>'
: '<svg width="12" height="12" viewBox="0 0 12 12" style="vertical-align:middle;" xmlns="http://www.w3.org/2000/svg"><rect x="5" y="2" width="2" height="8" rx="1" fill="currentColor"/><rect x="2" y="5" width="8" height="2" rx="1" fill="currentColor"/></svg>';
pinBtn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
const currentThs = Array.from(table.querySelectorAll('thead th'));
const realIdx = currentThs.indexOf(th);
if (th.classList.contains('pinned-column')) {
unpinColumn(realIdx);
updatePinBtnIcon(pinBtn, false);
} else {
pinColumn(realIdx);
updatePinBtnIcon(pinBtn, true);
}
});
// Remove any existing hide/pin button in the wrapper before appending
const oldHideBtn = contentWrap.querySelector('.hide-col-btn');
if (oldHideBtn) oldHideBtn.remove();
const oldPinBtn = contentWrap.querySelector('.pin-col-btn');
if (oldPinBtn) oldPinBtn.remove();
// Place the hide and pin buttons at the end (right) of the flex container
contentWrap.appendChild(hideBtn);
contentWrap.appendChild(pinBtn);
});
}
// Helper to update the pin icon and title
function updatePinBtnIcon(pinBtn, pinned) {
if (!pinBtn) return;
if (pinned) {
pinBtn.title = 'Unpin column';
pinBtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 12 12" style="vertical-align:middle;" xmlns="http://www.w3.org/2000/svg"><path d="M3 9l6-6M9 3v6H3" stroke="currentColor" stroke-width="2" fill="none"/></svg>';
} else {
pinBtn.title = 'Pin column';
pinBtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 12 12" style="vertical-align:middle;" xmlns="http://www.w3.org/2000/svg"><rect x="5" y="2" width="2" height="8" rx="1" fill="currentColor"/><rect x="2" y="5" width="8" height="2" rx="1" fill="currentColor"/></svg>';
}
}
// --- Hide/Show Columns Popup ---
function createShowColumnsPopup() {
if (document.getElementById('show-columns-popup')) return;
// Create overlay
const overlay = document.createElement('div');
overlay.id = 'show-columns-popup-overlay';
overlay.style.cssText = `
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.3);
z-index: 9998;
`;
const ths = table.querySelectorAll('thead th');
const popup = document.createElement('div');
popup.id = 'show-columns-popup';
popup.style.cssText = `
position: fixed;
top: 80px;
left: 50%;
transform: translateX(-50%);
background: var(--body-bg, #fff);
color: var(--body-fg, #333);
border: 1px solid var(--border-color, #ccc);
border-radius: 6px;
box-shadow: 0 4px 24px rgba(0,0,0,0.15);
z-index: 9999;
padding: 18px 24px 18px 24px;
min-width: 350px;
max-width: 90vw;
`;
const title = document.createElement('div');
title.textContent = 'MANAGE COLUMNS';
title.style.cssText = 'font-weight: bold; margin-bottom: 12px; font-size: 16px;';
popup.appendChild(title);
// --- Info message ---
const infoMsg = document.createElement('div');
infoMsg.textContent = 'Changes are saved only for this session.';
infoMsg.style.cssText = 'font-size: 13px; color: var(--body-quiet-color, #888); margin-bottom: 10px;';
popup.appendChild(infoMsg);
const closeBtn = document.createElement('button');
closeBtn.type = 'button';
closeBtn.innerHTML = '×';
closeBtn.style.cssText = `
position: absolute;
top: 8px;
right: 12px;
background: none;
border: none;
font-size: 22px;
color: var(--body-fg, #333);
cursor: pointer;
`;
closeBtn.onclick = () => {
popup.remove();
overlay.remove();
};
popup.appendChild(closeBtn);
// Container for list and ordering (now as a table)
const tableContainer = document.createElement('div');
tableContainer.style.cssText = 'clear: both; max-height: 50vh; overflow-y: auto; margin-top: 12px;';
const columnsTable = document.createElement('table');
columnsTable.style.cssText = `
width: 100%;
border-collapse: collapse;
background: transparent;
`;
// Build list of columns for show/hide and ordering
const columnItems = [];
// Add pin checkbox
ths.forEach((th, idx) => {
if (idx === 0 && th.classList.contains('action-checkbox-column')) return;
const tr = document.createElement('tr');
tr.className = 'column-popup-item';
tr.setAttribute('data-col-idx', idx);
tr.style.cursor = 'grab';
// Drag handle
const dragTd = document.createElement('td');
dragTd.style.cssText = 'width:28px;padding:4px 0 4px 0;text-align:center;';
const dragHandle = document.createElement('span');
dragHandle.innerHTML = `<svg width="16" height="16" viewBox="0 0 18 18" style="opacity:0.7;" xmlns="http://www.w3.org/2000/svg">
<circle cx="5" cy="5" r="1.5" fill="currentColor"/>
<circle cx="5" cy="9" r="1.5" fill="currentColor"/>
<circle cx="5" cy="13" r="1.5" fill="currentColor"/>
<circle cx="13" cy="5" r="1.5" fill="currentColor"/>
<circle cx="13" cy="9" r="1.5" fill="currentColor"/>
<circle cx="13" cy="13" r="1.5" fill="currentColor"/>
</svg>`;
dragHandle.style.cssText = 'cursor: grab; color: var(--primary, #3273dc);';
dragHandle.className = 'popup-drag-handle';
dragHandle.title = 'Order column';
dragTd.appendChild(dragHandle);
// Show/Hide Checkbox
const checkboxTd = document.createElement('td');
checkboxTd.style.cssText = 'width:32px;padding:4px 0 4px 0;text-align:center;';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = th.style.display !== 'none';
checkbox.title = 'Show/Hide column';
checkbox.style.marginRight = '0';
checkboxTd.appendChild(checkbox);
// Pin checkbox
const pinTd = document.createElement('td');
pinTd.style.cssText = 'width:32px;padding:4px 0 4px 0;text-align:center;';
const pinCheckbox = document.createElement('input');
pinCheckbox.type = 'checkbox';
pinCheckbox.checked = th.classList.contains('pinned-column');
pinCheckbox.title = 'Pin column';
pinTd.appendChild(pinCheckbox);
pinCheckbox.addEventListener('change', function() {
if (pinCheckbox.checked) {
pinColumn(idx);
} else {
unpinColumn(idx);
}
});
// Label
const labelTd = document.createElement('td');
labelTd.style.cssText = 'padding:4px 0 4px 0;';
let labelText = th.textContent || `Column ${idx+1}`;
const label = document.createElement('span');
label.textContent = labelText.trim();
label.style.flex = '1';
labelTd.appendChild(label);
checkbox.addEventListener('change', function() {
if (checkbox.checked) {
showColumn(idx);
} else {
hideColumn(idx);
}
});
tr.appendChild(dragTd);
tr.appendChild(checkboxTd);
tr.appendChild(pinTd);
tr.appendChild(labelTd);
columnItems.push(tr);
columnsTable.appendChild(tr);
});
// --- Update columnsTable header to include pin column ---
if (!columnsTable.querySelector('thead')) {
const thead = document.createElement('thead');
const headerRow = document.createElement('tr');
headerRow.innerHTML = `
<th>Order</th>
<th>Show</th>
<th>Pin</th>
<th>Column</th>
`;
thead.appendChild(headerRow);
columnsTable.appendChild(thead);
}
// --- Visual indicator for drop position ---
let dropIndicator = document.createElement('tr');
dropIndicator.className = 'popup-drop-indicator';
dropIndicator.style.pointerEvents = 'none';
dropIndicator.innerHTML = `<td colspan="4" style="padding:0;height:0;">
<div style="height:0;border-top:2px solid var(--primary,#3273dc);margin:0 0 0 0;"></div>
</td>`;
// Drag and drop logic for ordering columns in popup
let dragSrcItem = null;
let lastDropTarget = null;
let lastDropBefore = true;
columnItems.forEach(item => {
item.draggable = true;
item.addEventListener('dragstart', function(e) {
dragSrcItem = item;
item.style.opacity = '0.5';
e.dataTransfer.effectAllowed = 'move';
// Remove any existing indicator
if (dropIndicator.parentNode) dropIndicator.parentNode.removeChild(dropIndicator);
});
item.addEventListener('dragend', function(e) {
item.style.opacity = '';
dragSrcItem = null;
if (dropIndicator.parentNode) dropIndicator.parentNode.removeChild(dropIndicator);
});
item.addEventListener('dragover', function(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
// Determine if cursor is in top or bottom half
const rect = item.getBoundingClientRect();
const offset = e.clientY - rect.top;
const before = offset < rect.height / 2;
// Remove indicator if already present elsewhere
if (dropIndicator.parentNode) dropIndicator.parentNode.removeChild(dropIndicator);
if (before) {
columnsTable.insertBefore(dropIndicator, item);
} else {
if (item.nextSibling) {
columnsTable.insertBefore(dropIndicator, item.nextSibling);
} else {
columnsTable.appendChild(dropIndicator);
}
}
lastDropTarget = item;
lastDropBefore = before;
});
item.addEventListener('dragleave', function(e) {
// Remove indicator if leaving the row and not moving to indicator itself
if (dropIndicator.parentNode && !columnsTable.contains(e.relatedTarget)) {
dropIndicator.parentNode.removeChild(dropIndicator);
}
});
item.addEventListener('drop', function(e) {
e.preventDefault();
if (dragSrcItem && dragSrcItem !== item) {
if (dropIndicator.parentNode) dropIndicator.parentNode.removeChild(dropIndicator);
if (lastDropBefore) {
columnsTable.insertBefore(dragSrcItem, item);
} else {
if (item.nextSibling) {
columnsTable.insertBefore(dragSrcItem, item.nextSibling);
} else {
columnsTable.appendChild(dragSrcItem);
}
}
updateTableColumnOrder();
}
});
});
columnsTable.addEventListener('drop', function(e) {
// Handle drop at the end of the table
if (dragSrcItem && dropIndicator.parentNode) {
columnsTable.appendChild(dragSrcItem);
dropIndicator.parentNode.removeChild(dropIndicator);
updateTableColumnOrder();
}
});
// Update table column order based on popup table
function updateTableColumnOrder() {
// Get new order and pin states from popup (use the DOM order of .column-popup-item)
const newOrder = [];
const newVisibility = [];
const pinStates = [];
columnsTable.querySelectorAll('.column-popup-item').forEach(item => {
const label = item.querySelector('td:last-child span');
const colText = label ? label.textContent.trim() : '';
newOrder.push(colText);
const checkbox = item.querySelector('input[type="checkbox"]');
newVisibility.push(checkbox && checkbox.checked);
const pinCheckbox = item.querySelectorAll('input[type="checkbox"]')[1];
pinStates.push(pinCheckbox && pinCheckbox.checked);
});
// Get all ths and cells before any DOM manipulation
const thead = table.querySelector('thead');
const tr = thead.querySelector('tr');
const thsArr = Array.from(tr.children);
let startIdx = 0;
if (thsArr[0] && thsArr[0].classList.contains('action-checkbox-column')) startIdx = 1;
// Build a map of th label text to th element (before any DOM changes)
const thMap = {};
thsArr.forEach((th, i) => {
if (i === 0 && startIdx === 1) return; // skip action-checkbox-column
thMap[th.textContent.trim()] = th;
});
// Reorder thead
while (tr.firstChild) tr.removeChild(tr.firstChild);
if (startIdx === 1) tr.appendChild(thsArr[0]);
newOrder.forEach(colText => {
if (thMap[colText]) tr.appendChild(thMap[colText]);
});
// Reorder tbody rows
const rows = table.querySelectorAll('tbody tr');
rows.forEach(row => {
const cells = Array.from(row.children);
const cellMap = {};
cells.forEach((cell, i) => {
if (i === 0 && startIdx === 1) return; // skip action-checkbox-column
const th = thsArr[i];
if (th) cellMap[th.textContent.trim()] = cell;
});
const frag = document.createDocumentFragment();
if (startIdx === 1) frag.appendChild(cells[0]);
newOrder.forEach(colText => {
if (cellMap[colText]) frag.appendChild(cellMap[colText]);
});
row.innerHTML = '';
row.appendChild(frag);
});
// Set visibility and pin
const newThsArr = Array.from(tr.children);
newThsArr.forEach((th, i) => {
// Adjust index for show/hide/pin to skip action-checkbox-column
let colIdx = i;
if (startIdx === 1) colIdx = i + 1;
if (i < newVisibility.length) {
if (newVisibility[i]) showColumn(colIdx);
else hideColumn(colIdx);
}
});
// Pin columns according to the new order and pinStates
newThsArr.forEach((th, i) => {
let colIdx = i;
if (startIdx === 1) colIdx = i + 1;
const pinBtn = th.querySelector('.pin-col-btn');
if (i < pinStates.length) {
if (pinStates[i]) {
pinColumn(colIdx);
updatePinBtnIcon(pinBtn, true);
} else {
unpinColumn(colIdx);
updatePinBtnIcon(pinBtn, false);
}
}
});
}
// In the popup, after drag/drop or checkbox change, always update pin icons in the table
columnsTable.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
checkbox.addEventListener('change', function() {
updateTableColumnOrder();
// Update pin icons after any change
setTimeout(() => {
const ths = table.querySelectorAll('thead th');
ths.forEach(th => {
const pinBtn = th.querySelector('.pin-col-btn');
if (pinBtn) {
updatePinBtnIcon(pinBtn, th.classList.contains('pinned-column'));
}
});
}, 10);
});
});
tableContainer.appendChild(columnsTable);
// --- Footer with Reset Button ---
const footer = document.createElement('div');
footer.style.cssText = 'margin-top: 18px; display: flex; justify-content: flex-end; gap: 8px;';
// Reset Button
const resetBtn = document.createElement('button');
resetBtn.type = 'button';
resetBtn.textContent = 'RESET';
resetBtn.title = 'Reset to default columns';
resetBtn.style.cssText = `
background: var(--button-bg);
color: var(--button-fg);
border: none;
border-radius: 4px;
padding: 4px 12px;
cursor: pointer;
transition: background 0.15s;
`;
resetBtn.addEventListener('mouseenter', function() {
resetBtn.style.background = 'var(--button-hover-bg)';
});
resetBtn.addEventListener('mouseleave', function() {
resetBtn.style.background = 'var(--button-bg)';
});
resetBtn.onclick = function() {
if (window.confirm('Are you sure you want to reset columns to default configuration?')) {
clearTableState();
if (defaultState) {
restoreState(defaultState);
// Atualiza os ícones dos botões de pin após reset
setTimeout(() => {
const ths = table.querySelectorAll('thead th');
ths.forEach(th => {
const pinBtn = th.querySelector('.pin-col-btn');
if (pinBtn) {
updatePinBtnIcon(pinBtn, th.classList.contains('pinned-column'));
}
});
}, 10);
// Rebuild popup UI to reflect default
popup.remove();
overlay.remove();
setTimeout(createShowColumnsPopup, 50);
} else {
window.location.reload();
}
}
};
footer.appendChild(resetBtn);
popup.appendChild(tableContainer);
popup.appendChild(footer);
document.body.appendChild(overlay);
document.body.appendChild(popup);
// Focus on first checkbox
setTimeout(() => {
const firstCheckbox = popup.querySelector('input[type="checkbox"]');
if (firstCheckbox) firstCheckbox.focus();
}, 100);
}
// --- Add Show/Hide Columns Button to object-tools ---
function addShowColumnsButton() {
const objectTools = document.querySelector('.object-tools');
if (!objectTools || objectTools.querySelector('.show-columns-btn')) return;
const btn = document.createElement('li');
btn.innerHTML = `<a href="#" class="show-columns-btn" style="display:flex;align-items:center;gap:6px;">
Columns
<svg width="16" height="16" viewBox="0 0 18 18" style="opacity:0.7;" xmlns="http://www.w3.org/2000/svg">
<rect x="2" y="4" width="14" height="2" rx="1" fill="currentColor"/>
<rect x="2" y="8" width="14" height="2" rx="1" fill="currentColor"/>
<rect x="2" y="12" width="14" height="2" rx="1" fill="currentColor"/>
</svg>
</a>`;
btn.querySelector('a').onclick = function(e) {
e.preventDefault();
createShowColumnsPopup();
};
objectTools.appendChild(btn);
}
// --- Pin/Unpin Column Logic ---
function pinColumn(idx) {
const ths = table.querySelectorAll('thead th');
if (!ths[idx]) return;
ths[idx].classList.add('pinned-column');
ths[idx].style.position = 'sticky';
ths[idx].style.left = getPinnedLeft(idx) + 'px';
ths[idx].style.zIndex = '2';
ths[idx].style.background = 'var(--selected-row, #ffe)';
// Pin all cells in this column
const rows = table.querySelectorAll('tbody tr');
rows.forEach(row => {
if (row.children[idx]) {
row.children[idx].classList.add('pinned-column');
row.children[idx].style.position = 'sticky';
row.children[idx].style.left = getPinnedLeft(idx) + 'px';
row.children[idx].style.zIndex = '1';
row.children[idx].style.background = 'var(--selected-row, #ffe)';
}
});
saveTableState();
}
function unpinColumn(idx) {
const ths = table.querySelectorAll('thead th');
if (!ths[idx]) return;
ths[idx].classList.remove('pinned-column');
ths[idx].style.position = '';
ths[idx].style.left = '';
ths[idx].style.zIndex = '';
ths[idx].style.background = '';
// Unpin all cells in this column
const rows = table.querySelectorAll('tbody tr');
rows.forEach(row => {
if (row.children[idx]) {
row.children[idx].classList.remove('pinned-column');
row.children[idx].style.position = '';
row.children[idx].style.left = '';
row.children[idx].style.zIndex = '';
row.children[idx].style.background = '';
}
});
saveTableState();
}
// Helper to calculate left offset for sticky columns
function getPinnedLeft(idx) {
const ths = table.querySelectorAll('thead th');
let left = 0;
for (let i = 0; i < ths.length; i++) {
if (i === idx) break;
// Only count visible and pinned columns before idx
if (ths[i] && ths[i].style.display !== 'none' && ths[i].classList.contains('pinned-column')) {
left += ths[i].offsetWidth;
}
}
return left;
}
// --- Initial Setup ---
addHideButtons();
addShowColumnsButton();
saveDefaultState();
// --- Restore state from sessionStorage if present ---
const savedState = loadTableState();
if (savedState && Array.isArray(savedState)) {
restoreState(savedState);
}
});
To inject a JS file into your Django admin, you have these main options:
- By ModelAdmin Media class
Add a Media class to your ModelAdmin and specify the JS file path:
class MyModelAdmin(admin.ModelAdmin):
class Media:
js = ('admin/js/changelist_organizer_columns.js',)
This loads the JS only for that admin.
- Globally by admin templates
Override admin/base_site.html or admin/change_list.html and add a
<script> tag:
{% extends 'admin/base_site.html' %}
{% load static %}
{% block extrahead %}
{{ block.super }}
<script src="{% static 'admin/js/changelist_organizer_columns.js' %}"></script>
{% endblock %}
This loads the JS for all admin pages. You must create the HTML file within the templates directory configured in Django's settings. In this case, it would look like this: templates/admin
Header fixed at the top