json-rules-filter 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Adrian Barro Estévez
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "json-rules-filter",
3
+ "version": "1.0.0",
4
+ "description": "para crear reglas avanzadas de filtrado sobre datasets JSON de forma dinámica y visual.",
5
+ "main": "src/jquery.jsonRulesFilter.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1",
8
+ "dev": "vite",
9
+ "build": "vite build"
10
+ },
11
+ "keywords": [
12
+ "jquery-plugin",
13
+ "ecosystem:jquery",
14
+ "filter",
15
+ "rules",
16
+ "datatable-filter"
17
+ ],
18
+ "author": "Adrian Barro Estévez",
19
+ "license": "MIT",
20
+ "type": "commonjs",
21
+ "dependencies": {
22
+ "jquery": ">=3.6.0",
23
+ "select2": "^4.1.0-rc.0",
24
+ "bootstrap": "^5.3.0",
25
+ "@fortawesome/fontawesome-free": "^6.4.0"
26
+ },
27
+ "devDependencies": {
28
+ "vite": "^4.4.0"
29
+ },
30
+ "peerDependencies": {
31
+ "jquery": ">=1.7"
32
+ }
33
+ }
package/readme.md ADDED
@@ -0,0 +1,240 @@
1
+ # 📘 jsonRulesFilter.js
2
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
3
+ [![npm version](https://badge.fury.io/js/logic-filter.svg)](https://www.npmjs.com/package/logic-filter)
4
+ [![jQuery Plugin](https://img.shields.io/badge/jQuery-Plugin-blue.svg)](https://jquery.com/)
5
+
6
+ Plugin de **jQuery** para crear reglas avanzadas de filtrado sobre datasets JSON de forma dinámica y visual.
7
+
8
+ Permite construir filtros tipo:
9
+ - Texto (contiene, empieza por, etc.)
10
+ - Números (mayor que, menor que, etc.)
11
+ - Select (valores únicos del dataset)
12
+
13
+ ---
14
+
15
+ ## 🚀 Instalación
16
+ Install via **npm**:
17
+
18
+ ```bash
19
+ npm install json-rules-filter
20
+ ```
21
+
22
+ Incluye las dependencias necesarias:
23
+
24
+ ```html
25
+ <!-- jQuery -->
26
+ <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
27
+
28
+ <!-- Bootstrap (opcional, para estilos) -->
29
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet">
30
+
31
+ <!-- Select2 -->
32
+ <link href="https://cdn.jsdelivr.net/npm/select2@4.1.0/dist/css/select2.min.css" rel="stylesheet">
33
+ <script src="https://cdn.jsdelivr.net/npm/select2@4.1.0/dist/js/select2.min.js"></script>
34
+
35
+ <!-- FontAwesome (opcional para iconos) -->
36
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
37
+
38
+ <!-- jsonRulesFilter -->
39
+ <script src="jsonRulesFilter.js"></script>
40
+ ```
41
+
42
+ ---
43
+
44
+ ## ⚙️ Uso básico
45
+
46
+ ```javascript
47
+ const data = [
48
+ { name: "Juan", total: 100, cargo: "Admin" },
49
+ { name: "Ana", total: 200, cargo: "User" },
50
+ { name: "Luis", total: 150, cargo: "Admin" }
51
+ ];
52
+
53
+ $("#miFiltro").jsonRulesFilter(data, {
54
+ onApply: function (rules, filteredData) {
55
+ console.log("Reglas:", rules);
56
+ console.log("Resultados:", filteredData);
57
+ }
58
+ });
59
+ ```
60
+
61
+ ---
62
+
63
+ ## 🧠 Configuración
64
+
65
+ ### filters
66
+
67
+ Define las columnas disponibles para filtrar.
68
+
69
+ ```javascript
70
+ filters: [
71
+ {
72
+ name: "Total",
73
+ type: "number",
74
+ data: "total",
75
+ render: function(data) {
76
+ return data.text;
77
+ }
78
+ }
79
+ ]
80
+ ```
81
+
82
+ ---
83
+
84
+ ### language
85
+
86
+ Personaliza los operadores de filtrado.
87
+
88
+ ```javascript
89
+ language: {
90
+ string: {
91
+ sContain: "Contiene",
92
+ sEquals: "Igual a"
93
+ },
94
+ number: {
95
+ nEquals: "Igual a",
96
+ nGreatherThan: "Mayor que"
97
+ }
98
+ }
99
+ ```
100
+
101
+ ---
102
+
103
+ ### buttons
104
+
105
+ Configura textos y estilos de botones.
106
+
107
+ ```javascript
108
+ buttons: {
109
+ reset: {
110
+ text: "Resetear",
111
+ className: "btn btn-link"
112
+ },
113
+ dropdown: {
114
+ text: "Añadir regla",
115
+ className: "btn btn-secondary"
116
+ },
117
+ apply: {
118
+ text: "Aplicar",
119
+ className: "btn btn-primary"
120
+ }
121
+ }
122
+ ```
123
+
124
+ ---
125
+
126
+ ### title
127
+
128
+ Título del componente.
129
+
130
+ ```javascript
131
+ title: "Reglas de filtrado"
132
+ ```
133
+
134
+ ---
135
+
136
+ ### onApply
137
+
138
+ Callback ejecutado al aplicar filtros.
139
+
140
+ ```javascript
141
+ onApply: function (rules, filteredData) {
142
+ }
143
+ ```
144
+
145
+ ---
146
+
147
+ ## 🧩 Tipos de filtros
148
+
149
+ ### Texto (text)
150
+
151
+ - Contiene
152
+ - No contiene
153
+ - Igual a
154
+ - Diferente de
155
+ - Empieza con
156
+ - Termina con
157
+
158
+ ---
159
+
160
+ ### Número (number)
161
+
162
+ - Igual a
163
+ - Diferente de
164
+ - Mayor que
165
+ - Mayor o igual que
166
+ - Menor que
167
+ - Menor o igual que
168
+
169
+ ---
170
+
171
+ ### Select (select)
172
+
173
+ - Genera valores únicos automáticamente
174
+ - Permite selección múltiple
175
+
176
+ ---
177
+
178
+ ## 🛠️ Métodos públicos
179
+
180
+ ```javascript
181
+ $("#miFiltro").jsonRulesFilter("nombreMetodo", parametros);
182
+ ```
183
+
184
+ ### updateData
185
+
186
+ ```javascript
187
+ $("#miFiltro").jsonRulesFilter("updateData", nuevosDatos);
188
+ ```
189
+
190
+ ### getAplicatedRules
191
+
192
+ ```javascript
193
+ const reglas = $("#miFiltro").jsonRulesFilter("getAplicatedRules");
194
+ ```
195
+
196
+ ### reset
197
+
198
+ ```javascript
199
+ $("#miFiltro").jsonRulesFilter("reset");
200
+ ```
201
+
202
+ ---
203
+
204
+ ## 🧪 Ejemplo completo
205
+
206
+ ```javascript
207
+ const empleados = [
208
+ { name: "Carlos", total: 300, cargo: "Manager" },
209
+ { name: "Laura", total: 120, cargo: "Developer" },
210
+ { name: "Pedro", total: 220, cargo: "Developer" }
211
+ ];
212
+
213
+ $("#filtros").jsonRulesFilter(empleados, {
214
+ filters: [
215
+ { name: "Nombre", type: "text", data: "name" },
216
+ { name: "Total", type: "number", data: "total" },
217
+ { name: "Cargo", type: "select", data: "cargo" }
218
+ ],
219
+ onApply: function (rules, results) {
220
+ console.log(results);
221
+ }
222
+ });
223
+ ```
224
+
225
+ ---
226
+
227
+ ## 📌 Notas
228
+
229
+ - Soporta propiedades anidadas (ej: user.name)
230
+ - Usa lógica AND entre reglas
231
+ - Basado en Select2
232
+ - Permite múltiples instancias
233
+
234
+ ---
235
+
236
+ ## 💡 Ideas futuras
237
+
238
+ - Lógica OR
239
+ - Guardado de filtros
240
+ - Exportar/importar reglas
@@ -0,0 +1,308 @@
1
+ (function (factory) {
2
+ if (typeof define === 'function' && define.amd) {
3
+ define(['jquery'], factory);
4
+ } else if (typeof module === 'object' && module.exports) {
5
+ module.exports = factory(require('jquery'));
6
+ } else {
7
+ factory(jQuery);
8
+ }
9
+ }(function ($) {
10
+ $.fn.jsonRulesFilter = function (data, options) {
11
+
12
+ // --- COMMAND MANAGEMENT (Method calling via string) ---
13
+ if (typeof data === 'string') {
14
+ const instance = $(this).data('rulesControl');
15
+ if (instance && typeof instance[data] === 'function') {
16
+ return instance[data].apply(instance, Array.prototype.slice.call(arguments, 1));
17
+ }
18
+ return this;
19
+ }
20
+
21
+ // --- DEFAULT CONFIGURATION ---
22
+ const settings = $.extend(true, {
23
+ filters: [
24
+ { name: "Total", type: "number", data: "total", render: function(data){return data.text/*Callback to render html in options type select*/}},
25
+ { name: "Nombre", type: "text", data: "name", render: function(data){return data.text}},
26
+ { name: "Cargo", type: "select", data: "cargo", render: function(data){return data.text}}
27
+ ],
28
+ language: {
29
+ string: {
30
+ sContain: "Contiene", sNotContain: "No contiene", sEquals: "Igual a",
31
+ sDifference: "Diferente de", sStartWith: "Empieza con", sEndWith: "Termina con"
32
+ },
33
+ number: {
34
+ nEquals: "Igual a", nDifference: "Diferente de", nGreatherThan: "Mayor que",
35
+ nGreatherThanOrEquals: "Mayor o igual que", nLowerThan: "Menor que", nLowerThanOrEquals: "Menor o igual que"
36
+ }
37
+ },
38
+ title: "Reglas de filtrado",
39
+ buttons: {
40
+ reset: { text: "Resetear", className: "fw-semibold link-danger link-offset-2 link-underline link-underline-opacity-0" },
41
+ dropdown: { text: "Añadir regla", className: "btn btn-secondary" },
42
+ apply: { text: "Aplicar regla", className: "btn btn-primary" },
43
+ },
44
+ onApply: function (filtros, datosFiltrados) { }
45
+ }, options);
46
+
47
+ return this.each(function () {
48
+ const $contenedor = $(this);
49
+
50
+ // --- CONTROL OBJECT (Core Logic) ---
51
+ const control = {
52
+ data: data,
53
+ settings: settings,
54
+ $contenedor: $contenedor,
55
+
56
+ // Updates dataset from external source and refreshes the UI
57
+ updateData: function (newData) {
58
+ this.data = newData;
59
+ this.reset();
60
+ },
61
+
62
+ // Collects all rules currently defined in the DOM
63
+ getAplicatedRules: function () {
64
+ const rules_items = $contenedor.find("select[name=select-rule-option]");
65
+ let rulesAplicated = [];
66
+ $.each(rules_items, function (index, element) {
67
+ const $el = $(element);
68
+ const id = $el.data("id");
69
+
70
+ // Mapping DOM data to rule object
71
+ rulesAplicated.push({
72
+ optionFilter: $el.val(), // Selected operator or array of values (if select)
73
+ searchValue: $(`#input-rule-search-${id}`).val(), // Text input value
74
+ typeFilter: $el.data("type"), // Data type (number, text, select)
75
+ dataField: $el.data("field"), // JSON key path
76
+ dataName: $el.data("name"), // Display name
77
+ // Text of the selected option for labels/summary
78
+ optionFilterText: $el.find("option:selected").map(function(){ return $(this).text(); }).get()
79
+ });
80
+ })
81
+ return rulesAplicated;
82
+ },
83
+
84
+ // Initialize UI components
85
+ init: function () {
86
+ this.$contenedor.empty();
87
+ this.render();
88
+ this.bindEvents();
89
+ },
90
+
91
+ // Render main plugin skeleton
92
+ render: function () {
93
+ let columnasHtml = '<div class="row g-2">';
94
+ this.settings.filters.forEach((col, index) => {
95
+ let icon = col.type === "number" ? "fa-solid fa-hashtag" : (col.type === "string" ? "fa-solid fa-language" : "fa-solid fa-circle-chevron-down");
96
+ columnasHtml += `
97
+ <li class="col-6 list-unstyled">
98
+ <a class="dropdown-item dropdown-rules-item" href="#" data-column-type="${col.type}" data-column-data="${col.data}" data-column-name="${col.name}">
99
+ <i class="${icon}"></i> ${col.name}
100
+ </a>
101
+ </li>`;
102
+ if (index % 2 !== 0 && index !== 0) columnasHtml += '</div><div class="row g-2">';
103
+ });
104
+ columnasHtml += "</div>";
105
+
106
+ let template = `
107
+ <div class="d-flex justify-content-between">
108
+ <b><h4>${this.settings.title}</h4></b>
109
+ <p><a class="${this.settings.buttons.reset.className} remove-rules-containers" href="#">${this.settings.buttons.reset.text}</a></p>
110
+ </div>
111
+ <div class="py-2" id="container-rules-filters"></div>
112
+ <div class="mt-2">
113
+ <div class="d-flex justify-content-start dropdown">
114
+ <button data-bs-popper-config='{"strategy":"fixed"}' class="${this.settings.buttons.dropdown.className} dropdown-toggle" type="button" data-bs-toggle="dropdown">
115
+ ${this.settings.buttons.dropdown.text}
116
+ </button>
117
+ <div class="dropdown-menu p-2 dropdown-rules-columns" style="min-width: 300px;">
118
+ ${columnasHtml}
119
+ </div>
120
+ </div>
121
+ <div class="mt-2 d-flex justify-content-start">
122
+ <button type="button" id="apply-rules-btn" class="${this.settings.buttons.apply.className}">${this.settings.buttons.apply.text}</button>
123
+ </div>
124
+ </div>`;
125
+ this.$contenedor.append(template);
126
+ },
127
+
128
+ // Event delegation and listeners
129
+ bindEvents: function () {
130
+ const self = this;
131
+
132
+ // Add new rule row
133
+ this.$contenedor.find(".dropdown-rules-item").on("click", function (e) {
134
+ e.preventDefault();
135
+ self.addRuleRow($(this).data("column-type"), $(this).data("column-data"), $(this).data("column-name"));
136
+ });
137
+
138
+ // Clear all rules
139
+ this.$contenedor.find(".remove-rules-containers").on("click", function (e) {
140
+ e.preventDefault();
141
+ self.reset();
142
+ });
143
+
144
+ // Apply current filters to data
145
+ this.$contenedor.find("#apply-rules-btn").on("click", function (e) {
146
+ e.preventDefault();
147
+ self.applyRules();
148
+ });
149
+ },
150
+
151
+ // Resets rule container and re-runs filtering (show all)
152
+ reset: function () {
153
+ $("#container-rules-filters").empty();
154
+ this.applyRules();
155
+ },
156
+
157
+ // Adds a new rule row dynamically
158
+ addRuleRow: function (type, dataField, name) {
159
+ const self = this;
160
+ const $container = this.$contenedor.find("#container-rules-filters");
161
+ let id_select = 0;
162
+
163
+ // Generate unique ID for the new row based on the last element
164
+ const last_container = $contenedor.find("select[name=select-rule-option]").last();
165
+ if (last_container.length > 0) {
166
+ id_select = last_container.data("id") + 1;
167
+ }
168
+
169
+ // Define search input template (hidden for 'select' type)
170
+ const searchInput = `
171
+ <div class="col-7">
172
+ <label class="form-label font-bold"><span class="text-danger">*</span>${type}</label>
173
+ <input type="text" class="form-control" name="input-rule-search" id="input-rule-search-${id_select}">
174
+ </div>`;
175
+
176
+ const row = `
177
+ <div id="select-rule-container-${id_select}" class="d-flex justify-content-between mt-2 pt-2 mb-4 ${$contenedor.find("[name=select-rule-option]").length > 0 ? ' border-top my-3' : ""}">
178
+ <div class="row flex-grow-1">
179
+ <div class="col-4">
180
+ <label class="form-label font-bold">${name}</label>
181
+ <select class="form-select" ${type === 'select' ? 'multiple' : ''} data-type="${type}" data-name="${name}" data-field="${dataField}" id="select-rule-option-${id_select}" data-id="${id_select}" name="select-rule-option"></select>
182
+ </div>
183
+ ${type === 'select' ? '' : searchInput}
184
+ </div>
185
+ <div class="">
186
+ <a href="#" class="link-danger btn-remove-rule" data-target="select-rule-container-${id_select}">
187
+ <i class="fa-solid fa-trash-can"></i>
188
+ </a>
189
+ </div>
190
+ </div>`;
191
+
192
+ $container.append(row);
193
+
194
+ // Fetch custom render function from settings
195
+ const render = self.settings.filters.find((e) => e.data === dataField).render;
196
+ this.initSelect2(id_select, type, dataField, render);
197
+ },
198
+
199
+ // Initialize Select2 plugin on the newly created dropdown
200
+ initSelect2: function (id, type, dataField, render) {
201
+ const self = this;
202
+ const $select = this.$contenedor.find(`#select-rule-option-${id}`);
203
+
204
+ $select.select2({
205
+ width: '100%',
206
+ escapeMarkup: function (markup) { return markup; }, // Allow HTML rendering in badges
207
+ templateSelection: render,
208
+ templateResult: render,
209
+ // Load dataset based on type: unique values for 'select', operators for others
210
+ data: type == 'select' ? this.getSelectValues(dataField) : this.getRuleOptions(type)
211
+ });
212
+
213
+ // Remove rule event
214
+ this.$contenedor.find(`[data-target=select-rule-container-${id}]`).on("click", function (e) {
215
+ e.preventDefault();
216
+ $(`#select-rule-container-${id}`).remove();
217
+ });
218
+ },
219
+
220
+ // Helper to map operator objects from language settings
221
+ getRuleOptions: function (type) {
222
+ const lang = type === "number" ? this.settings.language.number : this.settings.language.string;
223
+ return $.map(lang, function (valor, clave) {
224
+ return { id: clave, text: valor };
225
+ });
226
+ },
227
+
228
+ // Extract unique values from data to populate 'select' type filters
229
+ getSelectValues: function (dataField) {
230
+ const getValueByPath = (obj, path) => {
231
+ return path.split('.').reduce((acc, part) => acc && acc[part], obj);
232
+ };
233
+
234
+ // Using Set + JSON stringify/parse to ensure unique objects in the dropdown
235
+ return [...new Set(this.data.map(function (item) {
236
+ const val = getValueByPath(item, dataField);
237
+ return JSON.stringify({ id: val, text: val });
238
+ }))].map((e) => JSON.parse(e));
239
+ },
240
+
241
+ // Core filtering engine
242
+ applyRules: function () {
243
+ const self = this;
244
+ const rules = this.getAplicatedRules();
245
+
246
+ // Utility to handle nested object properties (e.g., 'user.name')
247
+ const getValueByPath = (obj, path) => {
248
+ return path.split('.').reduce((acc, part) => acc && acc[part], obj);
249
+ };
250
+
251
+ const filteredResults = this.data.filter(item => {
252
+ // .every ensures all active rules must be met (AND logic)
253
+ return rules.every(regla => {
254
+ const valorOriginal = getValueByPath(item, regla.dataField);
255
+ const valorBusqueda = regla.searchValue;
256
+
257
+ // Handle empty/null values
258
+ if (valorOriginal === undefined || valorOriginal === null) return false;
259
+
260
+ // Special logic for Multiple Select filters
261
+ if(regla.typeFilter === 'select'){
262
+ const selectedValues = regla.optionFilter; // Array from Select2 multiple
263
+ return selectedValues.includes(valorOriginal.toString());
264
+ }
265
+
266
+ // Normalization for string and numeric comparisons
267
+ const v1 = valorOriginal.toString().toLowerCase();
268
+ const v2 = valorBusqueda.toString().toLowerCase();
269
+ const n1 = parseFloat(valorOriginal);
270
+ const n2 = parseFloat(valorBusqueda);
271
+
272
+ // Operator mapping
273
+ switch (regla.optionFilter) {
274
+ // String operators
275
+ case "sContain": return v1.includes(v2);
276
+ case "sNotContain": return !v1.includes(v2);
277
+ case "sEquals": return v1 === v2;
278
+ case "sDifference": return v1 !== v2;
279
+ case "sStartWith": return v1.startsWith(v2);
280
+ case "sEndWith": return v1.endsWith(v2);
281
+
282
+ // Numeric operators
283
+ case "nEquals": return n1 === n2;
284
+ case "nDifference": return n1 !== n2;
285
+ case "nGreatherThan": return n1 > n2;
286
+ case "nGreatherThanOrEquals": return n1 >= n2;
287
+ case "nLowerThan": return n1 < n2;
288
+ case "nLowerThanOrEquals": return n1 <= n2;
289
+
290
+ default: return true;
291
+ }
292
+ });
293
+ });
294
+
295
+ // EXECUTE onApply CALLBACK
296
+ if (typeof self.settings.onApply === 'function') {
297
+ // Pass rules summary and filtered array to the main program
298
+ self.settings.onApply.call(self.$contenedor, rules, filteredResults);
299
+ }
300
+ }
301
+ };
302
+
303
+ // Store instance in data attribute and initialize
304
+ $contenedor.data('rulesControl', control);
305
+ control.init();
306
+ });
307
+ };
308
+ }));