FreeDatas2HTML/src/freeDatas2HTML.ts

372 lines
16 KiB
TypeScript

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 <Pagination>this._Pagination;
}
// Parse des données distantes (url) fournies en CSV :
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.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<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.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="<label for='freeDatas2HTMLPaginationSelector'>"+converter.Pagination.options.name+" </label><select name='freeDatas2HTMLPaginationSelector' id='freeDatas2HTMLPaginationSelector'><option value='0'>----</option>";
for(let j in values)
selectorsHTML+="<option value='"+(Number(j)+1)+"'>"+values[j]+"</option>";
selectorsHTML+="</select>";
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="<table><thead>";
for (let i in fields)
datasHTML+="<th>"+fields[i]+"</th>";
datasHTML+="</thead><tbody>";
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+="<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>";
nbVisible++;
nbTotal++;
}
else if(visible)
nbTotal++;
}
datasHTML+="</tbody></table>";
// 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="<label for='freeDatas2HTMLPagesSelector'>"+this.Pagination.pages.name+" </label><select name='freeDatas2HTMLPagesSelector' id='freeDatas2HTMLPagesSelector'><option value='1'>1</option>";
this.Pagination.pages.values=[1];
for(let j=2; j <= nbPages; j++)
{
selectorsHTML+="<option value='"+j+"'>"+j+"</option>";
this.Pagination.pages.values.push(j);
}
selectorsHTML+="</select>";
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";