diff --git a/package.json b/package.json index cd47e16..798b1ce 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "freedatas2html", - "version": "1.1.0", + "version": "1.2.0", "description": "Conversion and display of data in different formats (CSV, JSON, HTML) with the possibility of filtering, classifying and paginating the results.", "main": "index.js", "scripts": { diff --git a/src/Selector.ts b/src/Selector.ts index c08ad40..c782c4c 100644 --- a/src/Selector.ts +++ b/src/Selector.ts @@ -8,10 +8,11 @@ export class Selector implements Selectors private _converter: FreeDatas2HTML; private _datasFieldNb: number; private _datasViewElt: DOMElement={ id: "", eltDOM: undefined }; - private _selectedValue: number|undefined=undefined; + private _selectedValues: number[]=[]; private _separator: string|undefined; private _values: string[]=[]; private _name: string=""; + public isMultiple: boolean=false;// permet à l'utilisateur de sélectionner plusieurs valeurs dans la liste. // Injection de la classe principale, mais uniquement si des données ont été importées. // Le champ duquel le sélecteur tire ses données doit exister. @@ -29,6 +30,8 @@ export class Selector implements Selectors // Pas de trim(), car l'espace peut être le séparateur : if(separator !== undefined && separator !== "") this._separator=separator; + this._name=this._converter.fields[this._datasFieldNb]; + this.setValues(); } } @@ -52,9 +55,9 @@ export class Selector implements Selectors return this._name; } - get selectedValue() : number|undefined + get selectedValues() : number[] { - return this._selectedValue; + return this._selectedValues; } get separator() : string|undefined @@ -67,11 +70,10 @@ export class Selector implements Selectors return this._values; } - // Création du "; // l'option zéro permet d'actualiser l'affichage en ignorant ce filtre. - for(let i=0; i< this._values.length; i++) - selectorsHTML+=""; - selectorsHTML+=""; - this. _datasViewElt.eltDOM!.innerHTML=selectorsHTML;// "!", car l'existence de "eltDOM" est testé par le constructeur. - - // Actualisation de l'affichage lorsqu'une valeur est sélectionnée : - const selectElement=document.getElementById("freeDatas2HTML_"+this._datasViewElt.id) as HTMLInputElement, mySelector=this; - selectElement.addEventListener("change", function(e) - { - if(selectElement.value === "0") - mySelector._selectedValue=undefined; - else - mySelector._selectedValue=parseInt(selectElement.value,10)-1; - mySelector._converter.refreshView(); - }); } } - - public dataIsOk(data: {[index: string]:string}) : boolean + + // Création du "; // l'option zéro permet d'actualiser l'affichage en ignorant ce filtre. + for(let i=0; i< this._values.length; i++) + selectorsHTML+=""; + selectorsHTML+=""; + this. _datasViewElt.eltDOM!.innerHTML=selectorsHTML;// "!", car l'existence de "eltDOM" est testé par le constructeur. + // Actualisation de l'affichage lorsqu'une valeur est sélectionnée : + const selectElement=document.getElementById("freeDatas2HTML_"+this._datasViewElt.id) as HTMLSelectElement, mySelector=this; + selectElement.addEventListener("change", function(e) + { + mySelector._selectedValues=[]; + if(mySelector.isMultiple) { - if(value.trim() === selectedValueTxt) + for(let i=0; i < selectElement.selectedOptions.length; i++) { - find=true; - break; + const selectedValue=parseInt(selectElement.selectedOptions[i].value,10); + if(selectedValue === 0) // = annulation de ce filtre + { + mySelector._selectedValues=[]; + break; + } + else + mySelector._selectedValues.push(selectedValue-1); } } - return find; + else + { + let selectedValue=parseInt(selectElement.value,10); + if(selectedValue === 0) // = annulation de ce filtre + mySelector._selectedValues=[]; + else + mySelector._selectedValues[0]=selectedValue-1; + } + mySelector._converter.refreshView(); + }); + } + + public dataIsOk(data: {[index: string]:string}) : boolean + { + const checkIsValid=(selector : Selector, data: {[index: string]:string}, checkedValue:string) : boolean => + { + if(selector._separator === undefined) + { + if(data[selector._name].trim() !== checkedValue) + return false; + else + return true; + } + else + { + let find=false; + let checkedValues=data[selector._name].split(selector._separator); + for(let value of checkedValues) + { + if(value.trim() === checkedValue) + { + find=true; + break; + } + } + return find; + } + }; + + // Pas de valeur sélectionnée = pas de filtre sur ce champ, donc tout passe : + if(this._selectedValues.length === 0) + return true; + + // Un enregistrement n'ayant pas le champ du filtre sera refusé : + if(data[this._name] === undefined) + return false; + + // Si plusieurs options sont sélectionnées dans une liste multiple, + // il suffit qu'une soit trouvée pour que l'enregistrement soit valide. + let find=false; + for(const value of this._selectedValues) + { + if(this._values[value] === undefined) // théoriquement impossible, mais cela vient du client... + throw new Error(errors.selectorSelectedIndexNotFound); + find=checkIsValid(this, data, this._values[value]); + if(find) + break; } + return find; } } \ No newline at end of file diff --git a/src/build/Selector.js b/src/build/Selector.js index ea4e108..52509c9 100644 --- a/src/build/Selector.js +++ b/src/build/Selector.js @@ -4,9 +4,10 @@ import { FreeDatas2HTML } from "./FreeDatas2HTML"; var Selector = (function () { function Selector(converter, datasFieldNb, elt, separator) { this._datasViewElt = { id: "", eltDOM: undefined }; - this._selectedValue = undefined; + this._selectedValues = []; this._values = []; this._name = ""; + this.isMultiple = false; if (converter.fields.length === 0 || converter.datas.length === 0) throw new Error(errors.filterNeedDatas); else if (!converter.checkFieldExist(Number(datasFieldNb))) @@ -17,6 +18,8 @@ var Selector = (function () { this._datasFieldNb = datasFieldNb; if (separator !== undefined && separator !== "") this._separator = separator; + this._name = this._converter.fields[this._datasFieldNb]; + this.setValues(); } } Object.defineProperty(Selector.prototype, "converter", { @@ -47,9 +50,9 @@ var Selector = (function () { enumerable: true, configurable: true }); - Object.defineProperty(Selector.prototype, "selectedValue", { + Object.defineProperty(Selector.prototype, "selectedValues", { get: function () { - return this._selectedValue; + return this._selectedValues; }, enumerable: true, configurable: true @@ -68,9 +71,7 @@ var Selector = (function () { enumerable: true, configurable: true }); - Selector.prototype.filter2HTML = function (label) { - if (label === void 0) { label = ""; } - this._name = this._converter.fields[this._datasFieldNb]; + Selector.prototype.setValues = function () { for (var _i = 0, _a = this._converter.datas; _i < _a.length; _i++) { var row = _a[_i]; var checkedValue = void 0; @@ -96,50 +97,76 @@ var Selector = (function () { this._values.sort(this._converter.getSortingFunctionForField(this._datasFieldNb).sort); else this._values.sort(compare()); - label = (label === "") ? this._name : label; - var selectorsHTML = ""; - this._datasViewElt.eltDOM.innerHTML = selectorsHTML; - var selectElement_1 = document.getElementById("freeDatas2HTML_" + this._datasViewElt.id), mySelector_1 = this; - selectElement_1.addEventListener("change", function (e) { - if (selectElement_1.value === "0") - mySelector_1._selectedValue = undefined; - else - mySelector_1._selectedValue = parseInt(selectElement_1.value, 10) - 1; - mySelector_1._converter.refreshView(); - }); } }; - Selector.prototype.dataIsOk = function (data) { - if (this._name === "") - throw new Error(errors.filterCheckIsOkFail); - if (this._selectedValue === undefined) - return true; - if (this._values[this._selectedValue] === undefined) - throw new Error(errors.selectorSelectedIndexNotFound); - if (data[this._name] === undefined) - return false; - var selectedValueTxt = this._values[this._selectedValue]; - if (this._separator === undefined) { - if (data[this._name].trim() !== selectedValueTxt) - return false; - else - return true; - } - else { - var find = false; - var checkedValues = data[this._name].split(this._separator); - for (var _i = 0, checkedValues_2 = checkedValues; _i < checkedValues_2.length; _i++) { - var value = checkedValues_2[_i]; - if (value.trim() === selectedValueTxt) { - find = true; - break; + Selector.prototype.filter2HTML = function (label) { + if (label === void 0) { label = ""; } + label = (label === "") ? this._name : label; + var multipleAttr = (this.isMultiple) ? " multiple" : ""; + var selectorsHTML = ""; + this._datasViewElt.eltDOM.innerHTML = selectorsHTML; + var selectElement = document.getElementById("freeDatas2HTML_" + this._datasViewElt.id), mySelector = this; + selectElement.addEventListener("change", function (e) { + mySelector._selectedValues = []; + if (mySelector.isMultiple) { + for (var i = 0; i < selectElement.selectedOptions.length; i++) { + var selectedValue = parseInt(selectElement.selectedOptions[i].value, 10); + if (selectedValue === 0) { + mySelector._selectedValues = []; + break; + } + else + mySelector._selectedValues.push(selectedValue - 1); } } - return find; + else { + var selectedValue = parseInt(selectElement.value, 10); + if (selectedValue === 0) + mySelector._selectedValues = []; + else + mySelector._selectedValues[0] = selectedValue - 1; + } + mySelector._converter.refreshView(); + }); + }; + Selector.prototype.dataIsOk = function (data) { + var checkIsValid = function (selector, data, checkedValue) { + if (selector._separator === undefined) { + if (data[selector._name].trim() !== checkedValue) + return false; + else + return true; + } + else { + var find_1 = false; + var checkedValues = data[selector._name].split(selector._separator); + for (var _i = 0, checkedValues_2 = checkedValues; _i < checkedValues_2.length; _i++) { + var value = checkedValues_2[_i]; + if (value.trim() === checkedValue) { + find_1 = true; + break; + } + } + return find_1; + } + }; + if (this._selectedValues.length === 0) + return true; + if (data[this._name] === undefined) + return false; + var find = false; + for (var _i = 0, _a = this._selectedValues; _i < _a.length; _i++) { + var value = _a[_i]; + if (this._values[value] === undefined) + throw new Error(errors.selectorSelectedIndexNotFound); + find = checkIsValid(this, data, this._values[value]); + if (find) + break; } + return find; }; return Selector; }()); diff --git a/src/build/errors.js b/src/build/errors.js index c5b1fdf..1526e9e 100644 --- a/src/build/errors.js +++ b/src/build/errors.js @@ -5,7 +5,6 @@ module.exports = 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.", converterRefreshFail: "Le nom des champs et l'élement du DOM receveur sont nécessaires à l'affichage des données.", - filterCheckIsOkFail: "Le test est lancé sur un filtre incorrectement initialisé ou sur un attribut absent de la donnée à tester.", filterNeedDatas: "Le création d'un filtre nécessite la présence des données à filtrer.", 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.", diff --git a/src/errors.js b/src/errors.js index c796d9d..b1bc094 100644 --- a/src/errors.js +++ b/src/errors.js @@ -4,7 +4,6 @@ module.exports = 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.", converterRefreshFail: "Le nom des champs et l'élement du DOM receveur sont nécessaires à l'affichage des données.", - filterCheckIsOkFail: "Le test est lancé sur un filtre incorrectement initialisé ou sur un attribut absent de la donnée à tester.", filterNeedDatas: "Le création d'un filtre nécessite la présence des données à filtrer.", 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.", diff --git a/src/interfaces.ts b/src/interfaces.ts index 392fcc4..112ce4c 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -85,7 +85,7 @@ export interface Selectors extends Filters { datasFieldNb: number; name: string; - selectedValue: number|undefined; + selectedValues: number[]; separator: string|undefined; values: string[]; } diff --git a/tests/datas/datas1-emtyinfield.csv b/tests/datas/datas1-emtyinfield.csv index e1cd28d..42d65b0 100644 --- a/tests/datas/datas1-emtyinfield.csv +++ b/tests/datas/datas1-emtyinfield.csv @@ -116,4 +116,4 @@ Z (numéro atomique),Élément,Symbole,Famille,Abondance des éléments dans la 8,Oxygène,O,Non-métal,> 100000 15,Phosphore,P,Non-métal,> 100000 16,Soufre,S,,> 100000 -34,Sélénium,Se,,> 1 et < 100 000 +34,Sélénium,Se,,> 1 et < 100 000 \ No newline at end of file diff --git a/tests/fixtures.js b/tests/fixtures.js index 81426bc..8c836c9 100644 --- a/tests/fixtures.js +++ b/tests/fixtures.js @@ -1,12 +1,10 @@ module.exports = { datasViewEltHTML: '
', - selector1HTML: '', + selector1HTML: '', selector2HTML: '', selector2HTMLWithLabel: '', - selector1HTMLWithSeparator: '', - selectorHTMLWithTrim: '', - selector1HTMLWithFunction: '', + selector2HTMLWithMultiple: '', selectorHTMLWithFakeItem: '', sortingColumn1HTML: 'Z (numéro atomique)', sortingColumn2HTML: 'Symbole', diff --git a/tests/selectorSpec.ts b/tests/selectorSpec.ts index 7d3da94..b4c1f1d 100644 --- a/tests/selectorSpec.ts +++ b/tests/selectorSpec.ts @@ -6,7 +6,7 @@ describe("Test des sélecteurs de données", () => { let converter: FreeDatas2HTML; let selector: Selector; - let selectElement : HTMLInputElement; + let selectElement : HTMLSelectElement; beforeEach( async () => { @@ -22,9 +22,9 @@ describe("Test des sélecteurs de données", () => document.body.removeChild(document.getElementById("fixture")); }); - describe("Test des données de configuration.", () => + describe("Test des données d'initialisation.", () => { - it("Doit générer une erreur, si initialisé sans avoir au préalable charger des données.", async () => + it("Doit générer une erreur, si initialisé sans avoir au préalable chargé des données.", async () => { // Convertisseur non lancé : converter=new FreeDatas2HTML("CSV"); @@ -42,6 +42,13 @@ describe("Test des sélecteurs de données", () => expect(() => { return new Selector(converter, -1, { id:"selector1" }); }).toThrowError(errors.selectorFieldNotFound); expect(() => { return new Selector(converter, 1.1, { id:"selector1" }); }).toThrowError(errors.selectorFieldNotFound); }); + + it("Doit générer une erreur, si aucune donnée n'a été trouvée dans le champ du filtre.", async () => + { + converter.parser.setRemoteSource({ url:"http://localhost:9876/datas/datasEmptyField.csv" }); + await converter.run(); + expect(() => { return new Selector(converter, 3, { id:"selector1" });}).toThrowError(errors.selectorFieldIsEmpty); + }); it("Si un séparateur vide est fourni pour un filtre, il doit être ignoré.", () => { @@ -49,12 +56,58 @@ describe("Test des sélecteurs de données", () => expect(selector.separator).toBeUndefined(); }); - it("Si toutes les paramètres sont valides, ils doivent être acceptés.", () => + it("Si toutes les paramètres sont valides, ils doivent être acceptés et les informations correctement récupérées.", () => { - expect(() => { selector=new Selector(converter, 2, { id:"selector1" }, ","); return true; }).not.toThrowError(); - expect(selector.datasFieldNb).toEqual(2); + expect(() => { selector=new Selector(converter, 3, { id:"selector1" }, ","); return true; }).not.toThrowError(); + expect(selector.datasFieldNb).toEqual(3); expect(selector.datasViewElt).toEqual({ id:"selector1", eltDOM:document.getElementById("selector1") }); expect(selector.separator).toEqual(","); + expect(selector.name).toEqual("Famille"); + expect(selector.values).toEqual(["Actinide","Gaz noble","gaz rare","Halogène","Indéfinie","Lanthanide","Métal alcalin","Métal alcalino-terreux","Métal de transition","Métal pauvre","Métalloïde","Non-métal"]); + }); + + it("Si des valeurs vides sont présentes dans un champ utilisé pour un filtre, elles doivent être ignorées.", async () => + { + converter.parser.setRemoteSource({ url:"http://localhost:9876/datas/datas1-emtyinfield.csv" }); + await converter.run(); + selector=new Selector(converter, 3, { id:"selector1" }, ""); + expect(selector.values).toEqual(["Actinide","Gaz noble","gaz rare","Halogène","Indéfinie","Lanthanide","Métal alcalin","Métal alcalino-terreux","Métal de transition","Métal pauvre","Métalloïde","Non-métal"]); + }); + + it("Si des espaces entourent certaines valeurs pour ce champ, ils doivent être supprimés.", async () => + { + converter.parser.setRemoteSource({ url:"http://localhost:9876/datas/datasNeedTrim.csv" }); + await converter.run(); + selector=new Selector(converter, 3, { id:"selector1" }, ""); + expect(selector.values).toEqual(["Gaz noble","Métal alcalin","Métal alcalino-terreux","Métalloïde","Non-métal"]); + }); + + it("Si un séparateur est fourni, les valeurs distinctes extraites de ce champ doivent le prendre en compte.", async () => + { + converter.parser.setRemoteSource({ url:"http://localhost:9876/datas/datas1+tagsfield.csv" }); + await converter.run(); + selector=new Selector(converter, 5, { id:"selector1" }, "|"); + selector.filter2HTML(); + expect(selector.values).toEqual(["Exemple0","Exemple1","Exemple2","Exemple3","Exemple4","Exemple5","Exemple6","Exemple7","Exemple8","Exemple9","Exemple10"]); + }); + + it("Si une fonction spécifique est fournie pour le champ utilisé pour ce filtre, elle doit être prise en compte.", () => + { + const mySort=(a: any, b: any, order: "asc"|"desc"="asc") => + { + const values=[ "> 100000", "> 1 et < 100 000", "≤ 1", "Traces", "Inexistant"]; + if(order === "desc") + values.reverse(); + if(values.indexOf(a) > values.indexOf(b)) + return -1; + else if(values.indexOf(a) < values.indexOf(b)) + return 1; + else + return 0; + }; + converter.datasSortingFunctions=[{ datasFieldNb: 4, sort:mySort }]; + selector=new Selector(converter, 4, { id:"selector1" }); + expect(selector.values).toEqual(["Inexistant","Traces","≤ 1","> 1 et < 100 000","> 100000"]); }); }); @@ -81,131 +134,143 @@ describe("Test des sélecteurs de données", () => expect(document.getElementById("selector2").innerHTML).toEqual(fixtures.selector2HTMLWithLabel); }); - it("Si des valeurs vides sont présentes dans une champ utilisé pour un sélecteur, elles doivent être ignorées.", async () => + it("Doit prendre en compte la possibilité de sélectionner plusieurs valeurs.", () => { - selector.converter.parser.setRemoteSource({ url:"http://localhost:9876/datas/datas1-emtyinfield.csv" }); - await selector.converter.run(); - selector.filter2HTML(); - expect(document.getElementById("selector1").innerHTML).toEqual(fixtures.selector1HTML); - }); - - it("Si des espaces entourent certaines valeurs pour ce champ, ils doivent être supprimés.", async () => - { - selector.converter.parser.setRemoteSource({ url:"http://localhost:9876/datas/datasNeedTrim.csv" }); - await selector.converter.run(); + selector=new Selector(converter, 4, { id:"selector2" }); + selector.isMultiple=true; selector.filter2HTML(); - expect(document.getElementById("selector1").innerHTML).toEqual(fixtures.selectorHTMLWithTrim); - }); - - it("Si un séparateur est fourni, les valeurs distinctes extraites de ce champ doivent le prendre en compte.", async () => - { - converter.parser.setRemoteSource({ url:"http://localhost:9876/datas/datas1+tagsfield.csv" }); - await converter.run(); - selector=new Selector(converter, 5, { id:"selector1" }, "|"); - selector.filter2HTML(); - expect(document.getElementById("selector1").innerHTML).toEqual(fixtures.selector1HTMLWithSeparator); - }); - - it("Si une fonction spécifique est fournie pour le champ utilisé pour ce filtre, elle doit être prise en compte.", () => - { - const mySort=(a: any, b: any, order: "asc"|"desc"="asc") => - { - const values=[ "> 100000", "> 1 et < 100 000", "≤ 1", "Traces", "Inexistant"]; - if(order === "desc") - values.reverse(); - if(values.indexOf(a) > values.indexOf(b)) - return -1; - else if(values.indexOf(a) < values.indexOf(b)) - return 1; - else - return 0; - }; - converter.datasSortingFunctions=[{ datasFieldNb: 4, sort:mySort }]; - selector=new Selector(converter, 4, { id:"selector1" }); - selector.filter2HTML(); - expect(document.getElementById("selector1").innerHTML).toEqual(fixtures.selector1HTMLWithFunction); - }); - - it("Doit générer une erreur, si aucune donnée n'a été trouvée pour créer le