const Papa = require("papaparse"); const errors = require("./errors.js"); const { compare }= require('natural-orderby'); import { DOMElement, Pagination, Selectors, SortingFields, SortingFunctions } from "./freeDatas2HTMLInterfaces"; import { Selector } from "./freeDatas2HTMLSelector"; import { SortingField } from "./freeDatas2HTMLSortingField"; import { PapaParseDatas, PapaParseErrors, PapaParseMeta } from "./papaParseInterfaces"; export class FreeDatas2HTML { // L'élément HTML où doivent être affichées les données : private _datasViewElt: DOMElement = { id:"", eltDOM:undefined }; // Le code HTML résultant (utile ?) : public datasHTML: string = ""; // L'url où accéder aux données : private _datasSourceUrl: string = ""; // Les fonctions spécifiques de classement pour certains champs : private _datasSortingFunctions: SortingFunctions[] = []; // Le nom des champs (interfaces à renommer, car PapaParse = cas particulier) : public parseMetas: PapaParseMeta|undefined = undefined; // Les données à proprement parler : public parseDatas: PapaParseDatas[] = []; // Les erreurs rencontrées durant le parsage : public parseErrors: PapaParseErrors[] = []; // Doit-on tout arrêter si une erreur est rencontrée durant la parsage ? public stopIfParseErrors: boolean = false; // Les filtres possible sur certains champs datasSelectors: Selectors[] = []; // Les champs pouvant être classés datasSortingFields: SortingFields[] = []; // La dernier champ pour lequel le classement a été demandé datasSortedField: SortingFields|undefined; // La Pagination : private _Pagination: Pagination|undefined; // Fonction utile pour tester les numéros de colonne : public static isPositiveInteger(nb: number) { return (Number.isInteger(nb) === false || nb <= 0) ? false : true; } // Fonction utile pour tester les valeurs de Pagination : public static isNaturalNumber(nb: number) { return (Number.isInteger(nb) === false || nb < 0) ? false : true; } // Vérifie que l'élément devant afficher les données existe dans le DOM : 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; } } // Vérifie que l'url où chercher les données n'est pas vide : inutile si données dans page ou tranmises set datasSourceUrl(url: string) { if(url.trim().length === 0) throw new Error(errors.needUrl); else this._datasSourceUrl=url.trim(); } // Vérifie que les numéros de champs pour lesquels il y a des fonctions de classement spécifiques sont cohérents 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 l'éventuelle fonction spécifique de classement associée à un champ public getSortingFunctionForField(datasFieldNb: number): SortingFunctions|undefined { for(let i in this._datasSortingFunctions) { if(this._datasSortingFunctions[i].datasFieldNb === datasFieldNb) return this._datasSortingFunctions[i]; } return undefined; } // Vérifie la cohérence de toutes les options de pagination reçues : 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 :", // rendre obligatoire cette option s'il doit y avoir affichage ? selectedValue:1, // c'est la 1ère page qui est affichée par défaut } } } // Les options de Pagination proposées à l'utilisateur : if(config.options !== undefined) { // Un élément HTML doit exister pour accueillir les options : let checkContainerExist=document.getElementById(config.options.displayElement.id); if(checkContainerExist === null) throw new Error(errors.elementNotFound+config.options.displayElement.id); else { // Seules des entiers positifs sont possibles 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 :", // idem, rendre obligatoire ? values:config.options.values }; } } // Valeur de Pagination par défaut qui doit faire partie des options proposées si elles existent : 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); } } // Retourne les options de Pagination actuelles get Pagination(): Pagination { return this._Pagination; } // Parse des données distantes (url) fournies en CSV : 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.parseMetas=results.meta; resolve(true); }, error:function(error :any) { reject(new Error(errors.parserFail)); }, download: true, skipEmptyLines: true, }); } else reject(new Error(errors.needUrl)); }); } // Lance FreeDatas2HTML suivant les données reçues : 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.parseMetas!.fields === undefined) // je force avec "!", car l'existence de parseMetas 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 d'une liste de valeurs de Pagination 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, on 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; // = pas de Pagination else converter.Pagination.selectedValue=values[Number(selectElement.value)-1]; // on regénère le HTML : converter.datasHTML=converter.createDatasHTML(converter.parseMetas!.fields as string[], converter.parseDatas); converter.refreshView(); }); } // Si tout est ok, affichage initial de toutes les données du fichier this.datasHTML=this.createDatasHTML(this.parseMetas!.fields, this.parseDatas); this.refreshView(); return true; } } refreshView() : void { if(this._datasViewElt.eltDOM !== undefined) { const converter=this; this._datasViewElt.eltDOM.innerHTML=this.datasHTML; // On réactive les éventuelles colonnes de classement if(this.datasSortingFields.length > 0) { for(let i in this.datasSortingFields) { let field=this.datasSortingFields[i]; field.field2HTML(); } } } } createDatasHTML(fields: string[], datas: any[]) : string { // Dois-je classer les données par rapport à un champ ? if(this.datasSortedField !== undefined && this.datasSortedField.datasFieldNb !==undefined) { const field=fields[this.datasSortedField.datasFieldNb]; const fieldOrder=this.datasSortedField.order; // Une fonction spécifique de classement a-t-elle été définie ? if(this.getSortingFunctionForField(this.datasSortedField.datasFieldNb) !== undefined) { let myFunction=this.getSortingFunctionForField(this.datasSortedField.datasFieldNb); datas.sort( (a, b) => { return myFunction!.sort(a[field], b[field], fieldOrder); }); } else datas.sort( (a, b) => compare( {order: fieldOrder} )(a[field], b[field])); } // 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(this.datasSelectors.length !== 0) { let i=0; while(this.datasSelectors[i] !== undefined && visible===true) { visible=this.datasSelectors[i].dataIsOk(datas[row]); // à revoir car cette fonction est nécessaire ! 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.parseMetas!.fields as string[], converter.parseDatas); converter.refreshView(); }); } else if(this.Pagination !== undefined && this.Pagination.pages !== undefined) this.Pagination.pages.displayElement.eltDOM!.innerHTML=""; return datasHTML; } } // Permet l'appel des dépendances via un seul script export { Selector } from "./freeDatas2HTMLSelector"; export { SortingField } from "./freeDatas2HTMLSortingField";