Skip to content

Instantly share code, notes, and snippets.

@rf5860
Last active June 23, 2024 14:39
Show Gist options
  • Save rf5860/aeb462e00fc2b511f60efdcd0a1c0776 to your computer and use it in GitHub Desktop.
Save rf5860/aeb462e00fc2b511f60efdcd0a1c0776 to your computer and use it in GitHub Desktop.
Allows searching, column re-ordering, and column removal to any table
// ==UserScript==
// @name DataTables Anywhere
// @author rjf89
// @version 0.92
// @description Allows tables on any page to be searchable via DataTables
// @namespace DataTablesAnywhere
// @updateURL https://gist.github.com/rf5860/aeb462e00fc2b511f60efdcd0a1c0776/raw/DataTableAnywhere.user.js?cachebust=dkjflskjfldkf
// @downloadURL https://gist.github.com/rf5860/aeb462e00fc2b511f60efdcd0a1c0776/raw/DataTableAnywhere.user.js?cachebust=dkjflskjfldkf
// @match *://*/*
// @require https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js
// @require https://cdn.datatables.net/1.13.7/js/jquery.dataTables.min.js
// @require https://cdn.datatables.net/colreorder/1.5.4/js/dataTables.colReorder.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/mark.js/8.11.1/jquery.mark.min.js
// @require https://cdn.datatables.net/buttons/1.7.1/js/dataTables.buttons.min.js
// @require https://cdn.datatables.net/buttons/1.7.1/js/buttons.colVis.min.js
// @resource https://cdn.datatables.net/1.13.7/css/dataTables.bootstrap4.min.css
// @resource https://cdn.datatables.net/buttons/1.7.1/css/buttons.dataTables.min.css
// @grant GM_registerMenuCommand
// @grant GM_addStyle
// ==/UserScript==
// Alternative Style:
// @resource https://cdn.datatables.net/1.13.7/css/jquery.dataTables.min.css
// GM_addStyle('@import url("https://cdn.datatables.net/1.13.7/css/jquery.dataTables.min.css");');
const $ = window.jQuery;
GM_addStyle(`
.dataTables_filter {
float: left !important;
}
button.dt-button {
background-color: skyblue !important;
}
.dataTables_filter input, .columnFilter {
width: 200px;
height: 30px;
border-radius: 5px;
border: 1px solid #ccc;
padding: 5px;
font-size: 16px;
}
.dataTables_wrapper .dataTables_scrollHead th {
padding-bottom: 10px;
}
.dataTables_wrapper .dataTables_scrollHead th input {
margin-top: 5px;
}
`);
GM_addStyle('@import url("https://cdn.datatables.net/1.13.7/css/dataTables.bootstrap4.min.css");');
GM_addStyle('@import url("https://cdn.datatables.net/buttons/1.7.1/css/buttons.dataTables.min.css");');
GM_registerMenuCommand("Activate DataTables", () => {
$.fn.dataTable.ext.search.push(
(_settings, searchData, _index, _rowData, _counter) => {
const excludeInput = $(`#${_settings.sTableId}_exclude_filter input`).val();
if (excludeInput && excludeInput.length > 0) {
return !new RegExp(excludeInput, "i").test(searchData.join(" "));
}
return true;
}
);
const createElement = (type, props) => Object.assign(document.createElement(type), props);
const createLabel = (name) => createElement("label", { innerText: `${name}: ` });
const createInput = (type, placeholder) => createElement("input", { type, placeholder });
const createFilterDiv = (id, className) => createElement("div", { id, className });
const createTableFilter = (tableId, name, withLabel = true, inputFunction) => {
const filter = createFilterDiv(`${tableId}_${name.toLowerCase()}_filter`, "dataTables_filter");
if (withLabel) filter.appendChild(createLabel(name));
filter.appendChild(createInput("text", `${name}...`));
$(filter).insertAfter(`#${tableId}_filter`);
$(`#${filter.id} input`).on("keyup change clear", inputFunction);
return filter;
};
const getOrCreate = (parent, tagName, attributes = {}) =>
parent.querySelector(tagName) || parent.appendChild(createElement(tagName, attributes));
const createFooterIfMissing = table => getOrCreate(table, "tfoot");
const createFooterRowIfMissing = footer => getOrCreate(footer, "tr");
const getColumns = table => {
const headerOrBody = table.querySelector("thead, tbody");
const rows = headerOrBody.querySelectorAll("tr");
const headerRow = rows[0];
const headerCells = headerRow.querySelectorAll("th, td");
return [...headerCells].map((col, idx) => {
const name = col.innerText;
const type = col.getAttribute("data-type") || "string";
const values = [...rows].slice(1).map(row => {
const cells = row.querySelectorAll("th, td");
return cells[idx] ? cells[idx].innerText : '';
});
return { name, type, values };
});
};
const addMissingFooters = function (table) {
const footer = createFooterIfMissing(table);
const columns = getColumns(table);
const footerRow = createFooterRowIfMissing(footer);
const numMissing = columns.length - footer.querySelectorAll("th").length;
if (numMissing > 0) {
for (let i = 0; i < numMissing; i++) {
footerRow.appendChild(createElement("th", {}));
}
}
}
const initDataTable = function () {
const api = this.api();
api.columns().every(function () {
let column = this;
let header = $(column.header());
let title = header.text().trim();
// Create a wrapper div for the header content
let headerWrapper = $('<div class="header-wrapper"></div>');
// Create a span for the title and attach the sorting to it
let titleSpan = $(`<span class="header-title">${title}</span>`);
titleSpan.on('click', function (e) {
api.order([column.index(), 'asc']).draw();
});
// Create a div for the filter inputs
let filterDiv = $('<div class="header-filter"></div>');
let input = $('<input class="columnFilter" type="text" placeholder="Search..." />');
let regexToggle = $('<input type="checkbox" class="regexToggle" />');
let regexLabel = $('<label>RegEx</label>');
// Append elements to the filter div
filterDiv.append(input, regexToggle, regexLabel);
// Clear the header and append the new structure
header.empty().append(headerWrapper);
headerWrapper.append(titleSpan, filterDiv);
// Prevent event propagation for filter inputs
filterDiv.on('click', function (e) {
e.stopPropagation();
});
input.on("keyup change clear", function (e) {
e.stopPropagation();
let useRegex = regexToggle.prop('checked');
if (column.search() !== this.value) {
if (useRegex) {
column.search(this.value, true, false).draw();
} else {
column.search(this.value.replace(/\\*/, ".*"), true, false).draw();
}
}
});
regexToggle.on('change', function (e) {
e.stopPropagation();
input.trigger('keyup');
});
});
const body = $(api.table().body());
const tableId = api.table().node().id;
api.on("draw", () =>
body.unmark({
done: () => body.mark(api.search(), { separateWordSearch: false }),
})
);
$(createTableFilter(tableId, "Exclude", true, () => $(`#${tableId}`).DataTable().draw()));
$(createTableFilter(tableId, "Global", false, () => filterGlobal(tableId)));
addMissingFooters(api);
};
const intVal = i => {
switch (typeof i) {
case 'string': return i.replace(/[^(-?\d.?)]/g, '') * 1 || 0;
case 'number': return i;
default: return 0;
}
};
const addFooter = function (_row, data, _start, _end, _display) {
this.api().columns().every(function (idx) {
const filteredRows = this.rows({ search: 'applied' }).data().toArray().map(row => row[idx]);
const sum = filteredRows.reduce((a, b) => intVal(a) + intVal(b), 0);
$(this.footer()).html('Sum: ' + sum);
});
};
const getMaxColumns = (table) => {
const rows = table.querySelectorAll('tr');
return Math.max(...Array.from(rows).map(row =>
row.querySelectorAll('th, td').length
));
};
const normalizeTable = (table) => {
const maxCols = getMaxColumns(table);
// Find the first row, whether it's in thead or tbody
const firstRow = table.querySelector('tr');
let thead = table.querySelector('thead');
let tbody = table.querySelector('tbody');
// If there's no thead, create one and move the first row into it
if (!thead) {
thead = table.createTHead();
if (tbody && tbody.firstElementChild) {
thead.appendChild(tbody.firstElementChild);
} else {
thead.insertRow();
}
}
// Ensure there's at least one row in thead
let headerRow = thead.querySelector('tr');
if (!headerRow) {
headerRow = thead.insertRow();
}
// Normalize header
const existingHeaderCount = headerRow.cells.length;
for (let i = existingHeaderCount; i < maxCols; i++) {
const newCell = headerRow.insertCell();
// Only set generic header text if it's a newly created cell
if (i >= existingHeaderCount) {
newCell.textContent = `Column ${i + 1}`;
}
}
// Ensure tbody exists
if (!tbody) {
tbody = table.createTBody();
}
// Normalize body rows
const bodyRows = tbody.querySelectorAll('tr');
bodyRows.forEach(row => {
while (row.cells.length < maxCols) {
row.insertCell();
}
});
};
const createDataTable = (table) => {
try {
normalizeTable(table);
addMissingFooters(table);
$(table).DataTable({
paging: false,
colReorder: true,
mark: true,
dom: 'Bfrtip',
buttons: ["colvis"],
initComplete: initDataTable,
footerCallback: addFooter,
order: [],
});
} catch (error) {
console.error("Error initializing DataTable:", error);
}
};
document.querySelectorAll("table").forEach(createDataTable);
function filterGlobal(tableId) {
const filter = document.querySelector(`#${tableId}_global_filter input`);
const dataTable = $(`#${tableId}`).DataTable();
dataTable.search(filter.value, true, false).draw();
markSearch($(dataTable.table().body()), filter.value);
}
const markSearch = (body, value) =>
body.unmark({ done: () => body.markRegExp(new RegExp(value, "i"), { separateWordSearch: false }) });
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment