@logilab/sparqlexplorer 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +73 -0
  3. package/build/src/lib/App.d.ts +6 -0
  4. package/build/src/lib/components/ClassList.d.ts +9 -0
  5. package/build/src/lib/components/EndpointForm.d.ts +1 -0
  6. package/build/src/lib/components/GraphSelector.d.ts +1 -0
  7. package/build/src/lib/components/HomePage.d.ts +1 -0
  8. package/build/src/lib/components/SearchPage.d.ts +1 -0
  9. package/build/src/lib/components/UriPage.d.ts +1 -0
  10. package/build/src/lib/components/UtilsComponents.d.ts +11 -0
  11. package/build/src/lib/components/ViewSelector.d.ts +2 -0
  12. package/build/src/lib/components/Yasgui.d.ts +6 -0
  13. package/build/src/lib/components/YasrTableResults.d.ts +55 -0
  14. package/build/src/lib/components/layout/DrawerContent.d.ts +4 -0
  15. package/build/src/lib/components/layout/Footer.d.ts +1 -0
  16. package/build/src/lib/components/layout/Layout.d.ts +2 -0
  17. package/build/src/lib/components/layout/Navbar.d.ts +4 -0
  18. package/build/src/lib/components/uri/URIDefaultView.d.ts +6 -0
  19. package/build/src/lib/components/uri/URIWithSelectedView.d.ts +7 -0
  20. package/build/src/lib/context/AuthContext.d.ts +11 -0
  21. package/build/src/lib/context/ConfigContext.d.ts +15 -0
  22. package/build/src/lib/context/ViewsContext.d.ts +14 -0
  23. package/build/src/lib/hooks/useClasses.d.ts +6 -0
  24. package/build/src/lib/hooks/useGraphs.d.ts +8 -0
  25. package/build/src/lib/hooks/useNavigateWithParams.d.ts +5 -0
  26. package/build/src/lib/hooks/useParams.d.ts +1 -0
  27. package/build/src/lib/hooks/useURIData.d.ts +21 -0
  28. package/build/src/lib/hooks/useURILink.d.ts +1 -0
  29. package/build/src/lib/index.d.ts +8 -0
  30. package/build/src/lib/public-path.d.ts +1 -0
  31. package/build/src/lib/routes/Home.d.ts +1 -0
  32. package/build/src/lib/routes/Search.d.ts +1 -0
  33. package/build/src/lib/routes/Uri.d.ts +1 -0
  34. package/build/src/lib/routes/Yasgui.d.ts +1 -0
  35. package/build/src/lib/setupTests.d.ts +1 -0
  36. package/build/src/lib/utils/getIconFromURI.d.ts +3 -0
  37. package/build/src/lib/utils/utils.d.ts +24 -0
  38. package/build/src/lib/yasgui-utils/Storage.d.ts +16 -0
  39. package/build/src/lib/yasgui-utils/index.d.ts +16 -0
  40. package/build/static/js/lib.js +285 -0
  41. package/build/static/js/lib.js.LICENSE.txt +71 -0
  42. package/build/static/js/lib.js.map +1 -0
  43. package/package.json +73 -0
  44. package/src/app/index.css +23 -0
  45. package/src/app/index.tsx +28 -0
  46. package/src/app/templates/constants.js +1 -0
  47. package/src/app/templates/index.hbs +18 -0
  48. package/src/lib/App.css +83 -0
  49. package/src/lib/App.tsx +31 -0
  50. package/src/lib/components/ClassList.tsx +173 -0
  51. package/src/lib/components/EndpointForm.tsx +126 -0
  52. package/src/lib/components/GraphSelector.tsx +114 -0
  53. package/src/lib/components/HomePage.tsx +51 -0
  54. package/src/lib/components/SearchPage.tsx +211 -0
  55. package/src/lib/components/UriPage.tsx +158 -0
  56. package/src/lib/components/UtilsComponents.tsx +54 -0
  57. package/src/lib/components/ViewSelector.css +22 -0
  58. package/src/lib/components/ViewSelector.tsx +78 -0
  59. package/src/lib/components/Yasgui.tsx +127 -0
  60. package/src/lib/components/YasrTableResults.ts +529 -0
  61. package/src/lib/components/layout/DrawerContent.tsx +55 -0
  62. package/src/lib/components/layout/Footer.tsx +32 -0
  63. package/src/lib/components/layout/Layout.tsx +103 -0
  64. package/src/lib/components/layout/Navbar.tsx +231 -0
  65. package/src/lib/components/uri/URIDefaultView.tsx +392 -0
  66. package/src/lib/components/uri/URIWithSelectedView.tsx +31 -0
  67. package/src/lib/context/AuthContext.tsx +32 -0
  68. package/src/lib/context/ConfigContext.tsx +50 -0
  69. package/src/lib/context/ViewsContext.tsx +53 -0
  70. package/src/lib/hooks/useClasses.ts +48 -0
  71. package/src/lib/hooks/useGraphs.ts +67 -0
  72. package/src/lib/hooks/useNavigateWithParams.tsx +97 -0
  73. package/src/lib/hooks/useParams.tsx +8 -0
  74. package/src/lib/hooks/useURIData.ts +180 -0
  75. package/src/lib/hooks/useURILink.ts +7 -0
  76. package/src/lib/index.tsx +9 -0
  77. package/src/lib/public-path.ts +3 -0
  78. package/src/lib/routes/Home.tsx +13 -0
  79. package/src/lib/routes/Search.tsx +13 -0
  80. package/src/lib/routes/Uri.tsx +10 -0
  81. package/src/lib/routes/Yasgui.tsx +13 -0
  82. package/src/lib/setupTests.ts +5 -0
  83. package/src/lib/types.d.ts +6 -0
  84. package/src/lib/utils/getIconFromURI.ts +32 -0
  85. package/src/lib/utils/prefixInverted.json +2445 -0
  86. package/src/lib/utils/utils.ts +131 -0
  87. package/src/lib/yasgui-utils/Storage.ts +117 -0
  88. package/src/lib/yasgui-utils/index.ts +66 -0
@@ -0,0 +1,127 @@
1
+ import Yasgui from "@logilab/yasgui";
2
+ import type { Yasqe as YasqeType } from "@logilab/yasqe";
3
+ import React, { useRef } from "react";
4
+ import "@logilab/yasgui/build/yasgui.min.css";
5
+
6
+ import { useEndpoint } from "../context/ConfigContext";
7
+ import {
8
+ useNavigateWithParams,
9
+ useSearchParamsString,
10
+ } from "../hooks/useNavigateWithParams";
11
+ import { YasrTableResults } from "./YasrTableResults";
12
+
13
+ const Yasqe = Yasgui.Yasqe;
14
+ const Yasr = Yasgui.Yasr;
15
+
16
+ Yasr.registerPlugin(
17
+ "table",
18
+ // biome-ignore lint/suspicious/noExplicitAny: any is ok
19
+ YasrTableResults as any,
20
+ );
21
+
22
+ // @ts-expect-error TS(2322): Type '(_yasque: YasqeType, longLink: string) => Pr... Remove this comment to see the full error message
23
+ Yasqe.defaults.createShortLink = (_yasque: YasqeType, longLink: string) => {
24
+ return fetch("https://shlink.demo-dedibox.logilab.fr/rest/v3/short-urls", {
25
+ method: "POST",
26
+ body: JSON.stringify({
27
+ longUrl: longLink,
28
+ }),
29
+ headers: {
30
+ "Content-type": "application/json",
31
+ Accept: "application/json",
32
+ "X-Api-Key": "3f3a637f-5eed-49d2-a440-7b5f024e9104",
33
+ },
34
+ })
35
+ .then((response) => {
36
+ return response
37
+ .json()
38
+ .then((data) => {
39
+ return data.shortUrl;
40
+ })
41
+ .catch((err) => {
42
+ console.log(err);
43
+ return longLink;
44
+ });
45
+ })
46
+ .catch((err) => {
47
+ console.log(err);
48
+ return longLink;
49
+ });
50
+ };
51
+
52
+ const createShareableLink = (yasqe: YasqeType): string => {
53
+ const urlSplit = location.hash.split("?");
54
+ const hashString = urlSplit[0];
55
+ const searchString = urlSplit[1];
56
+ const searchParams = new URLSearchParams(searchString);
57
+ const currentQuery = yasqe.configToQueryParams().query as string;
58
+ searchParams.set("yasguiQuery", currentQuery);
59
+ const ret =
60
+ document.location.protocol +
61
+ "//" +
62
+ document.location.host +
63
+ document.location.pathname +
64
+ hashString +
65
+ "?" +
66
+ searchParams.toString();
67
+ return ret;
68
+ };
69
+
70
+ const consumeShareLink = (yasqe: YasqeType) => {
71
+ const searchString = location.hash.split("?")[1];
72
+ const searchParams = new URLSearchParams(searchString);
73
+ const yasguiQuery = searchParams.get("yasguiQuery");
74
+ if (yasguiQuery !== null) {
75
+ yasqe.setValue(yasguiQuery);
76
+ }
77
+ };
78
+
79
+ export interface YasguiModuleProps {
80
+ endpoint: string;
81
+ }
82
+
83
+ export function YasguiModule({ endpoint }: YasguiModuleProps) {
84
+ const yasgui = useRef<Yasgui | null>(null);
85
+ const getSearchParamsString = useSearchParamsString();
86
+
87
+ React.useEffect(() => {
88
+ const yasguiAnchor = document.getElementById("yasgui");
89
+ Yasr.plugins.table.defaults.urlParams = getSearchParamsString();
90
+
91
+ if (yasguiAnchor !== null) {
92
+ if (yasgui.current) {
93
+ yasgui.current.destroy();
94
+ }
95
+ const hash = location.hash;
96
+ yasgui.current = new Yasgui(yasguiAnchor, {
97
+ requestConfig: { endpoint: endpoint },
98
+ copyEndpointOnNewTab: false,
99
+ persistenceId: endpoint,
100
+ yasqe: {
101
+ createShareableLink,
102
+ // @ts-expect-error TS(2322): Type '(yasqe: YasqeType) => void' is not assignabl... Remove this comment to see the full error message
103
+ consumeShareLink,
104
+ requestConfig: { endpoint: endpoint },
105
+ },
106
+ });
107
+ // FIXME Yasgui tries to reset the hash for some reason
108
+ // So we save it and reasign here to prevent it from resetting the router
109
+ location.hash = hash;
110
+ }
111
+ }, [endpoint, getSearchParamsString]);
112
+
113
+ return <div id="yasgui" />;
114
+ }
115
+
116
+ export function YasguiPage() {
117
+ const navigate = useNavigateWithParams();
118
+
119
+ const endpoint = useEndpoint();
120
+
121
+ if (endpoint === null) {
122
+ navigate("/", undefined, true);
123
+ return null;
124
+ }
125
+
126
+ return <YasguiModule endpoint={endpoint} />;
127
+ }
@@ -0,0 +1,529 @@
1
+ /* eslint-disable no-unused-expressions */
2
+
3
+ import * as faTableIcon from "@fortawesome/free-solid-svg-icons/faTable";
4
+ import type Yasr from "@logilab/yasr";
5
+ import type { DownloadInfo, Parser, Plugin } from "@logilab/yasr";
6
+ import DataTable, {
7
+ type Api,
8
+ type Config,
9
+ type ConfigColumns,
10
+ } from "datatables.net";
11
+ import { cloneDeep, escape as ld_escape } from "lodash-es";
12
+ import type { DeepReadonly } from "ts-essentials";
13
+ import {
14
+ addClass,
15
+ drawFontAwesomeIconAsSvg,
16
+ drawSvgStringAsElement,
17
+ removeClass,
18
+ } from "../yasgui-utils/index";
19
+
20
+ import "@logilab/yasr/src/plugins/table/index.scss";
21
+ import "datatables.net-dt/css/jquery.dataTables.css";
22
+ import { computeRedirect } from "../utils/utils";
23
+
24
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
25
+ const ColumnResizer = require("column-resizer");
26
+ const DEFAULT_PAGE_SIZE = 50;
27
+
28
+ export interface PluginConfig {
29
+ openIriInNewWindow: boolean;
30
+ tableConfig: Config;
31
+ urlParams: string;
32
+ }
33
+
34
+ export interface PersistentConfig {
35
+ pageSize?: number;
36
+ compact?: boolean;
37
+ isEllipsed?: boolean;
38
+ }
39
+
40
+ type DataRow = [number, ...(Parser.BindingValue | "")[]];
41
+
42
+ function expand(this: HTMLDivElement, event: MouseEvent) {
43
+ addClass(this, "expanded");
44
+ event.preventDefault();
45
+ }
46
+
47
+ export class YasrTableResults implements Plugin<PluginConfig> {
48
+ private config: DeepReadonly<PluginConfig>;
49
+ private persistentConfig: PersistentConfig = {};
50
+ private yasr: Yasr;
51
+ private tableControls: Element | undefined;
52
+ private tableEl: HTMLTableElement | undefined;
53
+ // biome-ignore lint/suspicious/noExplicitAny: any is ok
54
+ private dataTable: Api<any> | undefined;
55
+ private tableFilterField: HTMLInputElement | undefined;
56
+ private tableSizeField: HTMLSelectElement | undefined;
57
+ private tableCompactSwitch: HTMLInputElement | undefined;
58
+ private tableEllipseSwitch: HTMLInputElement | undefined;
59
+ private tableResizer:
60
+ | {
61
+ reset: (options: {
62
+ disable: boolean;
63
+ onResize?: () => void;
64
+ partialRefresh?: boolean;
65
+ headerOnly?: boolean;
66
+ }) => void;
67
+ onResize: () => unknown;
68
+ }
69
+ | undefined;
70
+ public helpReference = "https://triply.cc/docs/yasgui#table";
71
+ public label = "Table";
72
+ public priority = 10;
73
+ public getIcon() {
74
+ return drawSvgStringAsElement(drawFontAwesomeIconAsSvg(faTableIcon));
75
+ }
76
+ constructor(yasr: Yasr) {
77
+ this.yasr = yasr;
78
+ //TODO read options from constructor
79
+ this.config = YasrTableResults.defaults;
80
+ }
81
+ public static defaults: PluginConfig = {
82
+ openIriInNewWindow: false,
83
+ tableConfig: {
84
+ dom: "tip", // tip: Table, Page Information and Pager, change to ipt for showing pagination on top
85
+ pageLength: DEFAULT_PAGE_SIZE, //default page length
86
+ lengthChange: true, //allow changing page length
87
+ data: [],
88
+ columns: [],
89
+ order: [],
90
+ deferRender: true,
91
+ orderClasses: false,
92
+ language: {
93
+ paginate: {
94
+ first: "&lt;&lt;", // Have to specify these two due to TS defs, <<
95
+ last: "&gt;&gt;", // Have to specify these two due to TS defs, >>
96
+ next: "&gt;", // >
97
+ previous: "&lt;", // <
98
+ },
99
+ },
100
+ },
101
+ urlParams: "",
102
+ };
103
+ private getRows(): DataRow[] {
104
+ if (!this.yasr.results) return [];
105
+ const bindings = this.yasr.results.getBindings();
106
+ if (!bindings) return [];
107
+ // Vars decide the columns
108
+ const vars = this.yasr.results.getVariables();
109
+ // Use "" as the empty value, undefined will throw runtime errors
110
+ return bindings.map((binding, rowId) => [
111
+ rowId + 1,
112
+ ...vars.map((variable) => binding[variable] ?? ""),
113
+ ]);
114
+ }
115
+
116
+ private getUriLinkFromBinding(
117
+ binding: Parser.BindingValue,
118
+ prefixes?: { [key: string]: string },
119
+ ) {
120
+ const href = binding.value;
121
+ let visibleString = href;
122
+ let prefixed = false;
123
+ if (prefixes) {
124
+ for (const prefixLabel in prefixes) {
125
+ if (visibleString.indexOf(prefixes[prefixLabel]) === 0) {
126
+ visibleString =
127
+ prefixLabel +
128
+ ":" +
129
+ href.substring(prefixes[prefixLabel].length);
130
+ prefixed = true;
131
+ break;
132
+ }
133
+ }
134
+ }
135
+ // Hide brackets when prefixed or compact
136
+ const hideBrackets = prefixed || this.persistentConfig.compact;
137
+ const location = window.location;
138
+ const params = computeRedirect(href, this.config.urlParams);
139
+ const pathname = location.pathname.substring(
140
+ 0,
141
+ location.pathname.length - 1,
142
+ ); // remove trailing slash
143
+ const sparqlExplorerHref = `${location.origin}${pathname}${params}`;
144
+ return `${hideBrackets ? "" : "&lt;"}<a class='iri' target='${
145
+ this.config.openIriInNewWindow ? "_blank" : "_self"
146
+ }'${
147
+ this.config.openIriInNewWindow ? " ref='noopener noreferrer'" : ""
148
+ } href='${sparqlExplorerHref}'>${visibleString}</a>${
149
+ hideBrackets ? "" : "&gt;"
150
+ }`;
151
+ }
152
+ private getCellContent(
153
+ binding: Parser.BindingValue,
154
+ prefixes?: { [label: string]: string },
155
+ ): string {
156
+ let content: string;
157
+ if (binding.type === "uri") {
158
+ content = `<span>${this.getUriLinkFromBinding(binding, prefixes)}</span>`;
159
+ } else {
160
+ content = `<span class='nonIri'>${this.formatLiteral(
161
+ binding,
162
+ prefixes,
163
+ )}</span>`;
164
+ }
165
+
166
+ return `<div>${content}</div>`;
167
+ }
168
+ private formatLiteral(
169
+ literalBinding: Parser.BindingValue,
170
+ prefixes?: { [key: string]: string },
171
+ ) {
172
+ let stringRepresentation = ld_escape(literalBinding.value);
173
+ // Return now when in compact mode.
174
+ if (this.persistentConfig.compact) return stringRepresentation;
175
+
176
+ if (literalBinding["xml:lang"]) {
177
+ stringRepresentation = `"${stringRepresentation}"<sup>@${literalBinding["xml:lang"]}</sup>`;
178
+ } else if (literalBinding.datatype) {
179
+ const dataType = this.getUriLinkFromBinding(
180
+ { type: "uri", value: literalBinding.datatype },
181
+ prefixes,
182
+ );
183
+ stringRepresentation = `"${stringRepresentation}"<sup>^^${dataType}</sup>`;
184
+ }
185
+ return stringRepresentation;
186
+ }
187
+
188
+ private getColumns(): ConfigColumns[] {
189
+ if (!this.yasr.results) return [];
190
+ const prefixes = this.yasr.getPrefixes();
191
+
192
+ return [
193
+ {
194
+ name: "",
195
+ searchable: false,
196
+ width: `${this.getSizeFirstColumn()}px`,
197
+ type: "num",
198
+ orderable: false,
199
+ visible: this.persistentConfig.compact !== true,
200
+ render: (data: number, type: unknown) =>
201
+ type === "filter" || type === "sort" || !type
202
+ ? data
203
+ : `<div class="rowNumber">${data}</div>`,
204
+ }, //prepend with row numbers column
205
+ ...this.yasr.results.getVariables().map((name) => {
206
+ return {
207
+ name: name,
208
+ title: name,
209
+ render: (data: Parser.BindingValue | "", type: unknown) => {
210
+ // Handle empty rows
211
+ if (data === "") return data;
212
+ if (type === "filter" || type === "sort" || !type)
213
+ return data.value;
214
+ return this.getCellContent(data, prefixes);
215
+ },
216
+ } as ConfigColumns;
217
+ }),
218
+ ];
219
+ }
220
+ private getSizeFirstColumn() {
221
+ const numResults = this.yasr.results?.getBindings()?.length || 0;
222
+ return numResults.toString().length * 8;
223
+ }
224
+
225
+ public draw(persistentConfig: PersistentConfig) {
226
+ this.persistentConfig = {
227
+ ...this.persistentConfig,
228
+ ...persistentConfig,
229
+ };
230
+ this.tableEl = document.createElement("table");
231
+ const rows = this.getRows();
232
+ const columns = this.getColumns();
233
+
234
+ if (rows.length <= (persistentConfig?.pageSize || DEFAULT_PAGE_SIZE)) {
235
+ // this.yasr.pluginControls;
236
+ addClass(this.yasr.rootEl, "isSinglePage");
237
+ } else {
238
+ removeClass(this.yasr.rootEl, "isSinglePage");
239
+ }
240
+
241
+ if (this.dataTable) {
242
+ this.destroyResizer();
243
+
244
+ this.dataTable.destroy(true);
245
+ this.dataTable = undefined;
246
+ }
247
+ this.yasr.resultsEl.appendChild(this.tableEl);
248
+ // reset some default config properties as they couldn't be initialized beforehand
249
+ const dtConfig: Config = {
250
+ ...(cloneDeep(this.config.tableConfig) as unknown as Config),
251
+ pageLength: persistentConfig?.pageSize
252
+ ? persistentConfig.pageSize
253
+ : DEFAULT_PAGE_SIZE,
254
+ data: rows,
255
+ columns: columns,
256
+ };
257
+ this.dataTable = new DataTable(this.tableEl, dtConfig);
258
+ this.tableEl.style.removeProperty("width");
259
+ this.tableEl.style.width = this.tableEl.clientWidth + "px";
260
+ const widths = Array.from(this.tableEl.querySelectorAll("th")).map(
261
+ (h) => h.offsetWidth - 26,
262
+ );
263
+ this.tableResizer = new ColumnResizer.default(this.tableEl, {
264
+ widths:
265
+ this.persistentConfig.compact === true
266
+ ? widths
267
+ : [this.getSizeFirstColumn(), ...widths.slice(1)],
268
+ partialRefresh: true,
269
+ onResize:
270
+ this.persistentConfig.isEllipsed !== false &&
271
+ this.setEllipsisHandlers,
272
+ headerOnly: true,
273
+ });
274
+ // DataTables uses the rendered style to decide the widths of columns.
275
+ // Before a draw remove the ellipseTable styling
276
+ if (this.persistentConfig.isEllipsed !== false) {
277
+ this.dataTable?.on("preDraw", () => {
278
+ this.tableResizer?.reset({ disable: true });
279
+ removeClass(this.tableEl, "ellipseTable");
280
+ this.tableEl?.style.removeProperty("width");
281
+ this.tableEl?.style.setProperty(
282
+ "width",
283
+ this.tableEl.clientWidth + "px",
284
+ );
285
+ return true; // Indicate it should re-render
286
+ });
287
+ // After a draw
288
+ this.dataTable?.on("draw", () => {
289
+ if (!this.tableEl) return;
290
+ // Width of table after render, removing width will make it fall back to 100%
291
+ let targetSize = this.tableEl.clientWidth;
292
+ this.tableEl.style.removeProperty("width");
293
+ // Let's make sure the new size is not bigger
294
+ if (targetSize > this.tableEl.clientWidth)
295
+ targetSize = this.tableEl.clientWidth;
296
+ this.tableEl?.style.setProperty("width", `${targetSize}px`);
297
+ // Enable the re-sizer
298
+ this.tableResizer?.reset({
299
+ disable: false,
300
+ partialRefresh: true,
301
+ onResize: this.setEllipsisHandlers,
302
+ headerOnly: true,
303
+ });
304
+ // Re-add the ellipsis
305
+ addClass(this.tableEl, "ellipseTable");
306
+ // Check if cells need the ellipsisHandlers
307
+ this.setEllipsisHandlers();
308
+ });
309
+ }
310
+
311
+ this.drawControls();
312
+ // Draw again but with the events
313
+ if (this.persistentConfig.isEllipsed !== false) {
314
+ addClass(this.tableEl, "ellipseTable");
315
+ this.setEllipsisHandlers();
316
+ }
317
+ // if (this.tableEl.clientWidth > width) this.tableEl.parentElement?.style.setProperty("overflow", "hidden");
318
+ }
319
+
320
+ private setEllipsisHandlers = () => {
321
+ /* biome-ignore lint/suspicious/useIterableCallbackReturn: return as shortcut */
322
+ this.dataTable?.cells({ page: "current" }).every((rowIdx, colIdx) => {
323
+ const cell = this.dataTable?.cell({ row: rowIdx, column: colIdx });
324
+ if (cell?.data() === "") return;
325
+ const cellNode = cell?.node() as HTMLTableCellElement;
326
+ if (cellNode) {
327
+ const content = cellNode.firstChild as HTMLDivElement;
328
+ if (
329
+ (content.firstElementChild?.getBoundingClientRect().width ||
330
+ 0) > content.getBoundingClientRect().width
331
+ ) {
332
+ if (!content.classList.contains("expandable")) {
333
+ addClass(content, "expandable");
334
+ content.addEventListener("click", expand, {
335
+ once: true,
336
+ });
337
+ }
338
+ } else {
339
+ if (content.classList.contains("expandable")) {
340
+ removeClass(content, "expandable");
341
+ content.removeEventListener("click", expand);
342
+ }
343
+ }
344
+ }
345
+ });
346
+ };
347
+ private handleTableSearch = (event: KeyboardEvent) => {
348
+ this.dataTable
349
+ ?.search((event.target as HTMLInputElement).value)
350
+ .draw("page");
351
+ };
352
+ private handleTableSizeSelect = (event: Event) => {
353
+ const pageLength = parseInt(
354
+ (event.target as HTMLSelectElement).value,
355
+ 10,
356
+ );
357
+ // Set page length
358
+ this.dataTable?.page.len(pageLength).draw("page");
359
+ // Store in persistentConfig
360
+ this.persistentConfig.pageSize = pageLength;
361
+ this.yasr.storePluginConfig("table", this.persistentConfig);
362
+ };
363
+ private handleSetCompactToggle = (event: Event) => {
364
+ // Store in persistentConfig
365
+ this.persistentConfig.compact = (
366
+ event.target as HTMLInputElement
367
+ ).checked;
368
+ // Update the table
369
+ this.draw(this.persistentConfig);
370
+ this.yasr.storePluginConfig("table", this.persistentConfig);
371
+ };
372
+ private handleSetEllipsisToggle = (event: Event) => {
373
+ // Store in persistentConfig
374
+ this.persistentConfig.isEllipsed = (
375
+ event.target as HTMLInputElement
376
+ ).checked;
377
+ // Update the table
378
+ this.draw(this.persistentConfig);
379
+ this.yasr.storePluginConfig("table", this.persistentConfig);
380
+ };
381
+ /**
382
+ * Draws controls on each update
383
+ */
384
+ drawControls() {
385
+ // Remove old header
386
+ this.removeControls();
387
+ this.tableControls = document.createElement("div");
388
+ this.tableControls.className = "tableControls";
389
+
390
+ // Compact switch
391
+ const toggleWrapper = document.createElement("div");
392
+ const switchComponent = document.createElement("label");
393
+ const textComponent = document.createElement("span");
394
+ textComponent.innerText = "Simple view";
395
+ addClass(textComponent, "label");
396
+ switchComponent.appendChild(textComponent);
397
+ addClass(switchComponent, "switch");
398
+ toggleWrapper.appendChild(switchComponent);
399
+ this.tableCompactSwitch = document.createElement("input");
400
+ switchComponent.addEventListener("change", this.handleSetCompactToggle);
401
+ this.tableCompactSwitch.type = "checkbox";
402
+ switchComponent.appendChild(this.tableCompactSwitch);
403
+ this.tableCompactSwitch.defaultChecked =
404
+ !!this.persistentConfig.compact;
405
+ this.tableControls.appendChild(toggleWrapper);
406
+
407
+ // Ellipsis switch
408
+ const ellipseToggleWrapper = document.createElement("div");
409
+ const ellipseSwitchComponent = document.createElement("label");
410
+ const ellipseTextComponent = document.createElement("span");
411
+ ellipseTextComponent.innerText = "Ellipse";
412
+ addClass(ellipseTextComponent, "label");
413
+ ellipseSwitchComponent.appendChild(ellipseTextComponent);
414
+ addClass(ellipseSwitchComponent, "switch");
415
+ ellipseToggleWrapper.appendChild(ellipseSwitchComponent);
416
+ this.tableEllipseSwitch = document.createElement("input");
417
+ ellipseSwitchComponent.addEventListener(
418
+ "change",
419
+ this.handleSetEllipsisToggle,
420
+ );
421
+ this.tableEllipseSwitch.type = "checkbox";
422
+ ellipseSwitchComponent.appendChild(this.tableEllipseSwitch);
423
+ this.tableEllipseSwitch.defaultChecked =
424
+ this.persistentConfig.isEllipsed !== false;
425
+ this.tableControls.appendChild(ellipseToggleWrapper);
426
+
427
+ // Create table filter
428
+ this.tableFilterField = document.createElement("input");
429
+ this.tableFilterField.className = "tableFilter";
430
+ this.tableFilterField.placeholder = "Filter query results";
431
+ this.tableFilterField.setAttribute(
432
+ "aria-label",
433
+ "Filter query results",
434
+ );
435
+ this.tableControls.appendChild(this.tableFilterField);
436
+ this.tableFilterField.addEventListener("keyup", this.handleTableSearch);
437
+
438
+ // Create page wrapper
439
+ const pageSizerWrapper = document.createElement("div");
440
+ pageSizerWrapper.className = "pageSizeWrapper";
441
+
442
+ // Create label for page size element
443
+ const pageSizerLabel = document.createElement("span");
444
+ pageSizerLabel.textContent = "Page size: ";
445
+ pageSizerLabel.className = "pageSizerLabel";
446
+ pageSizerWrapper.appendChild(pageSizerLabel);
447
+
448
+ // Create page size element
449
+ this.tableSizeField = document.createElement("select");
450
+ this.tableSizeField.className = "tableSizer";
451
+
452
+ // Create options for page sizer
453
+ const options = [10, 50, 100, 1000, -1];
454
+ for (const option of options) {
455
+ const element = document.createElement("option");
456
+ element.value = option + "";
457
+ // -1 selects everything so we should call it All
458
+ element.innerText = option > 0 ? option + "" : "All";
459
+ // Set initial one as selected
460
+ if (this.dataTable?.page.len() === option) element.selected = true;
461
+ this.tableSizeField.appendChild(element);
462
+ }
463
+ pageSizerWrapper.appendChild(this.tableSizeField);
464
+ this.tableSizeField.addEventListener(
465
+ "change",
466
+ this.handleTableSizeSelect,
467
+ );
468
+ this.tableControls.appendChild(pageSizerWrapper);
469
+ this.yasr.pluginControls.appendChild(this.tableControls);
470
+ }
471
+ download(filename?: string) {
472
+ return {
473
+ getData: () => this.yasr.results?.asCsv() || "",
474
+ contentType: "text/csv",
475
+ title: "Download result",
476
+ filename: `${filename || "queryResults"}.csv`,
477
+ } as DownloadInfo;
478
+ }
479
+
480
+ public canHandleResults() {
481
+ return (
482
+ !!this.yasr.results &&
483
+ this.yasr.results.getVariables() &&
484
+ this.yasr.results.getVariables().length > 0
485
+ );
486
+ }
487
+ private removeControls() {
488
+ // Unregister listeners and remove references to old fields
489
+ this.tableFilterField?.removeEventListener(
490
+ "keyup",
491
+ this.handleTableSearch,
492
+ );
493
+ this.tableFilterField = undefined;
494
+ this.tableSizeField?.removeEventListener(
495
+ "change",
496
+ this.handleTableSizeSelect,
497
+ );
498
+ this.tableSizeField = undefined;
499
+ this.tableCompactSwitch?.removeEventListener(
500
+ "change",
501
+ this.handleSetCompactToggle,
502
+ );
503
+ this.tableCompactSwitch = undefined;
504
+ this.tableEllipseSwitch?.removeEventListener(
505
+ "change",
506
+ this.handleSetEllipsisToggle,
507
+ );
508
+ this.tableEllipseSwitch = undefined;
509
+ // Empty controls
510
+ while (this.tableControls?.firstChild)
511
+ this.tableControls.firstChild.remove();
512
+ this.tableControls?.remove();
513
+ }
514
+ private destroyResizer() {
515
+ if (this.tableResizer) {
516
+ this.tableResizer.reset({ disable: true });
517
+ window.removeEventListener("resize", this.tableResizer.onResize);
518
+ this.tableResizer = undefined;
519
+ }
520
+ }
521
+ destroy() {
522
+ this.removeControls();
523
+ this.destroyResizer();
524
+ // According to datatables docs, destroy(true) will also remove all events
525
+ this.dataTable?.destroy(true);
526
+ this.dataTable = undefined;
527
+ removeClass(this.yasr.rootEl, "isSinglePage");
528
+ }
529
+ }