FreeDatas2HTML/src/freeDatas2HTML.ts

358 lines
16 KiB
TypeScript

const Papa = require("papaparse");
const errors = require("./errors.js");
const { compare }= require('natural-orderby');
import { papaParseDatas, papaParseErrors, papaParseMeta } from "./papaParseInterfaces";
import { domElement, 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;
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 = 0; i < selectionElts.length; i++)
{
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;
}
public async parse(): Promise<any>
{
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<any>
{
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]="<label for='freeDatas2HTMLSelector"+i+"'>"+colName+" : </label><select name='freeDatas2HTMLSelector"+i+"' id='freeDatas2HTMLSelector"+i+"'><option value='0'>----</option>";
for(let j in values)
selectorsHTML[i]+="<option value='"+(Number(j)+1)+"'>"+values[j]+"</option>";
selectorsHTML[i]+="</select>";
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();
});
}
}
}
}
// 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="<a href='#freeDatas2HTMLSorting"+datasFieldNb+"' id='freeDatas2HTMLSorting"+datasFieldNb+"'>"+htmlContent+"</a>";
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]));
}
// Création du tableau de données :
let datasHTML="<table><thead>";
for (let i in fields)
datasHTML+="<th>"+fields[i]+"</th>";
datasHTML+="</thead><tbody>";
for (let row in datas)
{
let visible=true;
if(filters.length !== 0)
{
for(let i in filters)
{
// 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
{
let checkedValues=datas[row][filters[i].field].split(filters[i].separator as string), finded=false;
for(let j in checkedValues)
{
if(checkedValues[j].trim() === filters[i].value)
{
finded=true;
break;
}
}
if(!finded)
visible=false;
}
}
}
if(visible)
{
datasHTML+="<tr>";
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+="<td>"+datas[row][field]+"</td>";
}
datasHTML+="</tr>";
}
}
datasHTML+="</tbody></table>";
return datasHTML;
}
}