@yuneta/gobj-ui 7.23.48 → 7.23.50
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/README.md +45 -0
- package/dist/gobj-ui.cjs.js +188 -9
- package/dist/gobj-ui.es.js +188 -9
- package/package.json +1 -1
- package/src/c_yui_treedb_topic_with_form.css +14 -4
- package/src/c_yui_treedb_topic_with_form.js +67 -14
- package/src/yui_row_search.js +96 -0
- package/src/yui_row_search.test.js +62 -0
- package/src/yui_table_filter_clear.css +37 -0
- package/src/yui_table_filter_clear.js +151 -0
package/README.md
CHANGED
|
@@ -1780,6 +1780,51 @@ this facility: it is `formatter: "rowSelection"` with no `titleFormatter` and
|
|
|
1780
1780
|
`selectableRows: 1`, and the header checkbox, the count and the bar all mean
|
|
1781
1781
|
nothing there.
|
|
1782
1782
|
|
|
1783
|
+
### Clearing a header filter — `yui_table_filter_clear.js`
|
|
1784
|
+
|
|
1785
|
+
A column filter is set by typing and undone by deleting what you typed, letter
|
|
1786
|
+
by letter, and nothing in the header says it can be dropped at all. With
|
|
1787
|
+
several columns filtered, getting back to the whole table is an exercise in
|
|
1788
|
+
remembering which ones you touched. One call, on a table that is already built:
|
|
1789
|
+
|
|
1790
|
+
```js
|
|
1791
|
+
import {yui_table_filter_clear} from "@yuneta/gobj-ui/src/yui_table_filter_clear.js";
|
|
1792
|
+
|
|
1793
|
+
table.on("tableBuilt", () => {
|
|
1794
|
+
yui_table_filter_clear(table);
|
|
1795
|
+
});
|
|
1796
|
+
```
|
|
1797
|
+
|
|
1798
|
+
The app must define the i18n key **`"clear"`**. Three decisions are baked in:
|
|
1799
|
+
|
|
1800
|
+
- **The ✕ is visible whenever the filter has content, focus or not** — the
|
|
1801
|
+
opposite of the ✕ in a form (`yui_inputs.js`), which shows only on the field
|
|
1802
|
+
being edited so a form full of filled fields does not light up an ✕ on every
|
|
1803
|
+
one. Here the filter is already set, the focus is elsewhere, and the whole
|
|
1804
|
+
point is to see it and drop it; hidden behind focus it would take two clicks.
|
|
1805
|
+
- **It clears through `setHeaderFilterValue()`**, not by synthesising a
|
|
1806
|
+
keystroke on the input — what a header filter listens to depends on that
|
|
1807
|
+
column's filter editor.
|
|
1808
|
+
- **It decorates the header by walking it**, because a table's columns are
|
|
1809
|
+
often built from data (a treedb schema) and the header rebuilds itself when
|
|
1810
|
+
they change. The walk is idempotent and gated by a single `querySelector`, so
|
|
1811
|
+
repeating it on every render costs nothing.
|
|
1812
|
+
|
|
1813
|
+
### Searching a row that is not flat — `yui_row_search.js`
|
|
1814
|
+
|
|
1815
|
+
`row_matches(row, term)` is what the treedb topic table's search box uses, and
|
|
1816
|
+
it exists because **a treedb row is not flat**: with `list_dict` an fkey
|
|
1817
|
+
arrives as a list of objects — `[{id, topic_name, hook_name}]` — and
|
|
1818
|
+
`String(val)` of that is `"[object Object]"`. A search written the obvious way
|
|
1819
|
+
therefore never finds the place a node is linked to, while the cell plainly
|
|
1820
|
+
renders that id: what you see and what is searched are not the same thing.
|
|
1821
|
+
|
|
1822
|
+
It walks into lists and objects, and reads only the **`id`** of an fkey:
|
|
1823
|
+
`topic_name` and `hook_name` are the same two words on every row, so matching
|
|
1824
|
+
them turns any such term into a wildcard over the whole topic. Keys starting
|
|
1825
|
+
with `_` are never searched, at any level — scaffolding (`_operation`) and
|
|
1826
|
+
metadata (`__md_treedb__`).
|
|
1827
|
+
|
|
1783
1828
|
## Conventions
|
|
1784
1829
|
|
|
1785
1830
|
### i18n: a string must be able to CHANGE language, not just be translated once
|
package/dist/gobj-ui.cjs.js
CHANGED
|
@@ -22558,6 +22558,172 @@ function yui_selection_bar(t, opts) {
|
|
|
22558
22558
|
};
|
|
22559
22559
|
}
|
|
22560
22560
|
//#endregion
|
|
22561
|
+
//#region src/yui_table_filter_clear.js
|
|
22562
|
+
/***********************************************************************
|
|
22563
|
+
* yui_table_filter_clear.js
|
|
22564
|
+
*
|
|
22565
|
+
* LA ✕ DE LOS FILTROS DE CABECERA DE UNA TABULATOR.
|
|
22566
|
+
*
|
|
22567
|
+
* Un filtro de columna se pone escribiendo y se quita BORRANDO lo
|
|
22568
|
+
* escrito, letra a letra, y no hay nada en la cabecera que diga
|
|
22569
|
+
* que se puede quitar. Con varias columnas filtradas, volver a
|
|
22570
|
+
* ver la tabla entera es un ejercicio de memoria: cuáles toqué.
|
|
22571
|
+
* La ✕ lo dice y lo hace.
|
|
22572
|
+
*
|
|
22573
|
+
* TRES DECISIONES QUE NO SON OBVIAS:
|
|
22574
|
+
*
|
|
22575
|
+
* - **Se ve mientras el filtro tenga contenido, con foco o sin
|
|
22576
|
+
* él.** Es al revés que la ✕ de un formulario
|
|
22577
|
+
* (`yui_inputs.js`), que sólo aparece en el campo que se está
|
|
22578
|
+
* editando para no encender un aspa en cada campo relleno. Aquí
|
|
22579
|
+
* el caso de uso es justo el contrario: el filtro ya está puesto,
|
|
22580
|
+
* el foco está en otra parte, y lo que se quiere es verlo y
|
|
22581
|
+
* quitarlo. Escondida tras el foco harían falta dos clics.
|
|
22582
|
+
*
|
|
22583
|
+
* - **Se borra con la API, no simulando teclas.**
|
|
22584
|
+
* `setHeaderFilterValue(field, "")` es lo que Tabulator ofrece, y
|
|
22585
|
+
* deja el valor y el filtro consistentes. Sintetizar un `input`
|
|
22586
|
+
* sobre el `<input>` depende de a qué escuche el editor de ese
|
|
22587
|
+
* filtro, que cambia con el tipo de columna.
|
|
22588
|
+
*
|
|
22589
|
+
* - **Se recorre la cabecera, no se envuelve el editor.** Las
|
|
22590
|
+
* columnas se construyen del esquema del topic, así que no hay un
|
|
22591
|
+
* sitio único donde envolver el editor; y la cabecera se rehace
|
|
22592
|
+
* sola cuando cambian las columnas. El recorrido es idempotente
|
|
22593
|
+
* -- marca lo que ya tiene aspa -- y se dispara con un
|
|
22594
|
+
* `querySelector` de una sola pasada, así que repetirlo en cada
|
|
22595
|
+
* render no cuesta nada.
|
|
22596
|
+
*
|
|
22597
|
+
* Copyright (c) 2026, ArtGins.
|
|
22598
|
+
* All Rights Reserved.
|
|
22599
|
+
***********************************************************************/
|
|
22600
|
+
var MARK = "data-yui-filter-clear";
|
|
22601
|
+
/***************************************************************
|
|
22602
|
+
* Pon (o repón) la ✕ de cada filtro de cabecera de `table`.
|
|
22603
|
+
*
|
|
22604
|
+
* Idempotente: los que ya la tienen se saltan.
|
|
22605
|
+
***************************************************************/
|
|
22606
|
+
function decorate(table) {
|
|
22607
|
+
if (!table || !table.element) return;
|
|
22608
|
+
if (!table.element.querySelector(`.tabulator-header-filter:not([${MARK}])`)) return;
|
|
22609
|
+
let list = table.element.querySelectorAll(`.tabulator-header-filter:not([${MARK}])`);
|
|
22610
|
+
for (const $filter of list) {
|
|
22611
|
+
$filter.setAttribute(MARK, "1");
|
|
22612
|
+
let $input = $filter.querySelector("input");
|
|
22613
|
+
if (!$input) continue;
|
|
22614
|
+
let $col = $filter.closest(".tabulator-col");
|
|
22615
|
+
let field = $col ? $col.getAttribute("tabulator-field") : "";
|
|
22616
|
+
if (!field) continue;
|
|
22617
|
+
$filter.classList.add("yui-filter-has-clear");
|
|
22618
|
+
let $btn = (0, _yuneta_gobj_js.createElement2)(["button", {
|
|
22619
|
+
type: "button",
|
|
22620
|
+
class: "delete is-small yui-filter-clear",
|
|
22621
|
+
tabindex: "-1",
|
|
22622
|
+
title: i18next.default.t("clear"),
|
|
22623
|
+
"aria-label": i18next.default.t("clear"),
|
|
22624
|
+
"data-i18n-title": "clear",
|
|
22625
|
+
"data-i18n-aria-label": "clear"
|
|
22626
|
+
}]);
|
|
22627
|
+
const sync = () => {
|
|
22628
|
+
$btn.classList.toggle("is-visible", !!$input.value);
|
|
22629
|
+
};
|
|
22630
|
+
$input.addEventListener("input", sync);
|
|
22631
|
+
$input.addEventListener("change", sync);
|
|
22632
|
+
$btn.addEventListener("click", (event) => {
|
|
22633
|
+
event.stopPropagation();
|
|
22634
|
+
table.setHeaderFilterValue(field, "");
|
|
22635
|
+
sync();
|
|
22636
|
+
});
|
|
22637
|
+
$filter.appendChild($btn);
|
|
22638
|
+
sync();
|
|
22639
|
+
}
|
|
22640
|
+
}
|
|
22641
|
+
/***************************************************************
|
|
22642
|
+
* Engancha la ✕ a una tabla ya creada.
|
|
22643
|
+
*
|
|
22644
|
+
* Se llama una vez, con la tabla construida. Los enganches
|
|
22645
|
+
* cubren los dos momentos en que la cabecera aparece o se
|
|
22646
|
+
* rehace: cuando se construye y cuando cambian las columnas.
|
|
22647
|
+
***************************************************************/
|
|
22648
|
+
function yui_table_filter_clear(table) {
|
|
22649
|
+
if (!table) return;
|
|
22650
|
+
decorate(table);
|
|
22651
|
+
table.on("renderComplete", () => {
|
|
22652
|
+
decorate(table);
|
|
22653
|
+
});
|
|
22654
|
+
table.on("columnVisibilityChanged", () => {
|
|
22655
|
+
decorate(table);
|
|
22656
|
+
});
|
|
22657
|
+
}
|
|
22658
|
+
//#endregion
|
|
22659
|
+
//#region src/yui_row_search.js
|
|
22660
|
+
/***********************************************************************
|
|
22661
|
+
* yui_row_search.js
|
|
22662
|
+
*
|
|
22663
|
+
* ¿ESTÁ ESTE TÉRMINO EN ESTA FILA?
|
|
22664
|
+
*
|
|
22665
|
+
* La caja de búsqueda de una tabla de treedb miraba los valores de
|
|
22666
|
+
* la fila con `String(val)`, y una fila de treedb no es plana: un
|
|
22667
|
+
* fkey llega como una LISTA DE OBJETOS
|
|
22668
|
+
* `[{id, topic_name, hook_name}]`, y `String()` de eso es
|
|
22669
|
+
* `"[object Object]"`. O sea que buscar el taller de un equipo --
|
|
22670
|
+
* que es donde vive el dato que un operador tiene en la cabeza --
|
|
22671
|
+
* no encontraba nunca nada, y la caja no daba ninguna pista de que
|
|
22672
|
+
* había mirado en otro sitio.
|
|
22673
|
+
*
|
|
22674
|
+
* DOS REGLAS, Y LA SEGUNDA ES LA QUE HACE QUE SIRVA:
|
|
22675
|
+
*
|
|
22676
|
+
* - Se baja por listas y objetos hasta una profundidad corta: un
|
|
22677
|
+
* fkey está a dos niveles y nada de lo que se busca está más
|
|
22678
|
+
* hondo.
|
|
22679
|
+
* - **De un fkey se mira sólo el `id`.** `topic_name` y
|
|
22680
|
+
* `hook_name` son las MISMAS dos palabras en todas las filas, así
|
|
22681
|
+
* que mirarlas convierte el término en un comodín: buscar
|
|
22682
|
+
* "devices" traería el topic entero. El `id` es lo único que
|
|
22683
|
+
* nombra la cosa enlazada.
|
|
22684
|
+
*
|
|
22685
|
+
* Las claves que empiezan por `_` no se miran en ningún nivel: son
|
|
22686
|
+
* del armazón (`_check_box_state_`, `_operation`) o metadatos
|
|
22687
|
+
* (`__md_treedb__`), y nadie busca por ellas.
|
|
22688
|
+
*
|
|
22689
|
+
* Copyright (c) 2026, ArtGins.
|
|
22690
|
+
* All Rights Reserved.
|
|
22691
|
+
***********************************************************************/
|
|
22692
|
+
var MAX_DEPTH = 4;
|
|
22693
|
+
/***************************************************************
|
|
22694
|
+
* Un fkey, tal y como `nodes` lo contesta con `list_dict`.
|
|
22695
|
+
***************************************************************/
|
|
22696
|
+
function is_fkey_ref(value) {
|
|
22697
|
+
return !!value && typeof value === "object" && !Array.isArray(value) && "id" in value && "topic_name" in value && "hook_name" in value;
|
|
22698
|
+
}
|
|
22699
|
+
/***************************************************************
|
|
22700
|
+
* ¿Contiene `value` el término? El término viene YA en
|
|
22701
|
+
* minúsculas: quien busca lo baja una vez y no una por celda.
|
|
22702
|
+
***************************************************************/
|
|
22703
|
+
function value_matches(value, term, depth) {
|
|
22704
|
+
if (value === null || value === void 0) return false;
|
|
22705
|
+
if (depth > MAX_DEPTH) return false;
|
|
22706
|
+
if (Array.isArray(value)) return value.some((v) => value_matches(v, term, depth + 1));
|
|
22707
|
+
if (typeof value === "object") {
|
|
22708
|
+
if (is_fkey_ref(value)) return value_matches(value.id, term, depth + 1);
|
|
22709
|
+
return Object.entries(value).some(([key, v]) => {
|
|
22710
|
+
if (key.startsWith("_")) return false;
|
|
22711
|
+
return value_matches(v, term, depth + 1);
|
|
22712
|
+
});
|
|
22713
|
+
}
|
|
22714
|
+
return String(value).toLowerCase().includes(term);
|
|
22715
|
+
}
|
|
22716
|
+
/***************************************************************
|
|
22717
|
+
* ¿Está el término en alguna parte de esta fila?
|
|
22718
|
+
***************************************************************/
|
|
22719
|
+
function row_matches(row, term) {
|
|
22720
|
+
if (!row || typeof row !== "object" || !term) return false;
|
|
22721
|
+
return Object.entries(row).some(([key, value]) => {
|
|
22722
|
+
if (key.startsWith("_")) return false;
|
|
22723
|
+
return value_matches(value, term, 0);
|
|
22724
|
+
});
|
|
22725
|
+
}
|
|
22726
|
+
//#endregion
|
|
22561
22727
|
//#region src/treedb_write_plan.js
|
|
22562
22728
|
/***********************************************************************
|
|
22563
22729
|
* treedb_write_plan.js
|
|
@@ -23631,7 +23797,8 @@ function create_tabulator(gobj) {
|
|
|
23631
23797
|
50,
|
|
23632
23798
|
100,
|
|
23633
23799
|
200,
|
|
23634
|
-
500
|
|
23800
|
+
500,
|
|
23801
|
+
true
|
|
23635
23802
|
],
|
|
23636
23803
|
ajaxURL: "nodes",
|
|
23637
23804
|
ajaxRequestFunc: function(url, config, params) {
|
|
@@ -23674,17 +23841,32 @@ function create_tabulator(gobj) {
|
|
|
23674
23841
|
single_page = max === false || max <= 1;
|
|
23675
23842
|
} else {
|
|
23676
23843
|
let size = tabulator.getPageSize();
|
|
23677
|
-
single_page = !size || n <= size;
|
|
23844
|
+
single_page = size === true || !size || n <= size;
|
|
23678
23845
|
}
|
|
23679
23846
|
} catch (e) {
|
|
23680
23847
|
single_page = true;
|
|
23681
23848
|
}
|
|
23849
|
+
let total_rows;
|
|
23850
|
+
if (paged) total_rows = total;
|
|
23851
|
+
else try {
|
|
23852
|
+
total_rows = tabulator.getDataCount();
|
|
23853
|
+
} catch (e) {
|
|
23854
|
+
total_rows = n;
|
|
23855
|
+
}
|
|
23856
|
+
let min_size = 0;
|
|
23857
|
+
let sizes = tabulator.options ? tabulator.options.paginationSizeSelector : null;
|
|
23858
|
+
if (Array.isArray(sizes)) sizes.forEach(function(size) {
|
|
23859
|
+
if (typeof size === "number" && (!min_size || size < min_size)) min_size = size;
|
|
23860
|
+
});
|
|
23861
|
+
let no_paginator = single_page && (!min_size || typeof total_rows !== "number" || total_rows <= min_size);
|
|
23682
23862
|
tabulator.element.classList.toggle("yui-single-page", single_page);
|
|
23863
|
+
tabulator.element.classList.toggle("yui-no-paginator", no_paginator);
|
|
23683
23864
|
}
|
|
23684
23865
|
tabulator._ready = false;
|
|
23685
23866
|
tabulator.on("tableBuilt", function() {
|
|
23686
23867
|
tabulator._ready = true;
|
|
23687
23868
|
update_rowcount();
|
|
23869
|
+
yui_table_filter_clear(tabulator);
|
|
23688
23870
|
if (tabulator._pendingData !== void 0) {
|
|
23689
23871
|
tabulator.setData(tabulator._pendingData);
|
|
23690
23872
|
delete tabulator._pendingData;
|
|
@@ -24999,11 +25181,12 @@ function request_page(gobj, page, size) {
|
|
|
24999
25181
|
reject,
|
|
25000
25182
|
timer
|
|
25001
25183
|
};
|
|
25184
|
+
const all = size === true;
|
|
25002
25185
|
(0, _yuneta_gobj_js.gobj_publish_event)(gobj, "EV_REQUEST_PAGE", {
|
|
25003
25186
|
topic_name: (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "topic_name"),
|
|
25004
25187
|
req_id,
|
|
25005
|
-
from: (page - 1) * size + 1,
|
|
25006
|
-
limit: size
|
|
25188
|
+
from: all ? 1 : (page - 1) * size + 1,
|
|
25189
|
+
limit: all ? 0 : size
|
|
25007
25190
|
});
|
|
25008
25191
|
});
|
|
25009
25192
|
}
|
|
@@ -25085,11 +25268,7 @@ function ac_search(gobj, event, kw, src) {
|
|
|
25085
25268
|
return 0;
|
|
25086
25269
|
}
|
|
25087
25270
|
tabulator.setFilter(function(data) {
|
|
25088
|
-
return
|
|
25089
|
-
if (key.startsWith("_")) return false;
|
|
25090
|
-
if (val === null || val === void 0) return false;
|
|
25091
|
-
return String(val).toLowerCase().includes(term);
|
|
25092
|
-
});
|
|
25271
|
+
return row_matches(data, term);
|
|
25093
25272
|
});
|
|
25094
25273
|
return 0;
|
|
25095
25274
|
}
|
package/dist/gobj-ui.es.js
CHANGED
|
@@ -22553,6 +22553,172 @@ function yui_selection_bar(t, opts) {
|
|
|
22553
22553
|
};
|
|
22554
22554
|
}
|
|
22555
22555
|
//#endregion
|
|
22556
|
+
//#region src/yui_table_filter_clear.js
|
|
22557
|
+
/***********************************************************************
|
|
22558
|
+
* yui_table_filter_clear.js
|
|
22559
|
+
*
|
|
22560
|
+
* LA ✕ DE LOS FILTROS DE CABECERA DE UNA TABULATOR.
|
|
22561
|
+
*
|
|
22562
|
+
* Un filtro de columna se pone escribiendo y se quita BORRANDO lo
|
|
22563
|
+
* escrito, letra a letra, y no hay nada en la cabecera que diga
|
|
22564
|
+
* que se puede quitar. Con varias columnas filtradas, volver a
|
|
22565
|
+
* ver la tabla entera es un ejercicio de memoria: cuáles toqué.
|
|
22566
|
+
* La ✕ lo dice y lo hace.
|
|
22567
|
+
*
|
|
22568
|
+
* TRES DECISIONES QUE NO SON OBVIAS:
|
|
22569
|
+
*
|
|
22570
|
+
* - **Se ve mientras el filtro tenga contenido, con foco o sin
|
|
22571
|
+
* él.** Es al revés que la ✕ de un formulario
|
|
22572
|
+
* (`yui_inputs.js`), que sólo aparece en el campo que se está
|
|
22573
|
+
* editando para no encender un aspa en cada campo relleno. Aquí
|
|
22574
|
+
* el caso de uso es justo el contrario: el filtro ya está puesto,
|
|
22575
|
+
* el foco está en otra parte, y lo que se quiere es verlo y
|
|
22576
|
+
* quitarlo. Escondida tras el foco harían falta dos clics.
|
|
22577
|
+
*
|
|
22578
|
+
* - **Se borra con la API, no simulando teclas.**
|
|
22579
|
+
* `setHeaderFilterValue(field, "")` es lo que Tabulator ofrece, y
|
|
22580
|
+
* deja el valor y el filtro consistentes. Sintetizar un `input`
|
|
22581
|
+
* sobre el `<input>` depende de a qué escuche el editor de ese
|
|
22582
|
+
* filtro, que cambia con el tipo de columna.
|
|
22583
|
+
*
|
|
22584
|
+
* - **Se recorre la cabecera, no se envuelve el editor.** Las
|
|
22585
|
+
* columnas se construyen del esquema del topic, así que no hay un
|
|
22586
|
+
* sitio único donde envolver el editor; y la cabecera se rehace
|
|
22587
|
+
* sola cuando cambian las columnas. El recorrido es idempotente
|
|
22588
|
+
* -- marca lo que ya tiene aspa -- y se dispara con un
|
|
22589
|
+
* `querySelector` de una sola pasada, así que repetirlo en cada
|
|
22590
|
+
* render no cuesta nada.
|
|
22591
|
+
*
|
|
22592
|
+
* Copyright (c) 2026, ArtGins.
|
|
22593
|
+
* All Rights Reserved.
|
|
22594
|
+
***********************************************************************/
|
|
22595
|
+
var MARK = "data-yui-filter-clear";
|
|
22596
|
+
/***************************************************************
|
|
22597
|
+
* Pon (o repón) la ✕ de cada filtro de cabecera de `table`.
|
|
22598
|
+
*
|
|
22599
|
+
* Idempotente: los que ya la tienen se saltan.
|
|
22600
|
+
***************************************************************/
|
|
22601
|
+
function decorate(table) {
|
|
22602
|
+
if (!table || !table.element) return;
|
|
22603
|
+
if (!table.element.querySelector(`.tabulator-header-filter:not([${MARK}])`)) return;
|
|
22604
|
+
let list = table.element.querySelectorAll(`.tabulator-header-filter:not([${MARK}])`);
|
|
22605
|
+
for (const $filter of list) {
|
|
22606
|
+
$filter.setAttribute(MARK, "1");
|
|
22607
|
+
let $input = $filter.querySelector("input");
|
|
22608
|
+
if (!$input) continue;
|
|
22609
|
+
let $col = $filter.closest(".tabulator-col");
|
|
22610
|
+
let field = $col ? $col.getAttribute("tabulator-field") : "";
|
|
22611
|
+
if (!field) continue;
|
|
22612
|
+
$filter.classList.add("yui-filter-has-clear");
|
|
22613
|
+
let $btn = createElement2(["button", {
|
|
22614
|
+
type: "button",
|
|
22615
|
+
class: "delete is-small yui-filter-clear",
|
|
22616
|
+
tabindex: "-1",
|
|
22617
|
+
title: i18next.t("clear"),
|
|
22618
|
+
"aria-label": i18next.t("clear"),
|
|
22619
|
+
"data-i18n-title": "clear",
|
|
22620
|
+
"data-i18n-aria-label": "clear"
|
|
22621
|
+
}]);
|
|
22622
|
+
const sync = () => {
|
|
22623
|
+
$btn.classList.toggle("is-visible", !!$input.value);
|
|
22624
|
+
};
|
|
22625
|
+
$input.addEventListener("input", sync);
|
|
22626
|
+
$input.addEventListener("change", sync);
|
|
22627
|
+
$btn.addEventListener("click", (event) => {
|
|
22628
|
+
event.stopPropagation();
|
|
22629
|
+
table.setHeaderFilterValue(field, "");
|
|
22630
|
+
sync();
|
|
22631
|
+
});
|
|
22632
|
+
$filter.appendChild($btn);
|
|
22633
|
+
sync();
|
|
22634
|
+
}
|
|
22635
|
+
}
|
|
22636
|
+
/***************************************************************
|
|
22637
|
+
* Engancha la ✕ a una tabla ya creada.
|
|
22638
|
+
*
|
|
22639
|
+
* Se llama una vez, con la tabla construida. Los enganches
|
|
22640
|
+
* cubren los dos momentos en que la cabecera aparece o se
|
|
22641
|
+
* rehace: cuando se construye y cuando cambian las columnas.
|
|
22642
|
+
***************************************************************/
|
|
22643
|
+
function yui_table_filter_clear(table) {
|
|
22644
|
+
if (!table) return;
|
|
22645
|
+
decorate(table);
|
|
22646
|
+
table.on("renderComplete", () => {
|
|
22647
|
+
decorate(table);
|
|
22648
|
+
});
|
|
22649
|
+
table.on("columnVisibilityChanged", () => {
|
|
22650
|
+
decorate(table);
|
|
22651
|
+
});
|
|
22652
|
+
}
|
|
22653
|
+
//#endregion
|
|
22654
|
+
//#region src/yui_row_search.js
|
|
22655
|
+
/***********************************************************************
|
|
22656
|
+
* yui_row_search.js
|
|
22657
|
+
*
|
|
22658
|
+
* ¿ESTÁ ESTE TÉRMINO EN ESTA FILA?
|
|
22659
|
+
*
|
|
22660
|
+
* La caja de búsqueda de una tabla de treedb miraba los valores de
|
|
22661
|
+
* la fila con `String(val)`, y una fila de treedb no es plana: un
|
|
22662
|
+
* fkey llega como una LISTA DE OBJETOS
|
|
22663
|
+
* `[{id, topic_name, hook_name}]`, y `String()` de eso es
|
|
22664
|
+
* `"[object Object]"`. O sea que buscar el taller de un equipo --
|
|
22665
|
+
* que es donde vive el dato que un operador tiene en la cabeza --
|
|
22666
|
+
* no encontraba nunca nada, y la caja no daba ninguna pista de que
|
|
22667
|
+
* había mirado en otro sitio.
|
|
22668
|
+
*
|
|
22669
|
+
* DOS REGLAS, Y LA SEGUNDA ES LA QUE HACE QUE SIRVA:
|
|
22670
|
+
*
|
|
22671
|
+
* - Se baja por listas y objetos hasta una profundidad corta: un
|
|
22672
|
+
* fkey está a dos niveles y nada de lo que se busca está más
|
|
22673
|
+
* hondo.
|
|
22674
|
+
* - **De un fkey se mira sólo el `id`.** `topic_name` y
|
|
22675
|
+
* `hook_name` son las MISMAS dos palabras en todas las filas, así
|
|
22676
|
+
* que mirarlas convierte el término en un comodín: buscar
|
|
22677
|
+
* "devices" traería el topic entero. El `id` es lo único que
|
|
22678
|
+
* nombra la cosa enlazada.
|
|
22679
|
+
*
|
|
22680
|
+
* Las claves que empiezan por `_` no se miran en ningún nivel: son
|
|
22681
|
+
* del armazón (`_check_box_state_`, `_operation`) o metadatos
|
|
22682
|
+
* (`__md_treedb__`), y nadie busca por ellas.
|
|
22683
|
+
*
|
|
22684
|
+
* Copyright (c) 2026, ArtGins.
|
|
22685
|
+
* All Rights Reserved.
|
|
22686
|
+
***********************************************************************/
|
|
22687
|
+
var MAX_DEPTH = 4;
|
|
22688
|
+
/***************************************************************
|
|
22689
|
+
* Un fkey, tal y como `nodes` lo contesta con `list_dict`.
|
|
22690
|
+
***************************************************************/
|
|
22691
|
+
function is_fkey_ref(value) {
|
|
22692
|
+
return !!value && typeof value === "object" && !Array.isArray(value) && "id" in value && "topic_name" in value && "hook_name" in value;
|
|
22693
|
+
}
|
|
22694
|
+
/***************************************************************
|
|
22695
|
+
* ¿Contiene `value` el término? El término viene YA en
|
|
22696
|
+
* minúsculas: quien busca lo baja una vez y no una por celda.
|
|
22697
|
+
***************************************************************/
|
|
22698
|
+
function value_matches(value, term, depth) {
|
|
22699
|
+
if (value === null || value === void 0) return false;
|
|
22700
|
+
if (depth > MAX_DEPTH) return false;
|
|
22701
|
+
if (Array.isArray(value)) return value.some((v) => value_matches(v, term, depth + 1));
|
|
22702
|
+
if (typeof value === "object") {
|
|
22703
|
+
if (is_fkey_ref(value)) return value_matches(value.id, term, depth + 1);
|
|
22704
|
+
return Object.entries(value).some(([key, v]) => {
|
|
22705
|
+
if (key.startsWith("_")) return false;
|
|
22706
|
+
return value_matches(v, term, depth + 1);
|
|
22707
|
+
});
|
|
22708
|
+
}
|
|
22709
|
+
return String(value).toLowerCase().includes(term);
|
|
22710
|
+
}
|
|
22711
|
+
/***************************************************************
|
|
22712
|
+
* ¿Está el término en alguna parte de esta fila?
|
|
22713
|
+
***************************************************************/
|
|
22714
|
+
function row_matches(row, term) {
|
|
22715
|
+
if (!row || typeof row !== "object" || !term) return false;
|
|
22716
|
+
return Object.entries(row).some(([key, value]) => {
|
|
22717
|
+
if (key.startsWith("_")) return false;
|
|
22718
|
+
return value_matches(value, term, 0);
|
|
22719
|
+
});
|
|
22720
|
+
}
|
|
22721
|
+
//#endregion
|
|
22556
22722
|
//#region src/treedb_write_plan.js
|
|
22557
22723
|
/***********************************************************************
|
|
22558
22724
|
* treedb_write_plan.js
|
|
@@ -23626,7 +23792,8 @@ function create_tabulator(gobj) {
|
|
|
23626
23792
|
50,
|
|
23627
23793
|
100,
|
|
23628
23794
|
200,
|
|
23629
|
-
500
|
|
23795
|
+
500,
|
|
23796
|
+
true
|
|
23630
23797
|
],
|
|
23631
23798
|
ajaxURL: "nodes",
|
|
23632
23799
|
ajaxRequestFunc: function(url, config, params) {
|
|
@@ -23669,17 +23836,32 @@ function create_tabulator(gobj) {
|
|
|
23669
23836
|
single_page = max === false || max <= 1;
|
|
23670
23837
|
} else {
|
|
23671
23838
|
let size = tabulator.getPageSize();
|
|
23672
|
-
single_page = !size || n <= size;
|
|
23839
|
+
single_page = size === true || !size || n <= size;
|
|
23673
23840
|
}
|
|
23674
23841
|
} catch (e) {
|
|
23675
23842
|
single_page = true;
|
|
23676
23843
|
}
|
|
23844
|
+
let total_rows;
|
|
23845
|
+
if (paged) total_rows = total;
|
|
23846
|
+
else try {
|
|
23847
|
+
total_rows = tabulator.getDataCount();
|
|
23848
|
+
} catch (e) {
|
|
23849
|
+
total_rows = n;
|
|
23850
|
+
}
|
|
23851
|
+
let min_size = 0;
|
|
23852
|
+
let sizes = tabulator.options ? tabulator.options.paginationSizeSelector : null;
|
|
23853
|
+
if (Array.isArray(sizes)) sizes.forEach(function(size) {
|
|
23854
|
+
if (typeof size === "number" && (!min_size || size < min_size)) min_size = size;
|
|
23855
|
+
});
|
|
23856
|
+
let no_paginator = single_page && (!min_size || typeof total_rows !== "number" || total_rows <= min_size);
|
|
23677
23857
|
tabulator.element.classList.toggle("yui-single-page", single_page);
|
|
23858
|
+
tabulator.element.classList.toggle("yui-no-paginator", no_paginator);
|
|
23678
23859
|
}
|
|
23679
23860
|
tabulator._ready = false;
|
|
23680
23861
|
tabulator.on("tableBuilt", function() {
|
|
23681
23862
|
tabulator._ready = true;
|
|
23682
23863
|
update_rowcount();
|
|
23864
|
+
yui_table_filter_clear(tabulator);
|
|
23683
23865
|
if (tabulator._pendingData !== void 0) {
|
|
23684
23866
|
tabulator.setData(tabulator._pendingData);
|
|
23685
23867
|
delete tabulator._pendingData;
|
|
@@ -24994,11 +25176,12 @@ function request_page(gobj, page, size) {
|
|
|
24994
25176
|
reject,
|
|
24995
25177
|
timer
|
|
24996
25178
|
};
|
|
25179
|
+
const all = size === true;
|
|
24997
25180
|
gobj_publish_event(gobj, "EV_REQUEST_PAGE", {
|
|
24998
25181
|
topic_name: gobj_read_str_attr(gobj, "topic_name"),
|
|
24999
25182
|
req_id,
|
|
25000
|
-
from: (page - 1) * size + 1,
|
|
25001
|
-
limit: size
|
|
25183
|
+
from: all ? 1 : (page - 1) * size + 1,
|
|
25184
|
+
limit: all ? 0 : size
|
|
25002
25185
|
});
|
|
25003
25186
|
});
|
|
25004
25187
|
}
|
|
@@ -25080,11 +25263,7 @@ function ac_search(gobj, event, kw, src) {
|
|
|
25080
25263
|
return 0;
|
|
25081
25264
|
}
|
|
25082
25265
|
tabulator.setFilter(function(data) {
|
|
25083
|
-
return
|
|
25084
|
-
if (key.startsWith("_")) return false;
|
|
25085
|
-
if (val === null || val === void 0) return false;
|
|
25086
|
-
return String(val).toLowerCase().includes(term);
|
|
25087
|
-
});
|
|
25266
|
+
return row_matches(data, term);
|
|
25088
25267
|
});
|
|
25089
25268
|
return 0;
|
|
25090
25269
|
}
|
package/package.json
CHANGED
|
@@ -23,10 +23,20 @@
|
|
|
23
23
|
line-height: 1.5;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
/* All rows fit in one page: the
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
|
|
26
|
+
/* All rows fit in one page: the First/Prev/1/Next/Last strip is pure
|
|
27
|
+
* noise (worst on mobile) — update_rowcount() toggles the class on every
|
|
28
|
+
* data change. The page-size SELECTOR stays: it is what made the table
|
|
29
|
+
* one page in the first place ("All" on a topic of 300 rows), so hiding
|
|
30
|
+
* it together with the buttons left the reader no way back to 50. */
|
|
31
|
+
.tabulator.yui-single-page .tabulator-footer .tabulator-paginator .tabulator-page,
|
|
32
|
+
.tabulator.yui-single-page .tabulator-footer .tabulator-paginator .tabulator-pages {
|
|
33
|
+
display: none;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* No choice in the selector would change anything either — the topic is
|
|
37
|
+
* smaller than the smallest page size it offers — so the whole strip,
|
|
38
|
+
* label and select included, is noise. */
|
|
39
|
+
.tabulator.yui-no-paginator .tabulator-footer .tabulator-paginator {
|
|
30
40
|
display: none;
|
|
31
41
|
}
|
|
32
42
|
|
|
@@ -85,6 +85,9 @@ import {
|
|
|
85
85
|
yui_clear_selection,
|
|
86
86
|
} from "./yui_table_select.js";
|
|
87
87
|
|
|
88
|
+
import {yui_table_filter_clear} from "./yui_table_filter_clear.js";
|
|
89
|
+
import {row_matches} from "./yui_row_search.js";
|
|
90
|
+
|
|
88
91
|
import {t} from "i18next";
|
|
89
92
|
|
|
90
93
|
import {plan_treedb_writes} from "./treedb_write_plan.js";
|
|
@@ -1346,7 +1349,13 @@ function create_tabulator(gobj)
|
|
|
1346
1349
|
paginationMode: "remote",
|
|
1347
1350
|
filterMode: "local",
|
|
1348
1351
|
paginationSize: page_size,
|
|
1349
|
-
|
|
1352
|
+
/* `true` is Tabulator's "All", and here it is not a courtesy:
|
|
1353
|
+
* `filterMode: "local"` means the header filters and the search
|
|
1354
|
+
* box only see the page that is loaded, so on a topic that does
|
|
1355
|
+
* not fit, searching for something on page 17 finds nothing.
|
|
1356
|
+
* "All" is the way out, and the backend has it -- `nodes` takes
|
|
1357
|
+
* `limit: 0` for "every one", answering the plain list. */
|
|
1358
|
+
paginationSizeSelector: [50, 100, 200, 500, true],
|
|
1350
1359
|
ajaxURL: "nodes", /* dummy: only fires the func */
|
|
1351
1360
|
ajaxRequestFunc: function(url, config, params) {
|
|
1352
1361
|
/* Widget plumbing, not an action: Tabulator wants a PROMISE
|
|
@@ -1453,20 +1462,60 @@ function create_tabulator(gobj)
|
|
|
1453
1462
|
single_page = (max === false) || max <= 1;
|
|
1454
1463
|
} else {
|
|
1455
1464
|
/* Derived from the count and the page size rather than read
|
|
1456
|
-
* from getPageMax(), which is stale in the filter path.
|
|
1465
|
+
* from getPageMax(), which is stale in the filter path.
|
|
1466
|
+
* `true` is Tabulator's "All" and it is the SIZE itself, so
|
|
1467
|
+
* it has to be read as a word: compared as a number it is 1,
|
|
1468
|
+
* and a table showing every one of its 300 rows in a single
|
|
1469
|
+
* page answered that it needed 300 pages. */
|
|
1457
1470
|
let size = tabulator.getPageSize();
|
|
1458
|
-
single_page = !size || n <= size;
|
|
1471
|
+
single_page = (size === true) || !size || n <= size;
|
|
1459
1472
|
}
|
|
1460
1473
|
} catch(e) {
|
|
1461
1474
|
single_page = true;
|
|
1462
1475
|
}
|
|
1476
|
+
/* The page-size selector lives INSIDE the paginator, so hiding the
|
|
1477
|
+
* whole strip when everything fits took away the very control that
|
|
1478
|
+
* got the reader there: pick "All" on a topic of 300 rows and the
|
|
1479
|
+
* table becomes one page, the paginator goes, and 50 is
|
|
1480
|
+
* unreachable. The selector is noise only when no choice in it
|
|
1481
|
+
* would change anything -- when the topic is smaller than the
|
|
1482
|
+
* smallest size it offers. Measure that on the WHOLE topic, never
|
|
1483
|
+
* on the filtered count, or typing a filter that leaves 3 rows
|
|
1484
|
+
* takes the selector away too. */
|
|
1485
|
+
let total_rows;
|
|
1486
|
+
if(paged) {
|
|
1487
|
+
total_rows = total;
|
|
1488
|
+
} else {
|
|
1489
|
+
try {
|
|
1490
|
+
total_rows = tabulator.getDataCount();
|
|
1491
|
+
} catch(e) {
|
|
1492
|
+
total_rows = n;
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
let min_size = 0;
|
|
1496
|
+
let sizes = tabulator.options? tabulator.options.paginationSizeSelector : null;
|
|
1497
|
+
if(Array.isArray(sizes)) {
|
|
1498
|
+
sizes.forEach(function(size) {
|
|
1499
|
+
if(typeof size === "number" && (!min_size || size < min_size)) {
|
|
1500
|
+
min_size = size;
|
|
1501
|
+
}
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1504
|
+
let no_paginator = single_page &&
|
|
1505
|
+
(!min_size || typeof total_rows !== "number" || total_rows <= min_size);
|
|
1506
|
+
|
|
1463
1507
|
tabulator.element.classList.toggle("yui-single-page", single_page);
|
|
1508
|
+
tabulator.element.classList.toggle("yui-no-paginator", no_paginator);
|
|
1464
1509
|
}
|
|
1465
1510
|
|
|
1466
1511
|
tabulator._ready = false;
|
|
1467
1512
|
tabulator.on("tableBuilt", function() {
|
|
1468
1513
|
tabulator._ready = true;
|
|
1469
1514
|
update_rowcount();
|
|
1515
|
+
/* La ✕ de cada filtro de cabecera. Un filtro se quita borrando lo
|
|
1516
|
+
* escrito, y con varias columnas filtradas volver a la tabla entera
|
|
1517
|
+
* era acordarse de cuáles se tocaron. */
|
|
1518
|
+
yui_table_filter_clear(tabulator);
|
|
1470
1519
|
if(tabulator._pendingData !== undefined) {
|
|
1471
1520
|
tabulator.setData(tabulator._pendingData);
|
|
1472
1521
|
delete tabulator._pendingData;
|
|
@@ -3370,14 +3419,21 @@ function request_page(gobj, page, size)
|
|
|
3370
3419
|
|
|
3371
3420
|
priv._pending_pages[req_id] = {resolve: resolve, reject: reject, timer: timer};
|
|
3372
3421
|
|
|
3422
|
+
/* "All" (Tabulator sends the size as `true`): `nodes` says every
|
|
3423
|
+
* one with `limit: 0`, and then it answers the plain list -- which
|
|
3424
|
+
* nodes_answer() reads as a single page, so the paginator hides and
|
|
3425
|
+
* every filter sees every row. Arithmetic on `true` would send
|
|
3426
|
+
* `limit: true` to a backend that wants an integer. */
|
|
3427
|
+
const all = (size === true);
|
|
3428
|
+
|
|
3373
3429
|
gobj_publish_event(
|
|
3374
3430
|
gobj,
|
|
3375
3431
|
"EV_REQUEST_PAGE",
|
|
3376
3432
|
{
|
|
3377
3433
|
topic_name: gobj_read_str_attr(gobj, "topic_name"),
|
|
3378
3434
|
req_id: req_id,
|
|
3379
|
-
from: (page - 1) * size + 1,
|
|
3380
|
-
limit: size
|
|
3435
|
+
from: all? 1: (page - 1) * size + 1, /* `nodes` counts from 1 */
|
|
3436
|
+
limit: all? 0: size
|
|
3381
3437
|
}
|
|
3382
3438
|
);
|
|
3383
3439
|
});
|
|
@@ -3482,16 +3538,13 @@ function ac_search(gobj, event, kw, src)
|
|
|
3482
3538
|
return 0;
|
|
3483
3539
|
}
|
|
3484
3540
|
|
|
3541
|
+
/* A treedb row is not flat: an fkey arrives as a list of objects, and
|
|
3542
|
+
* `String()` of that is "[object Object]" -- so searching for the
|
|
3543
|
+
* workshop of a meter, which is where the value a person has in mind
|
|
3544
|
+
* lives, never found anything. `row_matches()` walks into them, and
|
|
3545
|
+
* reads only the `id` of an fkey. See yui_row_search.js. */
|
|
3485
3546
|
tabulator.setFilter(function(data) {
|
|
3486
|
-
return
|
|
3487
|
-
if(key.startsWith('_')) {
|
|
3488
|
-
return false;
|
|
3489
|
-
}
|
|
3490
|
-
if(val === null || val === undefined) {
|
|
3491
|
-
return false;
|
|
3492
|
-
}
|
|
3493
|
-
return String(val).toLowerCase().includes(term);
|
|
3494
|
-
});
|
|
3547
|
+
return row_matches(data, term);
|
|
3495
3548
|
});
|
|
3496
3549
|
|
|
3497
3550
|
return 0;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* yui_row_search.js
|
|
3
|
+
*
|
|
4
|
+
* ¿ESTÁ ESTE TÉRMINO EN ESTA FILA?
|
|
5
|
+
*
|
|
6
|
+
* La caja de búsqueda de una tabla de treedb miraba los valores de
|
|
7
|
+
* la fila con `String(val)`, y una fila de treedb no es plana: un
|
|
8
|
+
* fkey llega como una LISTA DE OBJETOS
|
|
9
|
+
* `[{id, topic_name, hook_name}]`, y `String()` de eso es
|
|
10
|
+
* `"[object Object]"`. O sea que buscar el taller de un equipo --
|
|
11
|
+
* que es donde vive el dato que un operador tiene en la cabeza --
|
|
12
|
+
* no encontraba nunca nada, y la caja no daba ninguna pista de que
|
|
13
|
+
* había mirado en otro sitio.
|
|
14
|
+
*
|
|
15
|
+
* DOS REGLAS, Y LA SEGUNDA ES LA QUE HACE QUE SIRVA:
|
|
16
|
+
*
|
|
17
|
+
* - Se baja por listas y objetos hasta una profundidad corta: un
|
|
18
|
+
* fkey está a dos niveles y nada de lo que se busca está más
|
|
19
|
+
* hondo.
|
|
20
|
+
* - **De un fkey se mira sólo el `id`.** `topic_name` y
|
|
21
|
+
* `hook_name` son las MISMAS dos palabras en todas las filas, así
|
|
22
|
+
* que mirarlas convierte el término en un comodín: buscar
|
|
23
|
+
* "devices" traería el topic entero. El `id` es lo único que
|
|
24
|
+
* nombra la cosa enlazada.
|
|
25
|
+
*
|
|
26
|
+
* Las claves que empiezan por `_` no se miran en ningún nivel: son
|
|
27
|
+
* del armazón (`_check_box_state_`, `_operation`) o metadatos
|
|
28
|
+
* (`__md_treedb__`), y nadie busca por ellas.
|
|
29
|
+
*
|
|
30
|
+
* Copyright (c) 2026, ArtGins.
|
|
31
|
+
* All Rights Reserved.
|
|
32
|
+
***********************************************************************/
|
|
33
|
+
|
|
34
|
+
const MAX_DEPTH = 4;
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
/***************************************************************
|
|
38
|
+
* Un fkey, tal y como `nodes` lo contesta con `list_dict`.
|
|
39
|
+
***************************************************************/
|
|
40
|
+
function is_fkey_ref(value)
|
|
41
|
+
{
|
|
42
|
+
return !!value
|
|
43
|
+
&& typeof value === "object"
|
|
44
|
+
&& !Array.isArray(value)
|
|
45
|
+
&& ("id" in value)
|
|
46
|
+
&& ("topic_name" in value)
|
|
47
|
+
&& ("hook_name" in value);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/***************************************************************
|
|
51
|
+
* ¿Contiene `value` el término? El término viene YA en
|
|
52
|
+
* minúsculas: quien busca lo baja una vez y no una por celda.
|
|
53
|
+
***************************************************************/
|
|
54
|
+
function value_matches(value, term, depth)
|
|
55
|
+
{
|
|
56
|
+
if(value === null || value === undefined) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
if(depth > MAX_DEPTH) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
if(Array.isArray(value)) {
|
|
63
|
+
return value.some((v) => value_matches(v, term, depth + 1));
|
|
64
|
+
}
|
|
65
|
+
if(typeof value === "object") {
|
|
66
|
+
if(is_fkey_ref(value)) {
|
|
67
|
+
return value_matches(value.id, term, depth + 1);
|
|
68
|
+
}
|
|
69
|
+
return Object.entries(value).some(([key, v]) => {
|
|
70
|
+
if(key.startsWith("_")) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
return value_matches(v, term, depth + 1);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return String(value).toLowerCase().includes(term);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/***************************************************************
|
|
80
|
+
* ¿Está el término en alguna parte de esta fila?
|
|
81
|
+
***************************************************************/
|
|
82
|
+
function row_matches(row, term)
|
|
83
|
+
{
|
|
84
|
+
if(!row || typeof row !== "object" || !term) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
return Object.entries(row).some(([key, value]) => {
|
|
88
|
+
if(key.startsWith("_")) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
return value_matches(value, term, 0);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
export {row_matches, value_matches, is_fkey_ref};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {describe, it, expect} from "vitest";
|
|
2
|
+
|
|
3
|
+
import {row_matches, is_fkey_ref} from "./yui_row_search.js";
|
|
4
|
+
|
|
5
|
+
describe("row_matches", () => {
|
|
6
|
+
const row = {
|
|
7
|
+
id: "E22003089",
|
|
8
|
+
name: "",
|
|
9
|
+
place: [
|
|
10
|
+
{
|
|
11
|
+
id: "es.madrid.bm-madrid-fuencarral-m-av.local-13-torno",
|
|
12
|
+
topic_name: "places",
|
|
13
|
+
hook_name: "devices"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
tension: 400,
|
|
17
|
+
enabled: true,
|
|
18
|
+
_operation: "hidden",
|
|
19
|
+
__md_treedb__: {topic_name: "devices"}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
it("finds a plain field", () => {
|
|
23
|
+
expect(row_matches(row, "e22003089")).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("finds a number", () => {
|
|
27
|
+
expect(row_matches(row, "400")).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("finds a value INSIDE an fkey, which is the whole point", () => {
|
|
31
|
+
expect(row_matches(row, "local-13-torno")).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("does NOT match the structural halves of an fkey", () => {
|
|
35
|
+
/* `topic_name`/`hook_name` are the same two words on every row:
|
|
36
|
+
* matching them turns the term into a wildcard. */
|
|
37
|
+
expect(row_matches(row, "places")).toBe(false);
|
|
38
|
+
expect(row_matches(row, "hook")).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("ignores keys of the scaffolding", () => {
|
|
42
|
+
expect(row_matches(row, "hidden")).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("says no when nothing matches", () => {
|
|
46
|
+
expect(row_matches(row, "zzz")).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("says no for an empty term or a missing row", () => {
|
|
50
|
+
expect(row_matches(row, "")).toBe(false);
|
|
51
|
+
expect(row_matches(null, "a")).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("is_fkey_ref", () => {
|
|
56
|
+
it("wants the three keys", () => {
|
|
57
|
+
expect(is_fkey_ref({id: "a", topic_name: "t", hook_name: "h"})).toBe(true);
|
|
58
|
+
expect(is_fkey_ref({id: "a"})).toBe(false);
|
|
59
|
+
expect(is_fkey_ref(["a"])).toBe(false);
|
|
60
|
+
expect(is_fkey_ref(null)).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* yui_table_filter_clear.css
|
|
3
|
+
*
|
|
4
|
+
* La ✕ de un filtro de cabecera (yui_table_filter_clear.js).
|
|
5
|
+
*
|
|
6
|
+
* Al reves que la ✕ de un formulario (yui_inputs.css), esta se ve
|
|
7
|
+
* SIN foco: el filtro ya esta puesto, el foco esta en otra parte,
|
|
8
|
+
* y lo que se quiere es verlo y quitarlo.
|
|
9
|
+
*
|
|
10
|
+
* Copyright (c) 2026, ArtGins.
|
|
11
|
+
* All Rights Reserved.
|
|
12
|
+
***********************************************************************/
|
|
13
|
+
.tabulator-header-filter.yui-filter-has-clear {
|
|
14
|
+
position: relative;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* Sitio para el aspa dentro del propio input, para que no se coma
|
|
18
|
+
* ancho de columna. */
|
|
19
|
+
.tabulator-header-filter.yui-filter-has-clear input {
|
|
20
|
+
padding-right: 1.6em;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* Dos clases sobre el elemento para ganarle a las reglas propias de
|
|
24
|
+
* Bulma para `.delete` (position:relative; display:inline-block) sin
|
|
25
|
+
* depender de que hoja carga la ultima. */
|
|
26
|
+
.tabulator-header-filter .yui-filter-clear {
|
|
27
|
+
position: absolute;
|
|
28
|
+
top: 50%;
|
|
29
|
+
right: 0.25em;
|
|
30
|
+
transform: translateY(-50%);
|
|
31
|
+
display: none;
|
|
32
|
+
z-index: 4;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.tabulator-header-filter .yui-filter-clear.is-visible {
|
|
36
|
+
display: inline-block;
|
|
37
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* yui_table_filter_clear.js
|
|
3
|
+
*
|
|
4
|
+
* LA ✕ DE LOS FILTROS DE CABECERA DE UNA TABULATOR.
|
|
5
|
+
*
|
|
6
|
+
* Un filtro de columna se pone escribiendo y se quita BORRANDO lo
|
|
7
|
+
* escrito, letra a letra, y no hay nada en la cabecera que diga
|
|
8
|
+
* que se puede quitar. Con varias columnas filtradas, volver a
|
|
9
|
+
* ver la tabla entera es un ejercicio de memoria: cuáles toqué.
|
|
10
|
+
* La ✕ lo dice y lo hace.
|
|
11
|
+
*
|
|
12
|
+
* TRES DECISIONES QUE NO SON OBVIAS:
|
|
13
|
+
*
|
|
14
|
+
* - **Se ve mientras el filtro tenga contenido, con foco o sin
|
|
15
|
+
* él.** Es al revés que la ✕ de un formulario
|
|
16
|
+
* (`yui_inputs.js`), que sólo aparece en el campo que se está
|
|
17
|
+
* editando para no encender un aspa en cada campo relleno. Aquí
|
|
18
|
+
* el caso de uso es justo el contrario: el filtro ya está puesto,
|
|
19
|
+
* el foco está en otra parte, y lo que se quiere es verlo y
|
|
20
|
+
* quitarlo. Escondida tras el foco harían falta dos clics.
|
|
21
|
+
*
|
|
22
|
+
* - **Se borra con la API, no simulando teclas.**
|
|
23
|
+
* `setHeaderFilterValue(field, "")` es lo que Tabulator ofrece, y
|
|
24
|
+
* deja el valor y el filtro consistentes. Sintetizar un `input`
|
|
25
|
+
* sobre el `<input>` depende de a qué escuche el editor de ese
|
|
26
|
+
* filtro, que cambia con el tipo de columna.
|
|
27
|
+
*
|
|
28
|
+
* - **Se recorre la cabecera, no se envuelve el editor.** Las
|
|
29
|
+
* columnas se construyen del esquema del topic, así que no hay un
|
|
30
|
+
* sitio único donde envolver el editor; y la cabecera se rehace
|
|
31
|
+
* sola cuando cambian las columnas. El recorrido es idempotente
|
|
32
|
+
* -- marca lo que ya tiene aspa -- y se dispara con un
|
|
33
|
+
* `querySelector` de una sola pasada, así que repetirlo en cada
|
|
34
|
+
* render no cuesta nada.
|
|
35
|
+
*
|
|
36
|
+
* Copyright (c) 2026, ArtGins.
|
|
37
|
+
* All Rights Reserved.
|
|
38
|
+
***********************************************************************/
|
|
39
|
+
import i18next from "i18next";
|
|
40
|
+
|
|
41
|
+
import {createElement2} from "@yuneta/gobj-js";
|
|
42
|
+
|
|
43
|
+
import "./yui_table_filter_clear.css";
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
const MARK = "data-yui-filter-clear";
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
/***************************************************************
|
|
50
|
+
* Pon (o repón) la ✕ de cada filtro de cabecera de `table`.
|
|
51
|
+
*
|
|
52
|
+
* Idempotente: los que ya la tienen se saltan.
|
|
53
|
+
***************************************************************/
|
|
54
|
+
function decorate(table)
|
|
55
|
+
{
|
|
56
|
+
if(!table || !table.element) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
let $pending = table.element.querySelector(`.tabulator-header-filter:not([${MARK}])`);
|
|
60
|
+
if(!$pending) {
|
|
61
|
+
return; /* nada nuevo en la cabecera */
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let list = table.element.querySelectorAll(`.tabulator-header-filter:not([${MARK}])`);
|
|
65
|
+
for(const $filter of list) {
|
|
66
|
+
$filter.setAttribute(MARK, "1");
|
|
67
|
+
|
|
68
|
+
let $input = $filter.querySelector("input");
|
|
69
|
+
if(!$input) {
|
|
70
|
+
continue; /* un filtro que no se escribe: un select, un rango */
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* El campo se lee del `.tabulator-col` que lo contiene, que es
|
|
74
|
+
* quien lo lleva; el `<input>` no sabe de qué columna es. */
|
|
75
|
+
let $col = $filter.closest(".tabulator-col");
|
|
76
|
+
let field = $col? $col.getAttribute("tabulator-field"): "";
|
|
77
|
+
if(!field) {
|
|
78
|
+
continue; /* sin campo no hay a quién decirle que se limpie */
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
$filter.classList.add("yui-filter-has-clear");
|
|
82
|
+
|
|
83
|
+
/* La clave viaja con el botón: un `title` puesto con t() al
|
|
84
|
+
* construir es invisible para refresh_language(), y se queda en
|
|
85
|
+
* el idioma de aquel momento para siempre. */
|
|
86
|
+
let $btn = createElement2(["button", {
|
|
87
|
+
type: "button",
|
|
88
|
+
class: "delete is-small yui-filter-clear",
|
|
89
|
+
tabindex: "-1",
|
|
90
|
+
title: i18next.t("clear"),
|
|
91
|
+
"aria-label": i18next.t("clear"),
|
|
92
|
+
"data-i18n-title": "clear",
|
|
93
|
+
"data-i18n-aria-label": "clear"
|
|
94
|
+
}]);
|
|
95
|
+
|
|
96
|
+
const sync = () => {
|
|
97
|
+
$btn.classList.toggle("is-visible", !!$input.value);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
$input.addEventListener("input", sync);
|
|
101
|
+
$input.addEventListener("change", sync);
|
|
102
|
+
$btn.addEventListener("click", (event) => {
|
|
103
|
+
event.stopPropagation(); /* no ordenar la columna al pulsar */
|
|
104
|
+
table.setHeaderFilterValue(field, "");
|
|
105
|
+
sync();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
$filter.appendChild($btn);
|
|
109
|
+
sync();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/***************************************************************
|
|
114
|
+
* Engancha la ✕ a una tabla ya creada.
|
|
115
|
+
*
|
|
116
|
+
* Se llama una vez, con la tabla construida. Los enganches
|
|
117
|
+
* cubren los dos momentos en que la cabecera aparece o se
|
|
118
|
+
* rehace: cuando se construye y cuando cambian las columnas.
|
|
119
|
+
***************************************************************/
|
|
120
|
+
export function yui_table_filter_clear(table)
|
|
121
|
+
{
|
|
122
|
+
if(!table) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
decorate(table);
|
|
126
|
+
table.on("renderComplete", () => {
|
|
127
|
+
decorate(table);
|
|
128
|
+
});
|
|
129
|
+
table.on("columnVisibilityChanged", () => {
|
|
130
|
+
decorate(table);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/***************************************************************
|
|
135
|
+
* Repasa el estado de las ✕ tras un cambio que no vino del
|
|
136
|
+
* teclado -- limpiar todos los filtros, cargar una vista
|
|
137
|
+
* guardada -- porque eso no dispara `input`.
|
|
138
|
+
***************************************************************/
|
|
139
|
+
export function yui_table_filter_clear_refresh(table)
|
|
140
|
+
{
|
|
141
|
+
if(!table || !table.element) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
for(const $filter of table.element.querySelectorAll(".yui-filter-has-clear")) {
|
|
145
|
+
let $input = $filter.querySelector("input");
|
|
146
|
+
let $btn = $filter.querySelector(".yui-filter-clear");
|
|
147
|
+
if($input && $btn) {
|
|
148
|
+
$btn.classList.toggle("is-visible", !!$input.value);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|