Skip to content

Commit 1f1aed8

Browse files
committed
update on download button
1 parent e52538e commit 1f1aed8

File tree

1 file changed

+221
-35
lines changed
  • src/sparnatural-yasr-tablex-plugin

1 file changed

+221
-35
lines changed

src/sparnatural-yasr-tablex-plugin/index.ts

Lines changed: 221 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import * as faTableIcon from "@fortawesome/free-solid-svg-icons/faTable";
1717
import { DeepReadonly } from "ts-essentials";
1818
import Parser from "../parsers";
1919
import { TableXResults } from "../TableXResults";
20+
import * as XLSX from "xlsx";
2021

2122
const ColumnResizer = require("column-resizer");
2223
const DEFAULT_PAGE_SIZE = 50;
@@ -117,11 +118,14 @@ export class TableX implements Plugin<PluginConfig> {
117118
},
118119
excludeColumnsFromCompactView: [],
119120
uriHrefAdapter: undefined,
120-
lang: "en"
121+
lang: "en",
121122
};
123+
122124
private getRows(): DataRow[] {
123125
if (!this.results) return [];
124126
const bindings = this.results.getBindings();
127+
console.log("bindings :", bindings);
128+
console.log("yasr :", this.yasr.results);
125129
if (!bindings) return [];
126130
// Vars decide the columns
127131
const vars = this.results.getVariables();
@@ -199,7 +203,6 @@ export class TableX implements Plugin<PluginConfig> {
199203
return `<div>${content}</div>`;
200204
}
201205

202-
203206
private formatLiteral(
204207
literalBinding: Parser.BindingValue,
205208
prefixes?: { [key: string]: string }
@@ -213,48 +216,52 @@ export class TableX implements Plugin<PluginConfig> {
213216
} else if (literalBinding.datatype) {
214217
// ***** TableX MODIFICATION
215218

216-
if(
217-
literalBinding.datatype == "http://www.w3.org/2001/XMLSchema#date"
218-
) {
219+
if (literalBinding.datatype == "http://www.w3.org/2001/XMLSchema#date") {
219220
// format the date according to the locale
220221
const date = new Date(literalBinding.value);
221222
stringRepresentation = date.toLocaleDateString(this.config.lang);
222-
} else if(literalBinding.datatype == "http://www.w3.org/2001/XMLSchema#dateTime") {
223+
} else if (
224+
literalBinding.datatype == "http://www.w3.org/2001/XMLSchema#dateTime"
225+
) {
223226
// format the date according to the locale
224227
const date = new Date(literalBinding.value);
225228
stringRepresentation = date.toLocaleString(this.config.lang);
226-
} else if(literalBinding.datatype == "http://www.w3.org/2001/XMLSchema#gYear") {
229+
} else if (
230+
literalBinding.datatype == "http://www.w3.org/2001/XMLSchema#gYear"
231+
) {
227232
stringRepresentation = literalBinding.value;
228-
} else if(
229-
literalBinding.datatype == "http://www.w3.org/2001/XMLSchema#integer"
230-
|| literalBinding.datatype == "http://www.w3.org/2001/XMLSchema#int"
231-
|| literalBinding.datatype == "http://www.w3.org/2001/XMLSchema#long"
232-
|| literalBinding.datatype == "http://www.w3.org/2001/XMLSchema#float"
233-
|| literalBinding.datatype == "http://www.w3.org/2001/XMLSchema#double"
233+
} else if (
234+
literalBinding.datatype == "http://www.w3.org/2001/XMLSchema#integer" ||
235+
literalBinding.datatype == "http://www.w3.org/2001/XMLSchema#int" ||
236+
literalBinding.datatype == "http://www.w3.org/2001/XMLSchema#long" ||
237+
literalBinding.datatype == "http://www.w3.org/2001/XMLSchema#float" ||
238+
literalBinding.datatype == "http://www.w3.org/2001/XMLSchema#double"
234239
) {
235240
stringRepresentation = literalBinding.value;
236-
} else if(
241+
} else if (
237242
literalBinding.datatype == "http://www.w3.org/2001/XMLSchema#boolean"
238243
) {
239244
let translations = {
240-
"en": {
241-
"true": "True",
242-
"false": "False"
245+
en: {
246+
true: "True",
247+
false: "False",
243248
},
244-
"fr": {
245-
"true": "Vrai",
246-
"false": "Faux"
249+
fr: {
250+
true: "Vrai",
251+
false: "Faux",
247252
},
248-
"de": {
249-
"true": "Wahr",
250-
"false": "Falsch"
251-
}
252-
}
253-
stringRepresentation = ((this.config.lang && translations[this.config.lang]) || translations["en"])[literalBinding.value];
254-
if(stringRepresentation == undefined) {
253+
de: {
254+
true: "Wahr",
255+
false: "Falsch",
256+
},
257+
};
258+
stringRepresentation = ((this.config.lang &&
259+
translations[this.config.lang]) ||
260+
translations["en"])[literalBinding.value];
261+
if (stringRepresentation == undefined) {
255262
stringRepresentation = literalBinding.value;
256263
}
257-
} else if(
264+
} else if (
258265
literalBinding.datatype == "http://www.w3.org/2001/XMLSchema#string"
259266
) {
260267
stringRepresentation = literalBinding.value;
@@ -266,8 +273,6 @@ export class TableX implements Plugin<PluginConfig> {
266273
stringRepresentation = `"${stringRepresentation}"<sup>^^${dataType}</sup>`;
267274
}
268275

269-
270-
271276
// ***** end TableX MODIFICATION
272277
}
273278
return stringRepresentation;
@@ -294,7 +299,9 @@ export class TableX implements Plugin<PluginConfig> {
294299
return <DataTables.ColumnSettings>{
295300
name: name,
296301
title: name,
297-
visible: (this.persistentConfig.compact)?(this.config.excludeColumnsFromCompactView.indexOf(name) == -1):true,
302+
visible: this.persistentConfig.compact
303+
? this.config.excludeColumnsFromCompactView.indexOf(name) == -1
304+
: true,
298305
render: (
299306
data: Parser.BindingValue | "",
300307
type: any,
@@ -306,15 +313,14 @@ export class TableX implements Plugin<PluginConfig> {
306313
if (type === "filter" || type === "sort" || !type) {
307314
// ***** TableX MODIFICATION
308315
// for sorting : sort on label and not on URI
309-
if(data.type == "x-labelled-uri") {
316+
if (data.type == "x-labelled-uri") {
310317
return data.label;
311318
} else {
312319
return data.value;
313320
}
314321
// ***** end TableX MODIFICATION
315-
316322
}
317-
323+
318324
return this.getCellContent(data, prefixes);
319325
},
320326
};
@@ -560,7 +566,12 @@ export class TableX implements Plugin<PluginConfig> {
560566
this.tableControls.appendChild(pageSizerWrapper);
561567
this.yasr.pluginControls.appendChild(this.tableControls);
562568
}
563-
download(filename?: string) {
569+
570+
// download method to provide custom download functionality
571+
download(filename?: string): DownloadInfo | undefined {
572+
// Setup the download handler on the download button
573+
this._setupDownloadHandler();
574+
564575
return {
565576
getData: () => this.yasr.results?.asCsv() || "",
566577
contentType: "text/csv",
@@ -569,6 +580,179 @@ export class TableX implements Plugin<PluginConfig> {
569580
} as DownloadInfo;
570581
}
571582

583+
private _downloadHandlerSetup = false;
584+
585+
// Méthode pour configurer le gestionnaire de clic sur le bouton de téléchargement
586+
private _setupDownloadHandler() {
587+
if (this._downloadHandlerSetup) return;
588+
589+
setTimeout(() => {
590+
const downloadBtn = document.querySelector(".yasr_downloadIcon");
591+
if (!downloadBtn) return;
592+
593+
const newBtn = downloadBtn.cloneNode(true) as HTMLElement;
594+
downloadBtn.parentNode?.replaceChild(newBtn, downloadBtn);
595+
596+
newBtn.classList.add("download-enabled");
597+
newBtn.title = "Télécharger les résultats";
598+
599+
newBtn.addEventListener("click", (e) => {
600+
e.preventDefault();
601+
e.stopPropagation();
602+
this._showFormatMenu(newBtn);
603+
});
604+
605+
this._downloadHandlerSetup = true;
606+
}, 100);
607+
}
608+
609+
// Menu de choix du format de téléchargement
610+
private _showFormatMenu(button: HTMLElement) {
611+
// Supprimer un ancien menu s'il existe
612+
document.querySelector(".format-menu")?.remove();
613+
614+
const menu = document.createElement("div");
615+
menu.className = "format-menu";
616+
menu.style.cssText = `
617+
position: fixed;
618+
background: white;
619+
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
620+
padding: 5px 0;
621+
border-radius: 4px;
622+
z-index: 10000;
623+
`;
624+
625+
const formats = [
626+
{ label: "CSV", format: "csv" },
627+
{ label: "XLSX", format: "xlsx" },
628+
{ label: "ODS", format: "ods" },
629+
];
630+
631+
// Construire les éléments du menu
632+
formats.forEach((item) => {
633+
const option = document.createElement("div");
634+
option.textContent = item.label;
635+
option.style.cssText = `
636+
padding: 6px 15px;
637+
cursor: pointer;
638+
white-space: nowrap;
639+
`;
640+
option.addEventListener(
641+
"mouseover",
642+
() => (option.style.background = "#f0f0f0")
643+
);
644+
option.addEventListener(
645+
"mouseout",
646+
() => (option.style.background = "transparent")
647+
);
648+
option.addEventListener("click", (e) => {
649+
e.stopPropagation();
650+
cleanup();
651+
this._downloadData(item.format as "csv" | "xlsx" | "ods");
652+
});
653+
menu.appendChild(option);
654+
});
655+
656+
document.body.appendChild(menu);
657+
658+
// Position initiale sous le bouton
659+
const rect = button.getBoundingClientRect();
660+
menu.style.top = `${rect.bottom + 6}px`;
661+
menu.style.left = `${rect.left}px`;
662+
663+
// Ajustement si le menu dépasse l'écran
664+
const menuRect = menu.getBoundingClientRect();
665+
const overflowRight = menuRect.right - window.innerWidth;
666+
const overflowLeft = menuRect.left;
667+
668+
if (overflowRight > 0) {
669+
const adjustedLeft = rect.left - overflowRight - 10; // marge 10px
670+
menu.style.left = `${Math.max(10, adjustedLeft)}px`;
671+
} else if (overflowLeft < 0) {
672+
menu.style.left = `10px`;
673+
}
674+
675+
// Fonction interne pour tout nettoyer
676+
const cleanup = () => {
677+
menu.remove();
678+
document.removeEventListener("click", handleClickOutside);
679+
window.removeEventListener("scroll", handleScroll);
680+
};
681+
682+
// Fermer si clic en dehors
683+
const handleClickOutside = (e: MouseEvent) => {
684+
if (!menu.contains(e.target as Node) && e.target !== button) cleanup();
685+
};
686+
document.addEventListener("click", handleClickOutside);
687+
688+
// Fermer au premier scroll
689+
const handleScroll = () => cleanup();
690+
window.addEventListener("scroll", handleScroll, { once: true });
691+
}
692+
693+
// Télécharge le fichier dans le format choisi
694+
private _downloadData(format: "csv" | "xlsx" | "ods" = "xlsx") {
695+
if (!this.results) {
696+
alert("Aucun résultat à exporter.");
697+
return;
698+
}
699+
700+
const filename = "queryResults";
701+
const workbook = this._createWorkbook();
702+
703+
// Conversion via SheetJS
704+
const wbout = XLSX.write(workbook, { bookType: format, type: "array" });
705+
const blob = new Blob([wbout]);
706+
const url = URL.createObjectURL(blob);
707+
708+
// Lancer le téléchargement
709+
const link = document.createElement("a");
710+
link.href = url;
711+
link.download = `${filename}.${format}`;
712+
document.body.appendChild(link);
713+
link.click();
714+
document.body.removeChild(link);
715+
716+
setTimeout(() => URL.revokeObjectURL(url), 100);
717+
}
718+
719+
// Génère le classeur Excel a partir Yasr
720+
private _createWorkbook(): XLSX.WorkBook {
721+
const workbook = XLSX.utils.book_new();
722+
723+
// Récupération des résultats YASR
724+
const yasrResults = this.yasr.results;
725+
if (!yasrResults) return workbook;
726+
727+
// Récupération des colonnes et lignes via les méthodes publiques du Parser
728+
const vars = yasrResults.getVariables() || [];
729+
const bindings = yasrResults.getBindings() || [];
730+
731+
// Préparation du tableau pour SheetJS
732+
const data: any[][] = [vars];
733+
734+
bindings.forEach((binding: any) => {
735+
const row: any[] = [];
736+
vars.forEach((variable: string) => {
737+
const cell = binding[variable];
738+
if (!cell) {
739+
row.push("");
740+
} else {
741+
// On prend value
742+
row.push(cell.value || "");
743+
}
744+
});
745+
746+
data.push(row);
747+
});
748+
749+
// Génération de la feuille Excel
750+
const sheet = XLSX.utils.aoa_to_sheet(data);
751+
XLSX.utils.book_append_sheet(workbook, sheet, "Results");
752+
753+
return workbook;
754+
}
755+
572756
public canHandleResults() {
573757
return (
574758
!!this.yasr.results &&
@@ -601,13 +785,15 @@ export class TableX implements Plugin<PluginConfig> {
601785
this.tableControls.firstChild.remove();
602786
this.tableControls?.remove();
603787
}
788+
604789
private destroyResizer() {
605790
if (this.tableResizer) {
606791
this.tableResizer.reset({ disable: true });
607792
window.removeEventListener("resize", this.tableResizer.onResize);
608793
this.tableResizer = undefined;
609794
}
610795
}
796+
611797
destroy() {
612798
this.removeControls();
613799
this.destroyResizer();

0 commit comments

Comments
 (0)