Tengo una tabla que construí en JavaScript que es básicamente una gran lista de publicaciones, con un encabezado de "año", seguido de una fila para cada publicación de ese año:
<table class="mytable" id="myTable"> <tr class="header"> <th style="width:60%;">2020</th> </tr> <tr name="latin_america_zero-deforestation"> <td>"Cattle Ranchers and Deforestation.” </td> </tr> <tr name="latin_america_policy-outcomes"> <td>“Impacts on Land Use Conversion.” </td> </tr> <tr name="latin_america_supply-chain_policy-outcomes"> <td>“Costs of the Amazon Soy Moratorium.” </td> </tr> <tr class="header"> <th style="width:60%;">2019</th> </tr> <tr name="africa_policy-outcomes"> <td>“Environmental Change” </td> </tr> <tr name="latin_america_policy-outcomes"> <td>“Land Use Change” </td> </tr> <tr class="header"> <th style="width:60%;">2018</th> </tr> <tr name="north_america_zero-deforestation"> <td>“Deforestation Remedies” </td> </tr> <tr name="latin_america_zero-deforestation"> <td>“Land Use Change in Latin American Cerrados” </td> </tr> </table> Según el nombre de tr , puedo filtrar por palabras clave (ya sea región o tema) a través de algunos botones:
<button class="btn" onclick="filterSelection('policy-outcomes')">Policy Outcomes</button> <button class="btn" onclick="filterSelection('latin_america')"> Latin America</button> Los botones llaman a esta función, que filtra según las palabras clave que se encuentran en la etiqueta de nombre tr :
//Sort by class.. function filterSelectionByTopic(thing) { var rows = $('#myTable').find('tr:not(:has(th))').get(); console.log(rows) $("#myTable tr").show() $('#myTable').find('tr:not(:has(th))').not('thead td').not("[name*=" + thing + "]").fadeOut(300) var tabhead = "header" //$("#myTable tbody tr:not(."+thing + ')').hide(); //$("#myTable tr.header").fadeIn(1) }Si bien esto funciona bien, quiero que los filtros oculten los encabezados de las tablas (años) donde no se devuelven filas.
Por ejemplo, si filtré usando filterSelection('policy-outcomes') : 2020 y 2019 tienen publicaciones que cumplen con este criterio, 2018 no. Actualmente, 2018 se mostrará como un encabezado de tabla sin filas debajo, pero me gustaría ocultar cualquier encabezado de tabla que no tenga filas que cumplan con los criterios.
Entonces, en esencia, filterSelection('policy-outcomes') debería verse así:
2020 Impacts on Land Use Conversion. Costs of the Amazon Soy Moratorium. 2019 Environmental Change. Land Use Change.¿Cómo puedo lograr esto?
Puede combinar el selector :has con el selector hermano adyacente ( s1 + s2 ).
Así que haz algo como:
$("#myTable").find("tr.header:has(+ tr.header)").hide()Lo que seleccionaría cada fila de encabezado seguida inmediatamente por otra fila de encabezado, es decir, cada encabezado que no tenga al menos una fila asociada.
y supongo que uso algo como :last-child para capturar el caso de borde donde el último encabezado no devuelve filas.
Esta solución comienza en la última fila y sube por el árbol DOM. Si una fila de la tabla no tiene un término, lo oculta. También agregué un botón claro.
function filterSelection(topic) { const index = {}; const HIDE_CLASS = 'policy-no-term'; let tr = document.querySelector('tr:last-child'); let hasTerms = false; while(tr) { tr.classList.remove(HIDE_CLASS); if (tr.classList.contains('header')) { if (hasTerms === false) { tr.classList.add(HIDE_CLASS); } hasTerms = false; } else if(tr.getAttribute('name').indexOf(topic) > -1) { hasTerms = true; tr.classList.remove(HIDE_CLASS); } else { hasTerms = hasTerms || false; tr.classList.add(HIDE_CLASS); } tr = tr.previousElementSibling; } }Esta solución pasa por cada fila y guarda los encabezados. Luego busca las filas de datos que le siguen que coincidan con la expresión regular del término de búsqueda. Si no hubo filas de datos después del encabezado antes de que llegue al siguiente encabezado, se aplicará un estilo de ninguno al nodo del encabezado.
El siguiente ejemplo establece un término de búsqueda de áfrica y luego, 4 segundos más tarde, aplica un término de búsqueda de latín.
//Sort by class.. function filterSelectionByTopic(thing) { $("#myTable tr").show() $('#myTable').find('tr:not(:has(th))').not('thead td').not("[name*=" + thing + "]").fadeOut(300) const allRows = Array.from($('#myTable').find('tr')); let currentHeaderNode = null; let visibleRows = 0; allRows.forEach((row, index,{ length })=>{ // reset display none from last search if (row.classList.contains("header")) row.style.display=undefined; if (row.classList.contains("header") || index===length-1) { if (!visibleRows && currentHeaderNode !== null) { currentHeaderNode.style.display="none"; } // header to target if no sibling data rows currentHeaderNode = row; visibleRows = 0; return } if (new RegExp(thing,"gi").test(row.getAttribute("name"))) { // There IS a row that will be shown (don't hide header) visibleRows++; } }) } filterSelectionByTopic("africa") setTimeout(()=>{filterSelectionByTopic("latin")},4000) th { background-color:red; } <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script> <table class="mytable" id="myTable"> <tr class="header"> <th style="width:60%;">2020</th> </tr> <tr name="latin_america_zero-deforestation"> <td>"Cattle Ranchers and Deforestation.” </td> </tr> <tr name="latin_america_policy-outcomes"> <td>“Impacts on Land Use Conversion.” </td> </tr> <tr name="latin_america_supply-chain_policy-outcomes"> <td>“Costs of the Amazon Soy Moratorium.” </td> </tr> <tr class="header"> <th style="width:60%;">2019</th> </tr> <tr name="africa_policy-outcomes"> <td>“Environmental Change” </td> </tr> <tr name="latin_america_policy-outcomes"> <td>“Land Use Change” </td> </tr> <tr class="header"> <th style="width:60%;">2018</th> </tr> <tr name="north_america_zero-deforestation"> <td>“Deforestation Remedies” </td> </tr> <tr name="latin_america_zero-deforestation"> <td>“Land Use Change in Latin American Cerrados” </td> </tr> </table>Esta es mi versión de Vanilla JavaScript.
Antes de escribir el script, cambié todos los atributos de name a data-name ya que tr s no puede tener atributos de name "legalmente".
En un primer bucle filter() , hago invisibles todos los tr s y filtro solo aquellos tr s que no coinciden con el patrón pat y no son de clase "encabezado". En un bucle forEach() aplicado en este conjunto filtrado, hago visibles solo aquellos tr s que tienen un atributo dataset.name o cuyo sucesor directo tiene este atributo.
const trs=[...document.querySelector("#myTable>tbody").children]; function fltTbl(pat){ trs .filter(tr=>(tr.style.display="none",tr.classList.contains("header")|| tr.dataset.name&&tr.dataset.name.indexOf(pat)>-1)) .forEach((tr,i,a)=>{ if (tr.dataset.name || a[i+1]&&a[i+1].dataset.name) tr.style.display=""; }); } <button class="btn" onclick="fltTbl('policy-outcomes')">Policy Outcomes</button> <button class="btn" onclick="fltTbl('latin_america')">Latin America</button> <button class="btn" onclick="fltTbl('deforestation')">Deforestation</button> <table class="mytable" id="myTable"> <tr class="header"> <th style="width:60%;">2020</th> </tr> <tr data-name="latin_america_zero-deforestation"> <td>"Cattle Ranchers and Deforestation.” </td> </tr> <tr data-name="latin_america_policy-outcomes"> <td>“Impacts on Land Use Conversion.” </td> </tr> <tr data-name="latin_america_supply-chain_policy-outcomes"> <td>“Costs of the Amazon Soy Moratorium.” </td> </tr> <tr class="header"> <th style="width:60%;">2019</th> </tr> <tr data-name="africa_policy-outcomes"> <td>“Environmental Change” </td> </tr> <tr data-name="latin_america_policy-outcomes"> <td>“Land Use Change” </td> </tr> <tr class="header"> <th style="width:60%;">2018</th> </tr> <tr data-name="north_america_zero-deforestation"> <td>“Deforestation Remedies” </td> </tr> <tr data-name="latin_america_zero-deforestation"> <td>“Land Use Change in Latin American Cerrados” </td> </tr> </table>Por lo general , no es una buena idea usar controladores de eventos en línea. El fragmento usa delegación de eventos y es-20xx sin formato.
Los botones contienen un atributo de data-term partir del cual se puede construir un selector de consulta css para las filas data-name . Los atributos de name se reemplazan por atributos data-name datos, con valores delimitados por comas, correspondientes a los atributos de data-term de datos de los botones.
A las filas de 'año' agregué un atributo data-year datos, para poder encontrar fácilmente un encabezado de una fila data-name .
El manejador:
data-term -atributo del botón en el que se hizo clicEl fragmento también está disponible en Stackblitz .
const doFilter = FilterHandlerFactory(); document.addEventListener(`click`, handle); function handle(evt) { if (evt.target.dataset.term) { return doFilter(evt.target); } } // Factory to avoid recreation of [showParentHeader] on every click function FilterHandlerFactory() { const isHead = r => r.dataset.year && r; // helper (closed over): // move up until a header is found and display that const showParentHeader = row => { let prev = row.previousElementSibling; while (prev && !isHead(prev)) { prev = prev.previousElementSibling; } prev.classList.remove(`hidden`); }; return fromBtn => { const all = fromBtn.dataset.term === `All`; const filter = RegExp(fromBtn.dataset.term, "i"); document.querySelectorAll(`#myTable tr`) .forEach( row => { if (all || filter.test(row.dataset.name)) { row.classList.remove(`hidden`); return all || showParentHeader(row); } row.classList.add(`hidden`); } ); } } body { font: normal 12px/15px verdana, arial; } #myTable { border: 1px solid #999; border-collapse: separate; margin-top: 1rem; width: 80vw; } #myTable th { background-color: #ddd; border-bottom: 1px dotted #777; border-top: 1px dotted #777; } button { margin-right: 2px; } button[data-term]:before { content: attr(data-term); } .hidden { display: none; } .btnBlock { display: flex; flex-direction: row; } <div class="btnBlock"> <button data-term="Policy Outcomes"></button> <button data-term="Latin America"></button> <button data-term="Deforestation"></button> <button data-term="Supply Chain"></button> <button data-term="Africa"></button> <button data-term="North America"></button> <button data-term="All"></button> </div> <table class="mytable" id="myTable"> <tr class="header" data-year="2020"> <th>2020</th> </tr> <tr data-name="latin america, deforestation"> <td>"Cattle Ranchers and Deforestation.”</td> </tr> <tr data-name="latin america, policy outcomes"> <td>“Impacts on Land Use Conversion.”</td> </tr> <tr data-name="latin america, supply chain, policy outcomes"> <td>“Costs of the Amazon Soy Moratorium.”</td> </tr> <tr class="header" data-year="2019"> <th>2019</th> </tr> <tr data-name="africa, policy outcomes"> <td>“Environmental Change”</td> </tr> <tr data-name="latin america, policy outcomes"> <td>“Land Use Change”</td> </tr> <tr class="header" data-year="2018"> <th>2018</th> </tr> <tr data-name="north america, deforestation"> <td>“Deforestation Remedies”</td> </tr> <tr data-name="latin america, deforestation"> <td>“Land Use Change in Latin American Cerrados”</td> </tr> </table> Alternativamente, considere no usar una <table> (después de todo, los datos no son realmente tabulares).
Patio de juegos Stackblitz para este fragmento.
document.addEventListener(`click`, handle); const doFilter = FilterHandlerFactory(`myData`); function handle(evt) { if (evt.target.dataset.term) { return doFilter(evt.target); } } // filter handler factory function FilterHandlerFactory(filterId) { let filterTerm, all; const filterYears = document.querySelectorAll(`#${filterId} div[data-year]`); const filterItems = document.querySelectorAll(`#${filterId} ul li`); const year = item => item.closest(`[data-year]`); const hide = el => el.classList.add(`hidden`); const show = el => el.classList.remove(`hidden`); const displayFilteredItem = item => { show(item); show(year(item)); }; const setTerms = fromBtn => { all = fromBtn.dataset.term === `All`; filterTerm = RegExp(fromBtn.dataset.term, "i"); }; const filter = item => all || filterTerm.test(item.dataset.name || '-') ? displayFilteredItem(item) : hide(item); return fromBtn => { setTerms(fromBtn); filterYears.forEach( hide ) filterItems.forEach( filter ); }; } body { margin: 2rem; font: normal 12px/15px verdana, arial; } #myData { margin-top: 1rem; width: 80vw; } #myData div[data-year] { opacity: 1; max-height: initial; transition: all ease-in-out .7s; } #myData div[data-year].hidden { opacity: 0; max-height: 0; margin: 0; } #myData div[data-year] ul li.hidden { display: none; } button { margin-right: 2px; } button[data-term]:before { content: attr(data-term); } .btnBlock { display: flex; flex-direction: row; } #myData div[data-year] ul { margin-left: -1.5rem; margin-top: 0.2rem; } #myData div[data-year] ul li { list-style-type: '\2713'; padding-left: 5px; } #myData div[data-year]:before { content: attr(data-year); text-align: center; font-weight: bold; display: block; width: 100%; background-color: #ddd; border-bottom: 1px dotted #777; border-top: 1px dotted #777; } <div class="btnBlock"> <button data-term="Policy Outcomes"></button> <button data-term="Latin America"></button> <button data-term="Deforestation"></button> <button data-term="Supply Chain"></button> <button data-term="Africa"></button> <button data-term="North America"></button> <button data-term="All"></button> </div> <div id="myData"> <div data-year="2020"> <ul> <li data-name="latin america, deforestation"> Cattle Ranchers and Deforestation. </li> <li data-name="latin america, policy outcomes"> Impacts on Land Use Conversion. </li> <li data-name="latin america, supply chain, policy outcomes"> Costs of the Amazon Soy Moratorium. </li> </ul> </div> <div data-year="2019"> <ul> <li data-name="africa, policy outcomes">Environmental Change</li> <li data-name="latin america, policy outcomes">Land Use Change</li> </ul> </div> <div data-year="2018"> <ul> <li data-name="north america, deforestation"> Deforestation Remedies </li> <li data-name="latin america, deforestation"> Land Use Change in Latin American Cerrados </li> </ul> </div> </div>Puede combinar el selector :has con el selector hermano adyacente (s1 + s2). Así que haz algo como:
$("#myTable").find("tr.header:has(+ tr.header)").hide()