From 4939153d897689614d541d0a8c64bf71509ef079 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabrice=20PENHO=C3=8BT?= Date: Thu, 2 Sep 2021 18:15:15 +0200 Subject: [PATCH] =?UTF-8?q?Ajout=20fonctionnalit=C3=A9=20permettant=20de?= =?UTF-8?q?=20classer=20les=20donn=C3=A9es=20via=20les=20colonnes=20+=20ad?= =?UTF-8?q?aptation=20exemple=20et=20nouvelle=20compilation=20du=20code=20?= =?UTF-8?q?en=20JS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- public/JS/firstExample.app.js | 765 +++++++++++++++++++++++++++++++- public/index.html | 1 + src/build/firstExample.js | 3 +- src/build/freeDatas2HTML.js | 116 ++++- src/errors.js | 1 + src/firstExample.ts | 1 + src/freeDatas2HTML.ts | 95 +++- src/freeDatas2HTMLInterfaces.ts | 5 + tests/fixtures.js | 4 + tests/freeDatas2HTMLSpec.ts | 65 ++- 11 files changed, 1015 insertions(+), 43 deletions(-) diff --git a/package.json b/package.json index 432e5b3..35bad6e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "freedatas2html", - "version": "0.3.4", + "version": "0.3.5", "description": "Visualization of data from various sources (CSV, API, HTML...) with filters, classification, pagination, etc.", "main": "index.js", "scripts": { diff --git a/public/JS/firstExample.app.js b/public/JS/firstExample.app.js index 040a7e0..39b6b3b 100644 --- a/public/JS/firstExample.app.js +++ b/public/JS/firstExample.app.js @@ -1,6 +1,649 @@ /******/ (() => { // webpackBootstrap /******/ var __webpack_modules__ = ({ +/***/ "./node_modules/natural-orderby/esm/natural-orderby.js": +/*!*************************************************************!*\ + !*** ./node_modules/natural-orderby/esm/natural-orderby.js ***! + \*************************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "orderBy": () => (/* binding */ orderBy), +/* harmony export */ "compare": () => (/* binding */ compare) +/* harmony export */ }); +var compareNumbers = function compareNumbers(numberA, numberB) { + if (numberA < numberB) { + return -1; + } + + if (numberA > numberB) { + return 1; + } + + return 0; +}; + +var RE_NUMBERS = /(^0x[\da-fA-F]+$|^([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?(?!\.\d+)(?=\D|\s|$))|\d+)/g; +var RE_LEADING_OR_TRAILING_WHITESPACES = /^\s+|\s+$/g; // trim pre-post whitespace + +var RE_WHITESPACES = /\s+/g; // normalize all whitespace to single ' ' character + +var RE_INT_OR_FLOAT = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/; // identify integers and floats + +var RE_DATE = /(^([\w ]+,?[\w ]+)?[\w ]+,?[\w ]+\d+:\d+(:\d+)?[\w ]?|^\d{1,4}[/-]\d{1,4}[/-]\d{1,4}|^\w+, \w+ \d+, \d{4})/; // identify date strings + +var RE_LEADING_ZERO = /^0+[1-9]{1}[0-9]*$/; +var RE_UNICODE_CHARACTERS = /[^\x00-\x80]/; + +var compareUnicode = function compareUnicode(stringA, stringB) { + var result = stringA.localeCompare(stringB); + return result ? result / Math.abs(result) : 0; +}; + +var stringCompare = function stringCompare(stringA, stringB) { + if (stringA < stringB) { + return -1; + } + + if (stringA > stringB) { + return 1; + } + + return 0; +}; + +var compareChunks = function compareChunks(chunksA, chunksB) { + var lengthA = chunksA.length; + var lengthB = chunksB.length; + var size = Math.min(lengthA, lengthB); + + for (var i = 0; i < size; i++) { + var chunkA = chunksA[i]; + var chunkB = chunksB[i]; + + if (chunkA.normalizedString !== chunkB.normalizedString) { + if (chunkA.normalizedString === '' !== (chunkB.normalizedString === '')) { + // empty strings have lowest value + return chunkA.normalizedString === '' ? -1 : 1; + } + + if (chunkA.parsedNumber !== undefined && chunkB.parsedNumber !== undefined) { + // compare numbers + var result = compareNumbers(chunkA.parsedNumber, chunkB.parsedNumber); + + if (result === 0) { + // compare string value, if parsed numbers are equal + // Example: + // chunkA = { parsedNumber: 1, normalizedString: "001" } + // chunkB = { parsedNumber: 1, normalizedString: "01" } + // chunkA.parsedNumber === chunkB.parsedNumber + // chunkA.normalizedString < chunkB.normalizedString + return stringCompare(chunkA.normalizedString, chunkB.normalizedString); + } + + return result; + } else if (chunkA.parsedNumber !== undefined || chunkB.parsedNumber !== undefined) { + // number < string + return chunkA.parsedNumber !== undefined ? -1 : 1; + } else if (RE_UNICODE_CHARACTERS.test(chunkA.normalizedString + chunkB.normalizedString) && chunkA.normalizedString.localeCompare) { + // use locale comparison only if one of the chunks contains unicode characters + return compareUnicode(chunkA.normalizedString, chunkB.normalizedString); + } else { + // use common string comparison for performance reason + return stringCompare(chunkA.normalizedString, chunkB.normalizedString); + } + } + } // if the chunks are equal so far, the one which has more chunks is greater than the other one + + + if (lengthA > size || lengthB > size) { + return lengthA <= size ? -1 : 1; + } + + return 0; +}; + +var compareOtherTypes = function compareOtherTypes(valueA, valueB) { + if (!valueA.chunks ? valueB.chunks : !valueB.chunks) { + return !valueA.chunks ? 1 : -1; + } + + if (valueA.isNaN ? !valueB.isNaN : valueB.isNaN) { + return valueA.isNaN ? -1 : 1; + } + + if (valueA.isSymbol ? !valueB.isSymbol : valueB.isSymbol) { + return valueA.isSymbol ? -1 : 1; + } + + if (valueA.isObject ? !valueB.isObject : valueB.isObject) { + return valueA.isObject ? -1 : 1; + } + + if (valueA.isArray ? !valueB.isArray : valueB.isArray) { + return valueA.isArray ? -1 : 1; + } + + if (valueA.isFunction ? !valueB.isFunction : valueB.isFunction) { + return valueA.isFunction ? -1 : 1; + } + + if (valueA.isNull ? !valueB.isNull : valueB.isNull) { + return valueA.isNull ? -1 : 1; + } + + return 0; +}; + +var compareValues = function compareValues(valueA, valueB) { + if (valueA.value === valueB.value) { + return 0; + } + + if (valueA.parsedNumber !== undefined && valueB.parsedNumber !== undefined) { + return compareNumbers(valueA.parsedNumber, valueB.parsedNumber); + } + + if (valueA.chunks && valueB.chunks) { + return compareChunks(valueA.chunks, valueB.chunks); + } + + return compareOtherTypes(valueA, valueB); +}; + +var compareMultiple = function compareMultiple(recordA, recordB, orders) { + var indexA = recordA.index, + valuesA = recordA.values; + var indexB = recordB.index, + valuesB = recordB.values; + var length = valuesA.length; + var ordersLength = orders.length; + + for (var i = 0; i < length; i++) { + var order = i < ordersLength ? orders[i] : null; + + if (order && typeof order === 'function') { + var result = order(valuesA[i].value, valuesB[i].value); + + if (result) { + return result; + } + } else { + var _result = compareValues(valuesA[i], valuesB[i]); + + if (_result) { + return _result * (order === 'desc' ? -1 : 1); + } + } + } + + return indexA - indexB; +}; + +var createIdentifierFn = function createIdentifierFn(identifier) { + if (typeof identifier === 'function') { + // identifier is already a lookup function + return identifier; + } + + return function (value) { + if (Array.isArray(value)) { + var index = Number(identifier); + + if (Number.isInteger(index)) { + return value[index]; + } + } else if (value && typeof value === 'object' && typeof identifier !== 'function') { + return value[identifier]; + } + + return value; + }; +}; + +var stringify = function stringify(value) { + if (typeof value === 'boolean' || value instanceof Boolean) { + return Number(value).toString(); + } + + if (typeof value === 'number' || value instanceof Number) { + return value.toString(); + } + + if (value instanceof Date) { + return value.getTime().toString(); + } + + if (typeof value === 'string' || value instanceof String) { + return value.toLowerCase().replace(RE_LEADING_OR_TRAILING_WHITESPACES, ''); + } + + return ''; +}; + +var parseNumber = function parseNumber(value) { + if (value.length !== 0) { + var parsedNumber = Number(value); + + if (!Number.isNaN(parsedNumber)) { + return parsedNumber; + } + } + + return undefined; +}; + +var parseDate = function parseDate(value) { + if (RE_DATE.test(value)) { + var parsedDate = Date.parse(value); + + if (!Number.isNaN(parsedDate)) { + return parsedDate; + } + } + + return undefined; +}; + +var numberify = function numberify(value) { + var parsedNumber = parseNumber(value); + + if (parsedNumber !== undefined) { + return parsedNumber; + } + + return parseDate(value); +}; + +var createChunks = function createChunks(value) { + return value.replace(RE_NUMBERS, '\0$1\0').replace(/\0$/, '').replace(/^\0/, '').split('\0'); +}; + +var normalizeAlphaChunk = function normalizeAlphaChunk(chunk) { + return chunk.replace(RE_WHITESPACES, ' ').replace(RE_LEADING_OR_TRAILING_WHITESPACES, ''); +}; + +var normalizeNumericChunk = function normalizeNumericChunk(chunk, index, chunks) { + if (RE_INT_OR_FLOAT.test(chunk)) { + // don´t parse a number, if there´s a preceding decimal point + // to keep significance + // e.g. 1.0020, 1.020 + if (!RE_LEADING_ZERO.test(chunk) || index === 0 || chunks[index - 1] !== '.') { + return parseNumber(chunk) || 0; + } + } + + return undefined; +}; + +var createChunkMap = function createChunkMap(chunk, index, chunks) { + return { + parsedNumber: normalizeNumericChunk(chunk, index, chunks), + normalizedString: normalizeAlphaChunk(chunk) + }; +}; + +var createChunkMaps = function createChunkMaps(value) { + var chunksMaps = createChunks(value).map(createChunkMap); + return chunksMaps; +}; + +var isFunction = function isFunction(value) { + return typeof value === 'function'; +}; + +var isNaN = function isNaN(value) { + return Number.isNaN(value) || value instanceof Number && Number.isNaN(value.valueOf()); +}; + +var isNull = function isNull(value) { + return value === null; +}; + +var isObject = function isObject(value) { + return value !== null && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Number) && !(value instanceof String) && !(value instanceof Boolean) && !(value instanceof Date); +}; + +var isSymbol = function isSymbol(value) { + return typeof value === 'symbol'; +}; + +var isUndefined = function isUndefined(value) { + return value === undefined; +}; + +var getMappedValueRecord = function getMappedValueRecord(value) { + if (typeof value === 'string' || value instanceof String || (typeof value === 'number' || value instanceof Number) && !isNaN(value) || typeof value === 'boolean' || value instanceof Boolean || value instanceof Date) { + var stringValue = stringify(value); + var parsedNumber = numberify(stringValue); + var chunks = createChunkMaps(parsedNumber ? "" + parsedNumber : stringValue); + return { + parsedNumber: parsedNumber, + chunks: chunks, + value: value + }; + } + + return { + isArray: Array.isArray(value), + isFunction: isFunction(value), + isNaN: isNaN(value), + isNull: isNull(value), + isObject: isObject(value), + isSymbol: isSymbol(value), + isUndefined: isUndefined(value), + value: value + }; +}; + +var getValueByIdentifier = function getValueByIdentifier(value, getValue) { + return getValue(value); +}; + +var getElementByIndex = function getElementByIndex(collection, index) { + return collection[index]; +}; + +var baseOrderBy = function baseOrderBy(collection, identifiers, orders) { + var identifierFns = identifiers.length ? identifiers.map(createIdentifierFn) : [function (value) { + return value; + }]; // temporary array holds elements with position and sort-values + + var mappedCollection = collection.map(function (element, index) { + var values = identifierFns.map(function (identifier) { + return getValueByIdentifier(element, identifier); + }).map(getMappedValueRecord); + return { + index: index, + values: values + }; + }); // iterate over values and compare values until a != b or last value reached + + mappedCollection.sort(function (recordA, recordB) { + return compareMultiple(recordA, recordB, orders); + }); + return mappedCollection.map(function (element) { + return getElementByIndex(collection, element.index); + }); +}; + +var getIdentifiers = function getIdentifiers(identifiers) { + if (!identifiers) { + return []; + } + + var identifierList = !Array.isArray(identifiers) ? [identifiers] : [].concat(identifiers); + + if (identifierList.some(function (identifier) { + return typeof identifier !== 'string' && typeof identifier !== 'number' && typeof identifier !== 'function'; + })) { + return []; + } + + return identifierList; +}; + +var getOrders = function getOrders(orders) { + if (!orders) { + return []; + } + + var orderList = !Array.isArray(orders) ? [orders] : [].concat(orders); + + if (orderList.some(function (order) { + return order !== 'asc' && order !== 'desc' && typeof order !== 'function'; + })) { + return []; + } + + return orderList; +}; + +/** + * Creates an array of elements, natural sorted by specified identifiers and + * the corresponding sort orders. This method implements a stable sort + * algorithm, which means the original sort order of equal elements is + * preserved. + * + * If `collection` is an array of primitives, `identifiers` may be unspecified. + * Otherwise, you should specify `identifiers` to sort by or `collection` will + * be returned unsorted. An identifier can expressed by: + * + * - an index position, if `collection` is a nested array, + * - a property name, if `collection` is an array of objects, + * - a function which returns a particular value from an element of a nested array or an array of objects. This function will be invoked by passing one element of `collection`. + * + * If `orders` is unspecified, all values are sorted in ascending order. + * Otherwise, specify an order of `'desc'` for descending or `'asc'` for + * ascending sort order of corresponding values. You may also specify a compare + * function for an order, which will be invoked by two arguments: + * `(valueA, valueB)`. It must return a number representing the sort order. + * + * @example + * + * import { orderBy } from 'natural-orderby'; + * + * const users = [ + * { + * username: 'Bamm-Bamm', + * ip: '192.168.5.2', + * datetime: 'Fri Jun 15 2018 16:48:00 GMT+0200 (CEST)' + * }, + * { + * username: 'Wilma', + * ip: '192.168.10.1', + * datetime: '14 Jun 2018 00:00:00 PDT' + * }, + * { + * username: 'dino', + * ip: '192.168.0.2', + * datetime: 'June 15, 2018 14:48:00' + * }, + * { + * username: 'Barney', + * ip: '192.168.1.1', + * datetime: 'Thu, 14 Jun 2018 07:00:00 GMT' + * }, + * { + * username: 'Pebbles', + * ip: '192.168.1.21', + * datetime: '15 June 2018 14:48 UTC' + * }, + * { + * username: 'Hoppy', + * ip: '192.168.5.10', + * datetime: '2018-06-15T14:48:00.000Z' + * }, + * ]; + * + * orderBy( + * users, + * [v => v.datetime, v => v.ip], + * ['desc', 'asc'] + * ); + * + * // => [ + * // { + * // username: 'dino', + * // ip: '192.168.0.2', + * // datetime: 'June 15, 2018 14:48:00', + * // }, + * // { + * // username: 'Pebbles', + * // ip: '192.168.1.21', + * // datetime: '15 June 2018 14:48 UTC', + * // }, + * // { + * // username: 'Bamm-Bamm', + * // ip: '192.168.5.2', + * // datetime: 'Fri Jun 15 2018 16:48:00 GMT+0200 (CEST)', + * // }, + * // { + * // username: 'Hoppy', + * // ip: '192.168.5.10', + * // datetime: '2018-06-15T14:48:00.000Z', + * // }, + * // { + * // username: 'Barney', + * // ip: '192.168.1.1', + * // datetime: 'Thu, 14 Jun 2018 07:00:00 GMT', + * // }, + * // { + * // username: 'Wilma', + * // ip: '192.168.10.1', + * // datetime: '14 Jun 2018 00:00:00 PDT', + * // }, + * // ] + */ +function orderBy(collection, identifiers, orders) { + if (!collection || !Array.isArray(collection)) { + return []; + } + + var validatedIdentifiers = getIdentifiers(identifiers); + var validatedOrders = getOrders(orders); + return baseOrderBy(collection, validatedIdentifiers, validatedOrders); +} + +var baseCompare = function baseCompare(options) { + return function (valueA, valueB) { + var a = getMappedValueRecord(valueA); + var b = getMappedValueRecord(valueB); + var result = compareValues(a, b); + return result * (options.order === 'desc' ? -1 : 1); + }; +}; + +var isValidOrder = function isValidOrder(value) { + return typeof value === 'string' && (value === 'asc' || value === 'desc'); +}; + +var getOptions = function getOptions(customOptions) { + var order = 'asc'; + + if (typeof customOptions === 'string' && isValidOrder(customOptions)) { + order = customOptions; + } else if (customOptions && typeof customOptions === 'object' && customOptions.order && isValidOrder(customOptions.order)) { + order = customOptions.order; + } + + return { + order: order + }; +}; + +/** + * Creates a compare function that defines the natural sort order considering + * the given `options` which may be passed to [`Array.prototype.sort()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort). + * + * If `options` or its property `order` is unspecified, values are sorted in + * ascending sort order. Otherwise, specify an order of `'desc'` for descending + * or `'asc'` for ascending sort order of values. + * + * @example + * + * import { compare } from 'natural-orderby'; + * + * const users = [ + * { + * username: 'Bamm-Bamm', + * lastLogin: { + * ip: '192.168.5.2', + * datetime: 'Fri Jun 15 2018 16:48:00 GMT+0200 (CEST)' + * }, + * }, + * { + * username: 'Wilma', + * lastLogin: { + * ip: '192.168.10.1', + * datetime: '14 Jun 2018 00:00:00 PDT' + * }, + * }, + * { + * username: 'dino', + * lastLogin: { + * ip: '192.168.0.2', + * datetime: 'June 15, 2018 14:48:00' + * }, + * }, + * { + * username: 'Barney', + * lastLogin: { + * ip: '192.168.1.1', + * datetime: 'Thu, 14 Jun 2018 07:00:00 GMT' + * }, + * }, + * { + * username: 'Pebbles', + * lastLogin: { + * ip: '192.168.1.21', + * datetime: '15 June 2018 14:48 UTC' + * }, + * }, + * { + * username: 'Hoppy', + * lastLogin: { + * ip: '192.168.5.10', + * datetime: '2018-06-15T14:48:00.000Z' + * }, + * }, + * ]; + * + * users.sort((a, b) => compare()(a.ip, b.ip)); + * + * // => [ + * // { + * // username: 'dino', + * // ip: '192.168.0.2', + * // datetime: 'June 15, 2018 14:48:00' + * // }, + * // { + * // username: 'Barney', + * // ip: '192.168.1.1', + * // datetime: 'Thu, 14 Jun 2018 07:00:00 GMT' + * // }, + * // { + * // username: 'Pebbles', + * // ip: '192.168.1.21', + * // datetime: '15 June 2018 14:48 UTC' + * // }, + * // { + * // username: 'Bamm-Bamm', + * // ip: '192.168.5.2', + * // datetime: 'Fri Jun 15 2018 16:48:00 GMT+0200 (CEST)' + * // }, + * // { + * // username: 'Hoppy', + * // ip: '192.168.5.10', + * // datetime: '2018-06-15T14:48:00.000Z' + * // }, + * // { + * // username: 'Wilma', + * // ip: '192.168.10.1', + * // datetime: '14 Jun 2018 00:00:00 PDT' + * // } + * // ] + */ +function compare(options) { + var validatedOptions = getOptions(options); + return baseCompare(validatedOptions); +} + +/* +* Javascript natural sort algorithm with unicode support +* based on chunking idea by Dave Koelle +* +* https://github.com/yobacca/natural-sort-order +* released under MIT License +*/ + + + + +/***/ }), + /***/ "./node_modules/papaparse/papaparse.min.js": /*!*************************************************!*\ !*** ./node_modules/papaparse/papaparse.min.js ***! @@ -69,11 +712,13 @@ var __generator = (undefined && undefined.__generator) || function (thisArg, bod }; var Papa = __webpack_require__(/*! papaparse */ "./node_modules/papaparse/papaparse.min.js"); var errors = __webpack_require__(/*! ./errors.js */ "./src/errors.js"); +var compare = __webpack_require__(/*! natural-orderby */ "./node_modules/natural-orderby/esm/natural-orderby.js").compare; var freeDatas2HTML = (function () { function freeDatas2HTML() { this._datasViewElt = { id: "", eltDOM: undefined }; this._datasSourceUrl = ""; this._datasSelectors = []; + this._datasSortingColumns = []; this.parseMeta = undefined; this.parseDatas = []; this.parseErrors = []; @@ -114,10 +759,12 @@ var freeDatas2HTML = (function () { checkContainerExist = document.getElementById(selectionElts[i].id); if (checkContainerExist === null) console.error(errors.elementNotFound + selectionElts[i].id); - else if (Number.isInteger(selectionElts[i].datasFielNb) === false || selectionElts[i].datasFielNb < 0) + else if (Number.isInteger(selectionElts[i].datasFieldNb) === false || selectionElts[i].datasFieldNb < 0) 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]); } } @@ -125,6 +772,24 @@ var freeDatas2HTML = (function () { enumerable: false, configurable: true }); + Object.defineProperty(freeDatas2HTML.prototype, "datasSortingColumns", { + get: function () { + return this._datasSortingColumns; + }, + set: function (sortingColumns) { + this._datasSortingColumns = []; + for (var i = 0; i < sortingColumns.length; i++) { + if (Number.isInteger(sortingColumns[i].datasFieldNb) === false || sortingColumns[i].datasFieldNb < 0) + console.error(errors.needNaturalNumber); + else { + sortingColumns[i].order = undefined; + this._datasSortingColumns.push(sortingColumns[i]); + } + } + }, + enumerable: false, + configurable: true + }); freeDatas2HTML.prototype.parse = function () { return __awaiter(this, void 0, void 0, function () { var converter; @@ -162,7 +827,7 @@ var freeDatas2HTML = (function () { }; freeDatas2HTML.prototype.run = function () { return __awaiter(this, void 0, void 0, function () { - var converter_1, selectorsHTML, i, values, colName, row, j, selectElement; + var converter_1, selectorsHTML, i, values, colName, row, checkedValue, checkedValues, i_1, checkedValue, j, selectElement, i; return __generator(this, function (_a) { switch (_a.label) { case 0: @@ -179,21 +844,30 @@ var freeDatas2HTML = (function () { console.error(this.parseErrors); else { converter_1 = this; - this.datasHTML = this.createDatasHTML(this.parseMeta.fields, this.parseDatas); - this._datasViewElt.eltDOM.innerHTML = this.datasHTML; if (this._datasSelectors.length > 0) { selectorsHTML = []; for (i in this._datasSelectors) { - if (this._datasSelectors[i].datasFielNb > (this.parseMeta.fields.length - 1)) + if (this._datasSelectors[i].datasFieldNb > (this.parseMeta.fields.length - 1)) throw new Error(errors.selectorFieldNotFound); else { - values = [], colName = this.parseMeta.fields[this._datasSelectors[i].datasFielNb]; + values = [], colName = this.parseMeta.fields[this._datasSelectors[i].datasFieldNb]; for (row in this.parseDatas) { - if (values.indexOf(this.parseDatas[row][colName].trim()) === -1) - values.push(this.parseDatas[row][colName].trim()); + if (this._datasSelectors[i].separator === undefined) { + checkedValue = this.parseDatas[row][colName].trim(); + if (checkedValue !== "" && values.indexOf(checkedValue) === -1) + values.push(checkedValue); + } + else { + checkedValues = this.parseDatas[row][colName].split(this._datasSelectors[i].separator); + for (i_1 in checkedValues) { + checkedValue = checkedValues[i_1].trim(); + if (checkedValue !== "" && values.indexOf(checkedValue) === -1) + values.push(checkedValue); + } + } } if (values.length > 0) { - values.sort(); + values.sort(compare()); this._datasSelectors[i].name = colName; this._datasSelectors[i].values = values; selectorsHTML[i] = ""; @@ -171,12 +202,18 @@ var freeDatas2HTML = (function () { selectElement = document.getElementById("freeDatas2HTMLSelector" + i); selectElement.addEventListener('change', function (e) { converter_1.datasHTML = converter_1.createDatasHTML(converter_1.parseMeta.fields, converter_1.parseDatas); - converter_1._datasViewElt.eltDOM.innerHTML = converter_1.datasHTML; + converter_1.refreshView(); }); } } } } + for (i in this._datasSortingColumns) { + if (this._datasSortingColumns[i].datasFieldNb > (this.parseMeta.fields.length - 1)) + throw new Error(errors.sortingColumnsFieldNotFound); + } + this.datasHTML = this.createDatasHTML(this.parseMeta.fields, this.parseDatas); + this.refreshView(); return [2, true]; } return [2]; @@ -184,12 +221,50 @@ var freeDatas2HTML = (function () { }); }); }; + freeDatas2HTML.prototype.refreshView = function () { + if (this._datasViewElt.eltDOM !== undefined) { + var converter_2 = this; + this._datasViewElt.eltDOM.innerHTML = this.datasHTML; + if (this._datasSortingColumns.length > 0) { + var getTableTh = document.querySelectorAll("table th"); + if (getTableTh !== null) { + var _loop_1 = function (i) { + var datasFieldNb = this_1._datasSortingColumns[i].datasFieldNb; + var htmlContent = getTableTh[datasFieldNb].innerHTML; + htmlContent = "" + htmlContent + ""; + getTableTh[datasFieldNb].innerHTML = htmlContent; + var sortingElement = document.getElementById("freeDatas2HTMLSorting" + datasFieldNb); + sortingElement.addEventListener('click', function (e) { + e.preventDefault(); + var order = converter_2.datasSortingColumns[i].order; + if (order === undefined || order === "desc") + converter_2.datasSortingColumns[i].order = "asc"; + else + converter_2.datasSortingColumns[i].order = "desc"; + converter_2._datasSortedColumn = converter_2.datasSortingColumns[i]; + converter_2.datasHTML = converter_2.createDatasHTML(converter_2.parseMeta.fields, converter_2.parseDatas); + converter_2.refreshView(); + }); + }; + var this_1 = this; + for (var i in this._datasSortingColumns) { + _loop_1(i); + } + } + } + } + }; freeDatas2HTML.prototype.createDatasHTML = function (fields, datas) { var checkSelectorExist, filters = []; for (var i in this._datasSelectors) { checkSelectorExist = document.querySelector("#" + this._datasSelectors[i].id + " select"); - if (checkSelectorExist != null && checkSelectorExist.value != "0") - filters.push({ field: this._datasSelectors[i].name, value: this._datasSelectors[i].values[checkSelectorExist.selectedIndex - 1] }); + 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 }); + } + if (this._datasSortedColumn !== undefined) { + var col_1 = fields[this._datasSortedColumn.datasFieldNb]; + var colOrder_1 = this._datasSortedColumn.order; + datas.sort(function (a, b) { return compare({ order: colOrder_1 })(a[col_1], b[col_1]); }); } var datasHTML = ""; for (var i in fields) @@ -199,8 +274,21 @@ var freeDatas2HTML = (function () { var visible = true; if (filters.length !== 0) { for (var i in filters) { - if (datas[row][filters[i].field].trim() != filters[i].value) - visible = false; + if (filters[i].separator === undefined) { + if (datas[row][filters[i].field].trim() != filters[i].value) + visible = false; + } + else { + var checkedValues = datas[row][filters[i].field].split(filters[i].separator), finded = false; + for (var j in checkedValues) { + if (checkedValues[j].trim() === filters[i].value) { + finded = true; + break; + } + } + if (!finded) + visible = false; + } } } if (visible) { diff --git a/src/errors.js b/src/errors.js index 5628da4..42aaeb1 100644 --- a/src/errors.js +++ b/src/errors.js @@ -7,4 +7,5 @@ module.exports = needUrl: "Merci de fournir une url valide pour le fichier CSV à parser.", parserFail: "La lecture des données du fichier a échoué.", selectorFieldNotFound: "Au moins une des colonnes devant servir à filtrer les données n'existe pas dans le fichier.", + sortingColumnsFieldNotFound: "Au moins une des colonnes devant servir à classer les données n'existe pas dans le fichier.", }; \ No newline at end of file diff --git a/src/firstExample.ts b/src/firstExample.ts index 21cfd9c..bc1fa98 100644 --- a/src/firstExample.ts +++ b/src/firstExample.ts @@ -7,6 +7,7 @@ const initialise = async () => let converter=new freeDatas2HTML(); converter.datasViewElt={ id:"datas" }; converter.datasSelectors=[{ datasFieldNb:3, id:"filtre1"}, { datasFieldNb:4, id:"filtre2"},{ datasFieldNb:5, id:"filtre3", separator:"," }]; + converter.datasSortingColumns=[{ datasFieldNb:0 }, { datasFieldNb:1 },{ datasFieldNb:2 }]; converter.datasSourceUrl="http://localhost:8080/datas/elements-chimiques.csv"; await converter.run(); } diff --git a/src/freeDatas2HTML.ts b/src/freeDatas2HTML.ts index ced3a84..f854357 100644 --- a/src/freeDatas2HTML.ts +++ b/src/freeDatas2HTML.ts @@ -3,13 +3,15 @@ const errors = require("./errors.js"); const { compare }= require('natural-orderby'); import { papaParseDatas, papaParseErrors, papaParseMeta } from "./papaParseInterfaces"; -import { domElement, selectors } from "./freeDatas2HTMLInterfaces"; +import { domElement, selectors, sortingColumns } from "./freeDatas2HTMLInterfaces"; export class freeDatas2HTML { private _datasViewElt: domElement = { id:"", eltDOM:undefined }; private _datasSourceUrl: string = ""; private _datasSelectors: selectors[] = []; + private _datasSortingColumns: sortingColumns[] = []; + private _datasSortedColumn : sortingColumns|undefined; public parseMeta: papaParseMeta|undefined = undefined; public parseDatas: papaParseDatas[] = []; @@ -46,7 +48,7 @@ export class freeDatas2HTML checkContainerExist=document.getElementById(selectionElts[i].id); if(checkContainerExist === null) console.error(errors.elementNotFound+selectionElts[i].id); - else if(Number.isInteger( selectionElts[i].datasFieldNb) === false || selectionElts[i].datasFieldNb < 0) + else if(Number.isInteger(selectionElts[i].datasFieldNb) === false || selectionElts[i].datasFieldNb < 0) console.error(errors.needNaturalNumber); else { @@ -57,12 +59,32 @@ export class freeDatas2HTML } } } - + get datasSelectors() : selectors[] { return this._datasSelectors; } - + + set datasSortingColumns(sortingColumns: sortingColumns[]) + { + this._datasSortingColumns=[]; + for(let i = 0; i < sortingColumns.length; i++) + { + if(Number.isInteger(sortingColumns[i].datasFieldNb) === false || sortingColumns[i].datasFieldNb < 0) + console.error(errors.needNaturalNumber); + else + { + sortingColumns[i].order=undefined; + this._datasSortingColumns.push(sortingColumns[i]); + } + } + } + + get datasSortingColumns() : sortingColumns[] + { + return this._datasSortingColumns; + } + public async parse(): Promise { const converter=this; @@ -118,9 +140,7 @@ export class freeDatas2HTML else { let converter=this; - // Affichage initial de toutes les données du fichier - this.datasHTML=this.createDatasHTML(this.parseMeta!.fields, this.parseDatas); - this._datasViewElt.eltDOM.innerHTML=this.datasHTML; + // Si demandé, création des listes permettant de filter les données if(this._datasSelectors.length > 0) { @@ -168,19 +188,67 @@ export class freeDatas2HTML selectElement.addEventListener('change', function(e) { converter.datasHTML=converter.createDatasHTML(converter.parseMeta!.fields as string[], converter.parseDatas); - converter._datasViewElt.eltDOM!.innerHTML=converter.datasHTML; + 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 la tableau soit déjà dans le DOM pour écouter 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=""+htmlContent+""; + 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 { - // Je vérifie si des valeurs ont été sélectionnées pour filter les données. + // Dois-je filtrer les données ? let checkSelectorExist: HTMLSelectElement|null, filters: any[] = []; for(let i in this._datasSelectors) { @@ -189,6 +257,15 @@ export class freeDatas2HTML 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; + datas.sort( (a, b) => compare( {order: colOrder} )(a[col], b[col])); + } + // Création du tableau de données : let datasHTML="
"; for (let i in fields) diff --git a/src/freeDatas2HTMLInterfaces.ts b/src/freeDatas2HTMLInterfaces.ts index 4e1d102..d31fe88 100644 --- a/src/freeDatas2HTMLInterfaces.ts +++ b/src/freeDatas2HTMLInterfaces.ts @@ -9,4 +9,9 @@ export interface selectors extends domElement separator?: string; name?: string; values? : string[]; +} +export interface sortingColumns +{ + datasFieldNb: number; + order?: "asc"|"desc"|undefined; } \ No newline at end of file diff --git a/tests/fixtures.js b/tests/fixtures.js index 27c6369..699a707 100644 --- a/tests/fixtures.js +++ b/tests/fixtures.js @@ -9,4 +9,8 @@ module.exports = datasHTMLFor2Select:'
Z (numéro atomique)ÉlémentSymboleFamilleAbondance des éléments dans la croûte terrestre (μg/k)
2HéliumHeGaz noble> 1 et < 100 000
10NéonNeGaz noble> 1 et < 100 000
18ArgonArGaz noble> 1 et < 100 000
', datasHTMLFor2SelectNone:'
Z (numéro atomique)ÉlémentSymboleFamilleAbondance des éléments dans la croûte terrestre (μg/k)
', datasHTMLForSelectTagsField:'
Z (numéro atomique)ÉlémentSymboleFamilleAbondance des éléments dans la croûte terrestre (μg/k)Étiquettes
118OganessonOgIndéfinieInexistantExemple10
', + sortingColumn1HTML: 'Z (numéro atomique)', + sortingColumn2HTML: 'Symbole', + datasHTMLFor2Select1Clic: '
Z (numéro atomique)ÉlémentSymboleFamilleAbondance des éléments dans la croûte terrestre (μg/k)
18ArgonArGaz noble> 1 et < 100 000
2HéliumHeGaz noble> 1 et < 100 000
10NéonNeGaz noble> 1 et < 100 000
', + datasHTMLFor2Select2Clic: '
Z (numéro atomique)ÉlémentSymboleFamilleAbondance des éléments dans la croûte terrestre (μg/k)
10NéonNeGaz noble> 1 et < 100 000
2HéliumHeGaz noble> 1 et < 100 000
18ArgonArGaz noble> 1 et < 100 000
', } \ No newline at end of file diff --git a/tests/freeDatas2HTMLSpec.ts b/tests/freeDatas2HTMLSpec.ts index 9349736..44994cd 100644 --- a/tests/freeDatas2HTMLSpec.ts +++ b/tests/freeDatas2HTMLSpec.ts @@ -50,6 +50,16 @@ describe("freeDatas2HTML", () => expect(converter.datasSelectors.length).toEqual(1); expect(converter.datasSelectors[0].id).toEqual("selector1"); }); + + it("Ne doit accepter que les colonnes de classement pour lesquelles les numéros fournis sont des nombres naturels.", () => + { + converter.datasSortingColumns=[{ datasFieldNb:2.3 },{ datasFieldNb:3 }]; + expect(converter.datasSortingColumns.length).toEqual(1); + converter.datasSortingColumns=[{ datasFieldNb:2 },{ datasFieldNb:-1 }]; + expect(converter.datasSortingColumns.length).toEqual(1); + converter.datasSortingColumns=[{ datasFieldNb:2 },{ datasFieldNb:3 }]; + expect(converter.datasSortingColumns.length).toEqual(2); + }); it("Si un séparateur vide est fourni pour un sélecteur, il doit être ignoré.", () => { @@ -153,7 +163,7 @@ describe("freeDatas2HTML", () => expect(txtDatasViewsElt).toEqual(fixtures.datasHTML); }); }); - + describe("Création et action des sélecteurs permettant de filter les données affichées.", () => { beforeEach( () => @@ -243,5 +253,56 @@ describe("freeDatas2HTML", () => expect(txtDatasViewsElt).toEqual(fixtures.datasHTMLForSelectTagsField); }); }); - + + describe("Création et action des colonnes permettant de classer les données affichées.", () => + { + beforeEach( () => + { + converter.datasViewElt={ id:"datas" }; + converter.datasSourceUrl="http://localhost:9876/datas/datas1.csv"; + }); + + it("Doit générer une erreur si au moins un des numéros de colonne de classement fournis pour ne correspond pas à une des colonne du fichier.", async () => + { + converter.datasSortingColumns=[{ datasFieldNb:0 },{ datasFieldNb:5 }]; + await expectAsync(converter.run()).toBeRejectedWith(new Error(errors.sortingColumnsFieldNotFound)); + }); + + it("Ne doit pas pas générer d'erreur si tous les numéros de colonne de classement fournis correspondent à des colonnes du fichier.", async () => + { + converter.datasSortingColumns=[{ datasFieldNb:3 },{ datasFieldNb:4 }]; + await expectAsync(converter.run()).not.toBeRejected(); + }); + + it("Pour chaque colonne de classement demandée, doit générer un lien hypertexte dans l'entête de la colonne.", async () => + { + converter.datasSortingColumns=[{ datasFieldNb:0 },{ datasFieldNb:2 }]; + await converter.run(); + let getTableTh=document.querySelectorAll("table th"); + expect(getTableTh[0].innerHTML).toEqual(fixtures.sortingColumn1HTML); + expect(getTableTh[2].innerHTML).toEqual(fixtures.sortingColumn2HTML); + }); + + it("Le 1er click sur l'entête d'une des colonnes doit classer les données dans le sens ascendant, puis descendant et ainsi de suite, en prenant en compte les éventuels filtres.", async () => + { + converter.datasSelectors=[{ datasFieldNb:3, id:"selector1"},{ datasFieldNb:4, id:"selector2"}]; + converter.datasSortingColumns=[{ datasFieldNb:2 }]; + await converter.run(); + let selectElement = document.getElementById("freeDatas2HTMLSelector0") as HTMLInputElement; + selectElement.value="2"; + selectElement = document.getElementById("freeDatas2HTMLSelector1") as HTMLInputElement; + selectElement.value="1"; + selectElement.dispatchEvent(new Event('change')); + let getTableThLink=document.querySelector("table th a") as HTMLElement; + getTableThLink.click();// tri ascendant + let txtDatasViewsElt=document.getElementById("datas").innerHTML; + expect(txtDatasViewsElt).toEqual(fixtures.datasHTMLFor2Select1Clic); + getTableThLink.click();// tri descendant + txtDatasViewsElt=document.getElementById("datas").innerHTML; + expect(txtDatasViewsElt).toEqual(fixtures.datasHTMLFor2Select2Clic); + getTableThLink.click();// de nouveau ascendant + txtDatasViewsElt=document.getElementById("datas").innerHTML; + expect(txtDatasViewsElt).toEqual(fixtures.datasHTMLFor2Select1Clic); + }); + }); }); \ No newline at end of file