Last active
June 23, 2024 14:39
-
-
Save rf5860/aeb462e00fc2b511f60efdcd0a1c0776 to your computer and use it in GitHub Desktop.
Allows searching, column re-ordering, and column removal to any table
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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