const Papa = require("papaparse"); const errors = require("./errors.js"); const { compare }= require('natural-orderby'); import { papaParseDatas, papaParseErrors, papaParseMeta } from "./papaParseInterfaces"; import { domElement, pagination, selectors, sortingColumns, sortingFunctions } from "./freeDatas2HTMLInterfaces"; export class freeDatas2HTML { private _datasViewElt: domElement = { id:"", eltDOM:undefined }; public datasHTML: string = ""; // Revoir car tous les attributs suivants sont liés aux colonnes/fields des données (créer une classe ?) private _datasSelectors: selectors[] = []; private _datasSortingColumns: sortingColumns[] = []; private _datasSortedColumn: sortingColumns|undefined; private _datasSortingFunctions: sortingFunctions[] = []; // Parseur fichier : private _datasSourceUrl: string = ""; public parseMeta: papaParseMeta|undefined = undefined; public parseDatas: papaParseDatas[] = []; public parseErrors: papaParseErrors[] = []; public stopIfParseErrors: boolean = false; // Pagination : private _pagination: pagination|undefined; public static isPositiveInteger(nb: number) { return (Number.isInteger(nb) === false || nb <= 0) ? false : true; } public static isNaturalNumber(nb: number) { return (Number.isInteger(nb) === false || nb < 0) ? false : true; } set datasViewElt(elt: domElement) { let checkContainerExist=document.getElementById(elt.id); if(checkContainerExist === null) throw new Error(errors.elementNotFound+elt.id); else { this._datasViewElt.id=elt.id; this._datasViewElt.eltDOM=checkContainerExist; } } set datasSourceUrl(url: string) { if(url.trim().length === 0) throw new Error(errors.needUrl); else this._datasSourceUrl=url.trim(); } set datasSelectors(selectionElts: selectors[]) { this._datasSelectors=[]; let checkContainerExist: HTMLElement|null; for(let i in selectionElts) { checkContainerExist=document.getElementById(selectionElts[i].id); if(checkContainerExist === null) console.error(errors.elementNotFound+selectionElts[i].id); else if(freeDatas2HTML.isNaturalNumber(selectionElts[i].datasFieldNb) === false) console.error(errors.needNaturalNumber); else { selectionElts[i].eltDOM=checkContainerExist; if(selectionElts[i].separator !== undefined && selectionElts[i].separator === "") selectionElts[i].separator=undefined; this._datasSelectors.push(selectionElts[i]); } } } get datasSelectors() : selectors[] { return this._datasSelectors; } set datasSortingColumns(sortingColumns: sortingColumns[]) { this._datasSortingColumns=[]; for(let i = 0; i < sortingColumns.length; i++) { if(freeDatas2HTML.isNaturalNumber(sortingColumns[i].datasFieldNb) === false) console.error(errors.needNaturalNumber); else { sortingColumns[i].order=undefined; this._datasSortingColumns.push(sortingColumns[i]); } } } get datasSortingColumns() : sortingColumns[] { return this._datasSortingColumns; } // Attention : une fonction de classement peut aussi bien servir à une colonne triable, qu'à une colonne servant à filtrer les données set datasSortingFunctions(sortingFunctions: sortingFunctions[]) { this._datasSortingFunctions=[]; for(let i = 0; i < sortingFunctions.length; i++) { if(freeDatas2HTML.isNaturalNumber(sortingFunctions[i].datasFieldNb) === false) console.error(errors.needNaturalNumber); else this._datasSortingFunctions.push(sortingFunctions[i]); } } // Retourne la fonction spécifique de classement associée à une colonne public getSortingFunctionForField(datasFieldNb: number): sortingFunctions|undefined { for(let i in this._datasSortingFunctions) { if(this._datasSortingFunctions[i].datasFieldNb === datasFieldNb) return this._datasSortingFunctions[i]; } return undefined; } // Long et tortueux ! créer une classe dédiée ? set pagination(config: pagination) { this._pagination={}; // Si une valeur par défaut est fournie ou des valeurs en option, un id valide doit être aussi fourni pour recueillir le sélecteur de pages : if(config.selectedValue !== undefined || config.options !== undefined) { if(config.pages === undefined) throw new Error(errors.needPagesSelectorElt); let checkContainerExist=document.getElementById(config.pages.displayElement.id); if(checkContainerExist === null) throw new Error(errors.elementNotFound+config.pages.displayElement.id); else { this.pagination.pages= { displayElement: { id:config.pages.displayElement.id, eltDOM: checkContainerExist }, name: (config.pages.name) ? config.pages.name : "Pages :", selectedValue:1,// 1ère page affichée par défaut } } } if(config.options !== undefined) { let checkContainerExist=document.getElementById(config.options.displayElement.id); if(checkContainerExist === null) throw new Error(errors.elementNotFound+config.options.displayElement.id); else { for(let i = 0; i < config.options.values.length; i++) { if(freeDatas2HTML.isPositiveInteger(config.options.values[i]) === false) throw new Error(errors.needPositiveInteger); } this._pagination.options = { displayElement: { id:config.options.displayElement.id, eltDOM:checkContainerExist }, name: (config.options.name) ? config.options.name : "Pagination :", values:config.options.values }; } } // Valeur de pagination par défaut qui peut être différente de celles éventuellement proposées en option : if(config.selectedValue !== undefined) { if(config.options !== undefined && (config.options.values.indexOf(config.selectedValue) === -1)) throw new Error(errors.needPaginationByDefaultBeInOptions); if(freeDatas2HTML.isPositiveInteger(config.selectedValue)) this._pagination.selectedValue=config.selectedValue; else throw new Error(errors.needPositiveInteger); } } get pagination(): pagination { return this._pagination; } public async parse(): Promise { const converter=this; return new Promise((resolve,reject) => { if(converter._datasSourceUrl !== "" ) { Papa.parse(converter._datasSourceUrl, { quoteChar: '"', header: true, complete: function(results :any) { converter.parseErrors=results.errors; converter.parseDatas=results.data; // Attention, papaParse peut accepter un nom de colonne vide let realFields: string[]=[]; for(let i in results.meta.fields) { if(results.meta.fields[i].trim() !== "") realFields.push(results.meta.fields[i]); } results.meta.fields=realFields; converter.parseMeta=results.meta; resolve(true); }, error:function(error :any) { reject(new Error(errors.parserFail)); }, download: true, skipEmptyLines: true, }); } else reject(new Error(errors.needUrl)); }); } public async run(): Promise { if (this._datasViewElt.eltDOM === undefined) throw new Error(errors.needDatasElt); if(this._datasSourceUrl === "" ) throw new Error(errors.needUrl); await this.parse(); if(this.parseDatas.length === 0 || this.parseMeta!.fields === undefined) // je force avec "!", car l'existence de parseMeta est certaine après parse(). throw new Error(errors.datasNotFound); else if(this.stopIfParseErrors && this.parseErrors.length!==0) console.error(this.parseErrors); else { let converter=this; // Si demandé, création des listes permettant de filter les données if(this._datasSelectors.length > 0) { let selectorsHTML : string [] = []; for(let i in this._datasSelectors) { // Les colonnes devant servir de filtre existent-elles vraiment dans le fichier ? if(this._datasSelectors[i].datasFieldNb > (this.parseMeta!.fields.length-1)) throw new Error(errors.selectorFieldNotFound); else { let values=[], colName=this.parseMeta!.fields[this._datasSelectors[i].datasFieldNb]; for (let row in this.parseDatas) { if(this._datasSelectors[i].separator === undefined) { let checkedValue=this.parseDatas[row][colName].trim(); // On ne garde pas les données vides (prévoir possible en option pour pouvoir sélectionner les données non classées sur cette colonne ?) if(checkedValue !== "" && values.indexOf(checkedValue) === -1) values.push(checkedValue); } else { let checkedValues=this.parseDatas[row][colName].split(this._datasSelectors[i].separator as string); for(let i in checkedValues) { let checkedValue=checkedValues[i].trim(); if(checkedValue !== "" && values.indexOf(checkedValue) === -1) values.push(checkedValue); } } } if(values.length > 0) { if(this.getSortingFunctionForField(this._datasSelectors[i].datasFieldNb) !== undefined) values.sort(this.getSortingFunctionForField(this._datasSelectors[i].datasFieldNb)!.sort); else values.sort(compare()); this._datasSelectors[i].name=colName; this._datasSelectors[i].values=values; selectorsHTML[i]=""; this._datasSelectors[i].eltDOM!.innerHTML=selectorsHTML[i]; let selectElement = document.getElementById("freeDatas2HTMLSelector"+i) as HTMLInputElement; selectElement.addEventListener('change', function(e) { converter.datasHTML=converter.createDatasHTML(converter.parseMeta!.fields as string[], converter.parseDatas); converter.refreshView(); }); } } } } // Si demandé, création d'une liste de paginations possibles if(converter.pagination !==undefined && converter.pagination.options !==undefined && converter.pagination.options.values.length > 0) { const values=converter.pagination.options.values!; let selectorsHTML=""; converter.pagination.options.displayElement.eltDOM!.innerHTML=selectorsHTML; let selectElement = document.getElementById("freeDatas2HTMLPaginationSelector") as HTMLInputElement; // Si une pagination par défaut existe et la sélectionne : if(converter.pagination.selectedValue !== undefined) { let indexSelectedValue=converter.pagination.options.values.indexOf(converter.pagination.selectedValue)+1; selectElement.value=""+indexSelectedValue; } selectElement.addEventListener('change', function(e) { if(selectElement.value === "0") converter.pagination.selectedValue=undefined; else converter.pagination.selectedValue=values[Number(selectElement.value)-1]; converter.datasHTML=converter.createDatasHTML(converter.parseMeta!.fields as string[], converter.parseDatas); converter.refreshView(); }); } // Je teste aussi les colonnes devant servir à classer les données. for(let i in this._datasSortingColumns) { if(this._datasSortingColumns[i].datasFieldNb > (this.parseMeta!.fields.length-1)) throw new Error(errors.sortingColumnsFieldNotFound); } // Si tout est ok, affichage initial de toutes les données du fichier this.datasHTML=this.createDatasHTML(this.parseMeta!.fields, this.parseDatas); this.refreshView(); return true; } } private refreshView() : void { if(this._datasViewElt.eltDOM !== undefined) { const converter=this; this._datasViewElt.eltDOM.innerHTML=this.datasHTML; // Ici, car il faut que le tableau soit déjà dans le DOM pour "mettre sous écoute" les clics if(this._datasSortingColumns.length > 0) { let getTableTh=document.querySelectorAll("table th"); if(getTableTh !== null) { for(let i in this._datasSortingColumns) { let datasFieldNb=this._datasSortingColumns[i].datasFieldNb; let htmlContent=getTableTh[datasFieldNb].innerHTML; htmlContent=""+htmlContent+""; getTableTh[datasFieldNb].innerHTML=htmlContent; let sortingElement=document.getElementById("freeDatas2HTMLSorting"+datasFieldNb); sortingElement!.addEventListener("click", function(e) { e.preventDefault(); let order=converter.datasSortingColumns[i].order ; if(order === undefined || order === "desc") converter.datasSortingColumns[i].order="asc"; else converter.datasSortingColumns[i].order="desc"; converter._datasSortedColumn = converter.datasSortingColumns[i]; converter.datasHTML=converter.createDatasHTML(converter.parseMeta!.fields as string[], converter.parseDatas); converter.refreshView(); }); } } } } } private createDatasHTML(fields: string[], datas: any[]) : string { // Dois-je filtrer les données ? let checkSelectorExist: HTMLSelectElement|null, filters: any[] = []; for(let i in this._datasSelectors) { // Attention : je peux avoir des _datasSelectors fournis, mais pas de liste dans le DOM si aucune donnée ou autre problème. checkSelectorExist=document.querySelector("#"+ this._datasSelectors[i].id+" select"); if(checkSelectorExist != null && checkSelectorExist.selectedIndex != 0) filters.push({ field: this._datasSelectors[i].name, value: this._datasSelectors[i].values![checkSelectorExist.selectedIndex-1], separator:this._datasSelectors[i].separator }); } // Dois-je classer les données par rapport à une colonne ? if(this._datasSortedColumn !== undefined) { const col=fields[this._datasSortedColumn.datasFieldNb]; const colOrder=this._datasSortedColumn.order; // Une fonction spécifique de classement a-t-elle été définie ? if(this.getSortingFunctionForField(this._datasSortedColumn.datasFieldNb) !== undefined) { let myFunction=this.getSortingFunctionForField(this._datasSortedColumn.datasFieldNb); datas.sort( (a, b) => { return myFunction!.sort(a[col], b[col], colOrder); }); } else datas.sort( (a, b) => compare( {order: colOrder} )(a[col], b[col])); } // Dois-je prendre en compte une pagination ? let firstData=0; if (this.pagination !== undefined && this.pagination.selectedValue !== undefined && this.pagination.pages !== undefined && this.pagination.pages.selectedValue !== undefined) firstData=this.pagination.selectedValue*(this.pagination.pages.selectedValue-1); let maxData = (this.pagination !== undefined && this.pagination.selectedValue !== undefined) ? this.pagination.selectedValue : datas.length+1; // Création du tableau de données : let datasHTML=""; for (let i in fields) datasHTML+=""; datasHTML+=""; let nbVisible=0, nbTotal=0; for (let row in datas) { let visible=true; if(filters.length !== 0) { let i=0; while(filters[i] !== undefined && visible===true) { // Il faut réutiliser le trim() utilisé pour créer les filtres, sinon on risque de ne pas retrouver certaines valeurs if(filters[i].separator === undefined) { if(datas[row][filters[i].field].trim() !== filters[i].value) visible=false; } else { visible=false; let checkedValues=datas[row][filters[i].field].split(filters[i].separator as string); for(let j in checkedValues) { if(checkedValues[j].trim() === filters[i].value) { visible=true; break; } } } i++; } } if(visible && nbTotal >= firstData && nbVisible < maxData) { datasHTML+=""; for(let field in datas[row]) { // Attention : si les erreurs papaParse ne sont pas bloquantes, il peut y avoir des données en trop, avec comme nom de colonne : "__parsed_extra" if(fields.indexOf(field) !== -1) datasHTML+=""; } datasHTML+=""; nbVisible++; nbTotal++; } else if(visible) nbTotal++; } datasHTML+="
"+fields[i]+"
"+datas[row][field]+"
"; // Si pagination définie et tous les enregistrements n'ont pas été affichés, alors création d'un sélecteur de pages if (this.pagination !== undefined && this.pagination.selectedValue !== undefined && this.pagination.pages !== undefined && nbTotal > this.pagination.selectedValue) { let nbPages=Math.ceil(nbTotal/this.pagination.selectedValue); let selectorsHTML=""; this.pagination.pages.displayElement.eltDOM!.innerHTML=selectorsHTML; let selectElement = document.getElementById("freeDatas2HTMLPagesSelector") as HTMLInputElement; if(this.pagination.pages.selectedValue !== undefined) selectElement.value=""+this.pagination.pages.selectedValue; let converter=this; this.pagination.pages.selectedValue=1; selectElement.addEventListener('change', function(e) { converter.pagination.pages!.selectedValue=Number(selectElement.value); converter.datasHTML=converter.createDatasHTML(converter.parseMeta!.fields as string[], converter.parseDatas); converter.refreshView(); }); } else if(this.pagination !== undefined && this.pagination.pages !== undefined) this.pagination.pages.displayElement.eltDOM!.innerHTML=""; return datasHTML; } }