1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

implement table filtering on generic object list

This commit is contained in:
checktheroads
2021-04-20 12:45:30 -07:00
parent d171e781d2
commit acca69a8a9
18 changed files with 197 additions and 84 deletions

View File

@ -1,4 +1,5 @@
import { getElements } from './util';
import debounce from 'just-debounce-it';
import { getElements, getRowValues, findFirstAdjacent } from './util';
interface SearchFilterButton extends EventTarget {
dataset: { searchValue: string };
@ -42,20 +43,21 @@ function initSearchBar() {
* Initialize Interface Table Filter Elements.
*/
function initInterfaceFilter() {
for (const element of getElements<HTMLInputElement>('input.interface-filter')) {
for (const input of getElements<HTMLInputElement>('input.interface-filter')) {
const table = findFirstAdjacent<HTMLTableElement>(input, 'table');
const rows = Array.from(
table?.querySelectorAll<HTMLTableRowElement>('tbody > tr') ?? [],
).filter(r => r !== null);
/**
* Filter on-page table by input text.
*/
function handleInput(event: Event) {
const target = event.target as HTMLInputElement;
// Create a regex pattern from the input search text to match against.
const filter = new RegExp(target.value);
const filter = new RegExp(target.value.toLowerCase().trim());
// Each row represents an interface and its attributes.
for (const row of getElements<HTMLTableRowElement>('table > tbody > tr')) {
// The data-name attribute's value contains the interface name.
const name = row.getAttribute('data-name');
for (const row of rows) {
// Find the row's checkbox and deselect it, so that it is not accidentally included in form
// submissions.
const checkBox = row.querySelector<HTMLInputElement>('input[type="checkbox"][name="pk"]');
@ -63,8 +65,11 @@ function initInterfaceFilter() {
checkBox.checked = false;
}
// The data-name attribute's value contains the interface name.
const name = row.getAttribute('data-name');
if (typeof name === 'string') {
if (filter.test(name)) {
if (filter.test(name.toLowerCase().trim())) {
// If this row matches the search pattern, but is already hidden, unhide it.
if (row.classList.contains('d-none')) {
row.classList.remove('d-none');
@ -76,12 +81,57 @@ function initInterfaceFilter() {
}
}
}
element.addEventListener('keyup', handleInput);
input.addEventListener('keyup', debounce(handleInput, 300));
}
}
function initTableFilter() {
for (const input of getElements<HTMLInputElement>('input.object-filter')) {
// Find the first adjacent table element.
const table = findFirstAdjacent<HTMLTableElement>(input, 'table');
// Build a valid array of <tr/> elements that are children of the adjacent table.
const rows = Array.from(
table?.querySelectorAll<HTMLTableRowElement>('tbody > tr') ?? [],
).filter(r => r !== null);
/**
* Filter table rows by matched input text.
* @param event
*/
function handleInput(event: Event) {
const target = event.target as HTMLInputElement;
// Create a regex pattern from the input search text to match against.
const filter = new RegExp(target.value.toLowerCase().trim());
for (const row of rows) {
// Find the row's checkbox and deselect it, so that it is not accidentally included in form
// submissions.
const checkBox = row.querySelector<HTMLInputElement>('input[type="checkbox"][name="pk"]');
if (checkBox !== null) {
checkBox.checked = false;
}
// Iterate through each row's cell values
for (const value of getRowValues(row)) {
if (filter.test(value.toLowerCase())) {
// If this row matches the search pattern, but is already hidden, unhide it and stop
// iterating through the rest of the cells.
row.classList.remove('d-none');
break;
} else {
// If none of the cells in this row match the search pattern, hide the row.
row.classList.add('d-none');
}
}
}
}
input.addEventListener('keyup', debounce(handleInput, 300));
}
}
export function initSearch() {
for (const func of [initSearchBar, initInterfaceFilter]) {
for (const func of [initSearchBar, initTableFilter, initInterfaceFilter]) {
func();
}
}

View File

@ -5,6 +5,16 @@ type Method = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
type ReqData = URLSearchParams | Dict | undefined | unknown;
type SelectedOption = { name: string; options: string[] };
// interface TableValue {
// row: {
// element: HTMLTableRowElement;
// };
// cell: {
// element: HTMLTableCellElement;
// value: string;
// };
// }
export function isApiError(data: Record<string, unknown>): data is APIError {
return 'error' in data && 'exception' in data;
}
@ -202,3 +212,41 @@ export function toggleLoader(action: 'show' | 'hide') {
}
}
}
/**
* Get the value of every cell in a table.
* @param table Table Element
*/
export function* getRowValues(table: HTMLTableRowElement): Generator<string> {
for (const element of table.querySelectorAll<HTMLTableCellElement>('td')) {
if (element !== null) {
if (isTruthy(element.innerText) && element.innerText !== '—') {
yield element.innerText.replaceAll(/[\n\r]/g, '').trim();
}
}
}
}
/**
* Recurse upward through an element's siblings until an element matching the query is found.
*
* @param base Base Element
* @param query CSS Query
*/
export function findFirstAdjacent<R extends HTMLElement, B extends Element = Element>(
base: B,
query: string,
): Nullable<R> {
function match<P extends Element | null>(parent: P): Nullable<R> {
if (parent !== null && parent.parentElement !== null) {
for (const child of parent.parentElement.querySelectorAll<R>(query)) {
if (child !== null) {
return child;
}
}
return match(parent.parentElement.parentElement);
}
return null;
}
return match(base);
}