Nouvelle version avec classe séparée pour parser le CSV.

This commit is contained in:
Fabrice PENHOËT 2021-09-29 17:56:10 +02:00
parent bcc5ea66f2
commit 50b8a8fccc
11 changed files with 251 additions and 130 deletions

View File

@ -1,6 +1,6 @@
{
"name": "freedatas2html",
"version": "0.5.2",
"version": "0.6.0",
"description": "Visualization of data from various sources (CSV, API, HTML...) with filters, classification, pagination, etc.",
"main": "index.js",
"scripts": {

View File

@ -3,6 +3,7 @@ module.exports =
converterElementNotFound : "Aucun élément HTML n'a été trouvé ayant comme \"id\" : ",
converterFieldNotFound : "Le champ n'existe pas dans les données ou les données n'ont pas encore été chargées.",
converterNeedDatasElt: "Merci de fournir un id valide pour l'élément où afficher les données.",
converterNeedDatas: "Merci de fournir les données à traiter.",
converterRefreshFail: "Le nom des champs et l'élement du DOM receveur sont nécessaires à l'affichage des données.",
pagination2HTMLFail : "Toutes les donnée nécessaires à la création des sélecteurs de pagination n'ont pas été fournies.",
paginationNeedByfaultValueBeInOptions: "La valeur de pagination par défaut doit faire partie des options proposées.",
@ -10,8 +11,11 @@ module.exports =
paginationNeedOptionsValues: "Vous n'avez fourni aucune options possibles pour la pagination.",
paginationNeedPositiveInteger: "Merci de fournir un nombre entier supérieur à zéro pour désigner chaque option de pagination.",
parserDatasNotFound : "Aucune donnée n'a été trouvée.",
parserFail: "La lecture des données du fichier a échoué.",
parserNeedUrl: "Merci de fournir une url valide pour le fichier à parser.",
parserFail: "La lecture des données a échoué.",
parserMeetErrors : "Au moins une erreur a été rencontrée durant le traitement des données.",
parserNeedDatas: "Merci de fournir une chaîne de caractères valide à parser.",
parserNeedSource: "Merci de fournir une chaîne de caractères où une url pour les données à parser.",
parserNeedUrl: "Merci de fournir une url valide pour la source distante de données.",
renderNeedDatas: "Il ne peut y avoir de pagination, si les données n'ont pas été récupérées.",
renderUnknownField: "Un champ non attendu a été trouvé dans les données à afficher : ",
selector2HTMLFail: "Le création d'un filtre dans le DOM nécessite l'initialisation de l'élément HTML et du numéro du champs à filter.",

View File

@ -19,11 +19,9 @@ const initialise = async () =>
};
// Création d'un convertisseur parsant les données d'un fichier CSV "distant"
let converter=new FreeDatas2HTML();
let converter=new FreeDatas2HTML("CSV","", { url:"http://localhost:8080/datas/elements-chimiques.csv"});
converter.datasViewElt={ id:"datas" };
converter.datasSourceUrl="http://localhost:8080/datas/elements-chimiques.csv";
await converter.parse();
converter.datasSortingFunctions=[{ datasFieldNb:4, sort:mySort }];
await converter.run();
// Adaptation du rendu suivant la taille de l'écran
const myRender=new Render(converter);
if(window.innerWidth < 800)
@ -46,6 +44,9 @@ const initialise = async () =>
converter.datasRender=myRender;
}
// Ajout d'une fonction de classement spécifique
converter.datasSortingFunctions=[{ datasFieldNb:4, sort:mySort }];
// Configuration de la pagination
const pagination=new Pagination(converter, { id:"pages" }, "Page à afficher :");
pagination.options={ displayElement: { id:"paginationOptions" }, values: [10,20,50,500] , name: "Choix de pagination :" };

View File

@ -1,15 +1,13 @@
const Papa = require("papaparse");
const errors = require("./errors.js");
const { compare }=require('natural-orderby');
const errors=require("./errors.js");
import { Counter, DatasRenders, DOMElement, Paginations, Selectors, SortingFields, SortingFunctions } from "./freeDatas2HTMLInterfaces";
import { Counter, Datas, DatasRenders, DOMElement, Paginations, Parsers, ParseErrors, RemoteSource, Selectors, SortingFields, SortingFunctions } from "./freeDatas2HTMLInterfaces";
import { Pagination} from "./freeDatas2HTMLPagination";
import { ParserForCSV} from "./freeDatas2HTMLParserForCSV";
import { Render} from "./freeDatas2HTMLRender";
import { Selector } from "./freeDatas2HTMLSelector";
import { SortingField } from "./freeDatas2HTMLSortingField";
import { PapaParseDatas, PapaParseErrors, PapaParseMeta } from "./papaParseInterfaces";
export class FreeDatas2HTML
{
// L'élément HTML où afficher les données. Laisser à undefined si non affichées :
@ -18,35 +16,62 @@ export class FreeDatas2HTML
public datasRender: DatasRenders;
// Le code HTML résultant :
public datasHTML: string = "";
// Le parseur :
public parser: Parsers; // public pour permettre de charger un parseur tiers après instanciation
// L'url où accéder aux données :
private _datasSourceUrl: string = "";
// Le nom des champs (interfaces à renommer, car PapaParse = cas particulier) :
public parseMetas: PapaParseMeta|undefined = undefined;
// Données distantes :
//private _datasRemoteSource: RemoteSource|undefined=undefined;
// Ou locales :
//private _datas2Parse:string|undefined=undefined;
// Dans tous les cas, besoin d'un type :
public datasType: "CSV"|"HTML"|"JSON"|undefined;
// Le nom des champs trouvés dans les données :
public fields: string[]|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 datas: []=[];
// Les erreurs rencontrées durant le traitement des données reçues :
public parseErrors: ParseErrors[]|undefined;
// Doit-on tout arrêter si une erreur est rencontrée durant le traitement ?
public stopIfParseErrors: boolean = false;
// Les fonctions spécifiques de classement pour certains champs :
private _datasSortingFunctions: SortingFunctions[] = [];
// Les filtres possible sur certains champs
// Les filtres possible sur certains champs :
datasSelectors: Selectors[] = [];
// Les champs pouvant être classés
// Les champs pouvant être classés :
datasSortingFields: SortingFields[] = [];
// La dernier champ pour lequel le classement a été demandé
// La dernier champ pour lequel le classement a été demandé :
datasSortedField: SortingFields|undefined;
// La Pagination :
// Éventuelle pagination :
pagination: Paginations|undefined;
// Affichage du nombre total de lignes de données (optionnel) :
private _datasCounter: Counter = {};
// J'initialiser avec des valeurs par défaut pouvant être surchargées par les setters
constructor()
constructor(datasType:"CSV"|"HTML"|"JSON", datas2Parse="", datasRemoteSource?:RemoteSource)
{
this.datasRender=new Render(this);
switch (datasType)
{
case "CSV":
this.parser=new ParserForCSV();
break;
case "HTML":
this.parser=new ParserForCSV();
console.error("Appeler le parseur HTML");
break;
case "JSON":
this.parser=new ParserForCSV();
console.error("Appeler le parseur JSON");
break;
}
if(datas2Parse !== "")
this.parser.datas2Parse=datas2Parse;
else if(datasRemoteSource!==undefined)
this.parser.datasRemoteSource=datasRemoteSource;
else
throw new Error(errors.converterNeedDatas);
}
// Vérifie s'il y a bien un élément dans le DOM pour l'id fourni
@ -65,7 +90,7 @@ export class FreeDatas2HTML
// Vérifie qu'un champ existe bien dans les données
public checkFieldExist(nb: number) : boolean
{
if(this.parseMetas === undefined || this.parseMetas.fields === undefined || this.parseMetas.fields[nb] === undefined)
if(this.fields === undefined || this.fields[nb] === undefined)
return false;
else
return true;
@ -77,15 +102,6 @@ export class FreeDatas2HTML
this._datasViewElt=FreeDatas2HTML.checkInDOMById(elt);
}
// 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.parserNeedUrl);
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
// ! Ne peut être testé qu'après avoir reçu les données
set datasSortingFunctions(SortingFunctions: SortingFunctions[])
@ -126,58 +142,21 @@ export class FreeDatas2HTML
return undefined;
}
// 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.parserNeedUrl));
});
}
// Lance FreeDatas2HTML suivant les données reçues :
public async run(): Promise<any>
{
if(this._datasSourceUrl === "" )
throw new Error(errors.parserNeedUrl);
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().
await this.parser.parse();
if(this.parser.parseResults === undefined)
throw new Error(errors.parserFail);
else
{
this.fields=this.parser.parseResults.fields;
this.datas=this.parser.parseResults.datas;
this.parseErrors=this.parser.parseResults.errors;
if(this.fields === undefined)
throw new Error(errors.parserDatasNotFound);
else if(this.stopIfParseErrors && this.parseErrors.length!==0)
console.error(this.parseErrors);
else if(this.stopIfParseErrors && this.parseErrors !== undefined)
throw new Error(errors.parrserMeetErrors);
else
{
// Si tout est ok, affichage initial de toutes les données du fichier
@ -185,13 +164,14 @@ export class FreeDatas2HTML
return true;
}
}
}
refreshView() : void
{
if(this.parseMetas === undefined || this.parseMetas.fields === undefined)
if(this.fields === undefined)
throw new Error(errors.converterRefreshFail);
this.datasHTML=this.createDatas2Display(this.parseMetas.fields, this.parseDatas);
this.datasHTML=this.createDatas2Display(this.fields, this.datas);
if(this._datasViewElt !== undefined && this._datasViewElt.eltDOM !== undefined)
this._datasViewElt.eltDOM.innerHTML=this.datasHTML;

View File

@ -46,6 +46,36 @@ export interface PaginationsPages
values?: number[];
selectedValue?: number;
}
export interface Datas
{
[key: string]: string;
}
export interface ParseErrors // erreurs non bloquantes rencontrées lors du parsage de données
{
code?: string;
message: string;
row: number;
type?: string;
}
export interface ParseResults
{
datas: [];
errors: ParseErrors[];
fields: string[];
}
export interface Parsers
{
datasRemoteSource: RemoteSource|undefined;
datas2Parse:string|undefined;
parseResults:ParseResults|undefined;
parse(): Promise<void>;
}
export interface RemoteSource
{
url: string;
headers?: { key:string, value:string|boolean|number }[] ;// revoir types possibles pour Headers ?
withCredentials?:boolean;
}
export interface Selectors
{
datasViewElt: DOMElement;

View File

@ -21,7 +21,7 @@ export class Pagination implements Paginations
// De plus l'élément du DOM devant recevoir la liste des pages doit exister
constructor(converter: FreeDatas2HTML, pagesElt: DOMElement, pagesName: string="Pages")
{
if(converter.parseMetas === undefined || converter.parseMetas.fields === undefined)
if(converter.fields === undefined)
throw new Error(errors.paginationNeedDatas);
else
{

View File

@ -0,0 +1,129 @@
const Papa = require("papaparse");
const errors = require("./errors.js");
import { ParseResults, Parsers, RemoteSource } from "./freeDatas2HTMLInterfaces";
interface papaParseOptions
{
delimiter: string;
newline: string;
quoteChar: string;
escapeChar: string;
transformHeader?(field: string, index: number): string;
preview: number;
comments: false|string,
fastMode: boolean|undefined;
transform?(value: string): string;
}
export class ParserForCSV implements Parsers
{
private _datasRemoteSource: RemoteSource|undefined=undefined;
private _datas2Parse:string|undefined=undefined;
private _parseResults:ParseResults|undefined=undefined;
// Ouverture de certaines options de Papa Parse :
// cf. https://www.papaparse.com/docs#config
public options: papaParseOptions =
{
delimiter: "",
newline: "",
quoteChar: '"',
escapeChar: '"',
transformHeader: function(field: string, index: number): string { return field.trim() },
preview: 0,
comments: "",
fastMode: undefined,
transform: undefined
}
set datasRemoteSource(source: RemoteSource)
{
if(source.url.trim().length === 0)
throw new Error(errors.parserNeedUrl);
else
{
source.url=source.url.trim();
this._datasRemoteSource=source;
}
}
set datas2Parse(datas: string)
{
if(datas.trim().length === 0)
throw new Error(errors.parserNeedDatas);
else
this._datas2Parse=datas.trim();
}
get parseResults() : ParseResults|undefined
{
return this._parseResults;
}
// async dans le cas d'une source distante
// Et création d'une Promise car PapaParse utilise une fonction callback.
public async parse(): Promise<any>
{
const parser=this, options=this.options;
return new Promise((resolve,reject) =>
{
let parseContent="", parseDownload=false, parseDownloadRequestHeaders: any=undefined, parseWithCredentials: any=undefined;
if(parser._datasRemoteSource !== undefined)
{
parseContent=parser._datasRemoteSource.url;
parseDownload=true;
parseWithCredentials=parser._datasRemoteSource.withCredentials; // undefined ok pour PapaParse
if(parser._datasRemoteSource.headers !== undefined)
{
parseDownloadRequestHeaders={};
for (let i in parser._datasRemoteSource.headers)
parseDownloadRequestHeaders[parser._datasRemoteSource.headers[Number(i)].key]=parser._datasRemoteSource.headers[Number(i)].value;
}
}
else if(parser._datas2Parse !== undefined)
parseContent=parser._datas2Parse;
else
reject(new Error(errors.parserNeedSource));
Papa.parse(parseContent,
{
delimiter: options.delimiter,
newline: options.newline,
quoteChar: options.quoteChar,
escapeChar: options.escapeChar,
header: true, // nécessaire pour obtenir le nom des champs
transformHeader: options.transformHeader,
preview: options.preview,
comments: options.comments,
complete: function(results :any)
{
// Attention, Papa Parse peut accepter un nom de champ vide
let realFields: string[]=[];
for(let i in results.meta.fields)
{
if(results.meta.fields[i].trim() !== "")
realFields.push(results.meta.fields[i]);
}
if(realFields.length === 0)
reject(new Error(errors.parserFail));
else
{
parser._parseResults={
datas: results.data,
errors: results.errors,
fields: realFields,
};
resolve(true);
}
},
download: parseDownload,
downloadRequestHeaders: parseDownloadRequestHeaders,
skipEmptyLines:"greedy",
fastMode: options.fastMode,
withCredentials: parseWithCredentials,
transform: options.transform,
});
});
}
}

View File

@ -31,7 +31,7 @@ export class Render implements DatasRenders
public rend2HTML(datas: any[]) : string
{
// Il peut n'y avoir aucune donnée (filtres...), mais les noms des champs doivent être connus.
if(this._converter.parseMetas === undefined || this._converter.parseMetas.fields === undefined)
if(this._converter.fields === undefined)
throw new Error(errors.renderNeedDatas);
else
{
@ -40,8 +40,8 @@ export class Render implements DatasRenders
if(this.settings.fieldsBegining !== undefined && this.settings.fieldDisplaying !== undefined && this.settings.fieldsEnding !== undefined )
{
datasHTML+=this.settings.fieldsBegining;
for (let i in this._converter.parseMetas.fields)
datasHTML+=this.settings.fieldDisplaying.replace("#FIELDNAME", this._converter.parseMetas.fields[Number(i)]);
for (let i in this._converter.fields)
datasHTML+=this.settings.fieldDisplaying.replace("#FIELDNAME", this._converter.fields[Number(i)]);
datasHTML+=this.settings.fieldsEnding;
}
datasHTML+=this.settings.linesBegining;
@ -51,7 +51,7 @@ export class Render implements DatasRenders
for(let field in datas[row])
{
// On n'affiche que les champs attendus et signale les erreurs dans la console
if(this._converter.parseMetas.fields.indexOf(field) !== -1)
if(this._converter.fields.indexOf(field) !== -1)
datasHTML+=this.settings.dataDisplaying.replace("#VALUE" , datas[row][field]).replace("#FIELDNAME" , field);
else
console.log(errors.renderUnknownField+field);

View File

@ -15,7 +15,7 @@ export class Selector implements Selectors
// Injection de la classe principale, mais uniquement si les données ont été importées
constructor(converter: FreeDatas2HTML, datasFieldNb: number, elt: DOMElement)
{
if(converter.parseMetas === undefined || converter.parseMetas.fields === undefined || converter.parseDatas.length === 0)
if(converter.fields === undefined || converter.datas.length === 0)
throw new Error(errors.selectorNeedDatas);
else if(! converter.checkFieldExist(Number(datasFieldNb)))
throw new Error(errors.selectorFieldNotFound);
@ -59,18 +59,18 @@ export class Selector implements Selectors
throw new Error(errors.selector2HTMLFail);
else
{
this.name=this._converter.parseMetas!.fields![this._datasFieldNb]; // this._converter.parse... ne peuvent être indéfinis si this._converter existe (cf constructeur)
for (let row in this._converter.parseDatas)
this.name=this._converter.fields![this._datasFieldNb]; // this._converter.parse... ne peuvent être indéfinis si this._converter existe (cf constructeur)
for (let row in this._converter.datas)
{
if(this._separator === undefined)
{
let checkedValue=String(this._converter.parseDatas[row][this.name]).trim(); // trim() nécessaire pour éviter problème de classement du tableau (sort)
let checkedValue=String(this._converter.datas[row][this.name]).trim(); // trim() nécessaire pour éviter problème de classement du tableau (sort)
if(checkedValue !== "" && this.values.indexOf(checkedValue) === -1)
this.values.push(checkedValue);
}
else
{
let checkedValues=String(this._converter.parseDatas[row][this.name]).split(this._separator); // String() car les données peuvent être des chiffres, etc.
let checkedValues=String(this._converter.datas[row][this.name]).split(this._separator); // String() car les données peuvent être des chiffres, etc.
for(let i in checkedValues)
{
let checkedValue=checkedValues[i].trim();

View File

@ -13,7 +13,7 @@ export class SortingField implements SortingFields
// Injection de la classe principale, mais uniquement si le nom des champs ont été importés et affichés correctement
constructor(converter: FreeDatas2HTML, datasFieldNb: number, fieldsDOMSelector: string = "th")
{
if(converter.parseMetas === undefined || converter.parseMetas.fields === undefined)
if(converter.fields === undefined)
throw new Error(errors.sortingFieldNeedDatas);
else if(! converter.checkFieldExist(Number(datasFieldNb)))
throw new Error(errors.sortingFieldFieldNotFound);
@ -22,7 +22,7 @@ export class SortingField implements SortingFields
const fields=document.querySelectorAll(fieldsDOMSelector);
if(fields === undefined)
throw new Error(errors.sortingFieldsNotInHTML);
else if(fields.length !== converter.parseMetas.fields.length)
else if(fields.length !== converter.fields.length)
throw new Error(errors.sortingFieldsNbFail);
else
{

View File

@ -1,23 +0,0 @@
// cf. https://www.papaparse.com/docs#results
export interface PapaParseDatas
{
[key: string]: string;
}
export interface PapaParseErrors
{
type: string;
code: string;
message: string;
row: number;
}
export interface PapaParseMeta
{
delimiter?: string;
linebreak?: string;
aborted?: boolean;
fields?: string[];
truncated?: boolean;
}