Skip to content

Commit db6db13

Browse files
committed
Move the grid view filter into a separate component
1 parent 4b8bafe commit db6db13

File tree

2 files changed

+138
-91
lines changed

2 files changed

+138
-91
lines changed

ts/WoltLabSuite/Core/Component/GridView.ts

Lines changed: 20 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,22 @@ import { getRow } from "../Api/Gridviews/GetRow";
22
import { getRows } from "../Api/Gridviews/GetRows";
33
import DomChangeListener from "../Dom/Change/Listener";
44
import DomUtil from "../Dom/Util";
5-
import { promiseMutex } from "../Helper/PromiseMutex";
65
import { wheneverFirstSeen } from "../Helper/Selector";
76
import UiDropdownSimple from "../Ui/Dropdown/Simple";
8-
import { dialogFactory } from "./Dialog";
7+
import Filter from "./GridView/Filter";
98

109
export class GridView {
10+
readonly #filter: Filter;
1111
readonly #gridClassName: string;
1212
readonly #table: HTMLTableElement;
1313
readonly #pagination: WoltlabCorePaginationElement;
1414
readonly #baseUrl: string;
15-
readonly #filterButton: HTMLButtonElement;
16-
readonly #filterPills: HTMLElement;
1715
readonly #noItemsNotice: HTMLElement;
1816
#pageNo: number;
1917
#sortField: string;
2018
#sortOrder: string;
2119
#defaultSortField: string;
2220
#defaultSortOrder: string;
23-
#filters: Map<string, string>;
2421
#gridViewParameters?: Map<string, string>;
2522

2623
constructor(
@@ -35,8 +32,6 @@ export class GridView {
3532
this.#gridClassName = gridClassName;
3633
this.#table = document.getElementById(`${gridId}_table`) as HTMLTableElement;
3734
this.#pagination = document.getElementById(`${gridId}_pagination`) as WoltlabCorePaginationElement;
38-
this.#filterButton = document.getElementById(`${gridId}_filterButton`) as HTMLButtonElement;
39-
this.#filterPills = document.getElementById(`${gridId}_filters`) as HTMLElement;
4035
this.#noItemsNotice = document.getElementById(`${gridId}_noItemsNotice`) as HTMLElement;
4136
this.#pageNo = pageNo;
4237
this.#baseUrl = baseUrl;
@@ -49,9 +44,8 @@ export class GridView {
4944
this.#initPagination();
5045
this.#initSorting();
5146
this.#initInteractions();
52-
this.#initFilters();
47+
this.#filter = this.#setupFilter(gridId);
5348
this.#initEventListeners();
54-
this.#initSelectCheckboxes();
5549

5650
window.addEventListener("popstate", () => {
5751
this.#handlePopState();
@@ -113,7 +107,7 @@ export class GridView {
113107
this.#pageNo,
114108
this.#sortField,
115109
this.#sortOrder,
116-
this.#filters,
110+
this.#filter.getActiveFilters(),
117111
this.#gridViewParameters,
118112
)
119113
).unwrap();
@@ -129,7 +123,7 @@ export class GridView {
129123

130124
DomChangeListener.trigger();
131125

132-
this.#renderFilters(response.filterLabels);
126+
this.#filter.setFilterLabels(response.filterLabels);
133127
}
134128

135129
async #refreshRow(row: HTMLElement): Promise<void> {
@@ -153,11 +147,10 @@ export class GridView {
153147
parameters.push(["sortField", this.#sortField]);
154148
parameters.push(["sortOrder", this.#sortOrder]);
155149
}
156-
if (this.#filters) {
157-
this.#filters.forEach((value, key) => {
158-
parameters.push([`filters[${key}]`, value]);
159-
});
160-
}
150+
151+
this.#filter.getActiveFilters().forEach((value, key) => {
152+
parameters.push([`filters[${key}]`, value]);
153+
});
161154

162155
if (parameters.length > 0) {
163156
url.search += url.search !== "" ? "&" : "?";
@@ -189,84 +182,11 @@ export class GridView {
189182
});
190183
}
191184

192-
#initFilters(): void {
193-
if (!this.#filterButton) {
194-
return;
195-
}
196-
197-
this.#filterButton.addEventListener(
198-
"click",
199-
promiseMutex(() => this.#showFilterDialog()),
200-
);
201-
202-
if (!this.#filterPills) {
203-
return;
204-
}
205-
206-
const filterButtons = this.#filterPills.querySelectorAll<HTMLButtonElement>("[data-filter]");
207-
if (!filterButtons.length) {
208-
return;
209-
}
210-
211-
this.#filters = new Map<string, string>();
212-
filterButtons.forEach((button) => {
213-
this.#filters.set(button.dataset.filter!, button.dataset.filterValue!);
214-
button.addEventListener("click", () => {
215-
this.#removeFilter(button.dataset.filter!);
216-
});
217-
});
218-
}
219-
220-
async #showFilterDialog(): Promise<void> {
221-
const url = new URL(this.#filterButton.dataset.endpoint!);
222-
if (this.#filters) {
223-
this.#filters.forEach((value, key) => {
224-
url.searchParams.set(`filters[${key}]`, value);
225-
});
226-
}
227-
228-
const { ok, result } = await dialogFactory().usingFormBuilder().fromEndpoint(url.toString());
229-
230-
if (ok) {
231-
this.#filters = new Map(Object.entries(result as ArrayLike<string>));
232-
this.#switchPage(1);
233-
}
234-
}
235-
236-
#renderFilters(labels: ArrayLike<string>): void {
237-
if (!this.#filterPills) {
238-
return;
239-
}
240-
this.#filterPills.innerHTML = "";
241-
if (!this.#filters) {
242-
return;
243-
}
244-
245-
this.#filters.forEach((value, key) => {
246-
const button = document.createElement("button");
247-
button.type = "button";
248-
button.classList.add("button", "small");
249-
const icon = document.createElement("fa-icon");
250-
icon.setIcon("circle-xmark");
251-
button.append(icon, labels[key]);
252-
button.addEventListener("click", () => {
253-
this.#removeFilter(key);
254-
});
255-
256-
this.#filterPills.append(button);
257-
});
258-
}
259-
260-
#removeFilter(filter: string): void {
261-
this.#filters.delete(filter);
262-
this.#switchPage(1);
263-
}
264-
265185
#handlePopState(): void {
266186
let pageNo = 1;
267187
this.#sortField = this.#defaultSortField;
268188
this.#sortOrder = this.#defaultSortOrder;
269-
this.#filters = new Map<string, string>();
189+
this.#filter.resetFilters();
270190

271191
const url = new URL(window.location.href);
272192
url.searchParams.forEach((value, key) => {
@@ -285,7 +205,7 @@ export class GridView {
285205

286206
const matches = key.match(/^filters\[([a-z0-9_]+)\]$/i);
287207
if (matches) {
288-
this.#filters.set(matches[1], value);
208+
this.#filter.setFilter(matches[1], value);
289209
}
290210
});
291211

@@ -301,4 +221,13 @@ export class GridView {
301221
(event.target as HTMLElement).remove();
302222
});
303223
}
224+
225+
#setupFilter(gridId: string): Filter {
226+
const filter = new Filter(gridId);
227+
filter.addEventListener("switchPage", (event) => {
228+
this.#switchPage(event.detail.pageNo);
229+
});
230+
231+
return filter;
232+
}
304233
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { promiseMutex } from "../../Helper/PromiseMutex";
2+
import { dialogFactory } from "../Dialog";
3+
4+
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
5+
export class Filter extends EventTarget {
6+
readonly #filterButton: HTMLButtonElement | null;
7+
readonly #filterPills: HTMLElement | null;
8+
#filters: Map<string, string> = new Map();
9+
10+
constructor(gridId: string) {
11+
super();
12+
13+
this.#filterButton = document.getElementById(`${gridId}_filterButton`) as HTMLButtonElement;
14+
this.#filterPills = document.getElementById(`${gridId}_filters`) as HTMLElement;
15+
16+
this.#setupEventListeners();
17+
}
18+
19+
resetFilters(): void {
20+
this.#filters.clear();
21+
}
22+
23+
setFilter(key: string, value: string): void {
24+
this.#filters.set(key, value);
25+
}
26+
27+
getActiveFilters(): Map<string, string> {
28+
return new Map(this.#filters);
29+
}
30+
31+
setFilterLabels(labels: ArrayLike<string>): void {
32+
if (this.#filterPills === null) {
33+
return;
34+
}
35+
36+
this.#filterPills.innerHTML = "";
37+
if (this.#filters.size === 0) {
38+
return;
39+
}
40+
41+
for (const key of this.#filters.keys()) {
42+
const button = document.createElement("button");
43+
button.type = "button";
44+
button.classList.add("button", "small");
45+
const icon = document.createElement("fa-icon");
46+
icon.setIcon("circle-xmark");
47+
button.append(icon, labels[key]);
48+
button.addEventListener("click", () => {
49+
this.#removeFilter(key);
50+
});
51+
52+
this.#filterPills.append(button);
53+
}
54+
}
55+
56+
#setupEventListeners(): void {
57+
if (this.#filterButton === null) {
58+
return;
59+
}
60+
61+
this.#filterButton.addEventListener(
62+
"click",
63+
promiseMutex(() => this.#showFilterDialog()),
64+
);
65+
66+
if (this.#filterPills === null) {
67+
return;
68+
}
69+
70+
const filterButtons = this.#filterPills.querySelectorAll<HTMLButtonElement>("[data-filter]");
71+
filterButtons.forEach((button) => {
72+
this.#filters.set(button.dataset.filter!, button.dataset.filterValue!);
73+
button.addEventListener("click", () => {
74+
this.#removeFilter(button.dataset.filter!);
75+
});
76+
});
77+
}
78+
79+
async #showFilterDialog(): Promise<void> {
80+
const url = new URL(this.#filterButton!.dataset.endpoint!);
81+
if (this.#filters) {
82+
this.#filters.forEach((value, key) => {
83+
url.searchParams.set(`filters[${key}]`, value);
84+
});
85+
}
86+
87+
const { ok, result } = await dialogFactory().usingFormBuilder().fromEndpoint(url.toString());
88+
89+
if (ok) {
90+
this.#filters = new Map(Object.entries(result as ArrayLike<string>));
91+
92+
this.dispatchEvent(new CustomEvent("switchPage", { detail: { pageNo: 1 } }));
93+
}
94+
}
95+
96+
#removeFilter(filter: string): void {
97+
this.#filters.delete(filter);
98+
99+
this.dispatchEvent(new CustomEvent("switchPage", { detail: { pageNo: 1 } }));
100+
}
101+
}
102+
103+
interface FilterEventMap {
104+
switchPage: CustomEvent<{ pageNo: number }>;
105+
}
106+
107+
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
108+
export interface Filter extends EventTarget {
109+
addEventListener: {
110+
<T extends keyof FilterEventMap>(
111+
type: T,
112+
listener: (this: Filter, ev: FilterEventMap[T]) => any,
113+
options?: boolean | AddEventListenerOptions,
114+
): void;
115+
} & HTMLElement["addEventListener"];
116+
}
117+
118+
export default Filter;

0 commit comments

Comments
 (0)