@@ -17,6 +17,7 @@ import * as faTableIcon from "@fortawesome/free-solid-svg-icons/faTable";
1717import { DeepReadonly } from "ts-essentials" ;
1818import Parser from "../parsers" ;
1919import { TableXResults } from "../TableXResults" ;
20+ import * as XLSX from "xlsx" ;
2021
2122const ColumnResizer = require ( "column-resizer" ) ;
2223const 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