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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -452,3 +452,8 @@ div.card-overlay {
color: $secondary;
}
}
div.card > div.card-header > div.table-controls {
max-width: 25%;
width: 100%;
}

View File

@@ -19,6 +19,7 @@
"flatpickr": "4.6.3",
"jquery": "3.5.1",
"jquery-ui": "1.12.1",
"just-debounce-it": "^1.4.0",
"masonry-layout": "^4.2.2",
"parcel-bundler": "1.12.3",
"query-string": "^6.14.1",

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);
}

View File

@@ -4715,6 +4715,11 @@ jsprim@^1.2.2:
json-schema "0.2.3"
verror "1.10.0"
just-debounce-it@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/just-debounce-it/-/just-debounce-it-1.4.0.tgz#02c8c95a1bdb70697e72e37fa64ca8689c10e78c"
integrity sha512-D6wp9toCJ77OAL8AvY+fgcNLlR9NC4HKnz6yx6r/IrOFcuDYdqk+P9asMg9nTLYT24Wpu1sT0lukDES6uvQvqA==
kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
version "3.2.2"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"