Javascript Сортировка пустых значений в таблице HTML - PullRequest
7 голосов
/ 16 марта 2020

Я использую приведенный ниже код JavaScript для сортировки своих таблиц по алфавиту и цифрам. Однако он помещает строки со значениями null сверху, а не снизу.

На изображении ниже, взято из этого URL, над которым я работаю, при сортировке таблицы из наибольшего наименьшее в столбце «Изменение ранга», нули находятся сверху, а не снизу.

* В этой таблице значения Null представляют собой ячейки с тегом NEW или da sh. * Эта проблема относится ко всем столбцам / строкам

Не следует ли классифицировать пустые значения как 1 и отсортировать как таковые? Что я делаю не так?

Любая помощь действительно приветствуется.

enter image description here

const getCellValue = (tr, idx) => tr.children[idx].innerText || tr.children[idx].textContent;

const comparer = (idx, asc) => (a, b) => ((v1, v2) => 
    v1 !=='' && v2 !=='' && !isNaN(v1) && !isNaN(v2) ? v1 - v2 : v1.toString().localeCompare(v2)
    )(getCellValue(asc ? a : b, idx), getCellValue(asc ? b : a, idx));

document.querySelectorAll('th').forEach(th => th.addEventListener('click', (() => {
    const table = th.closest('table');
    Array.from(table.querySelectorAll('tr:nth-child(n+2)'))
        .sort(comparer(Array.from(th.parentNode.children).indexOf(th), this.asc = !this.asc))
        .forEach(tr => table.appendChild(tr) );
})));

Ответы [ 6 ]

8 голосов
/ 18 марта 2020

Вы можете сначала проверить значения null и проверить оба значения на конечность, такие как числа или строки, которые приводятся к числу, а затем взять либо дельту чисел, либо отсортировать по строке.

Примеры:

 v1   v2  (v1 === null) - (v2 === null) isFinite(v1) && isFinite(v2)             result
---- ---- ----------------------------- ---------------------------------------- ------
null null       true -  true ->  0       true -> v1 - v2                             0
null abc        true - false ->  1                                                   1
null  2         true - false ->  1                                                   1
abc  null      false -  true -> -1                                                  -1
 2   null      false -  true -> -1                                                  -1
abc  abc       false - false ->  0      false -> v1.toString().localeCompare(v2)     0
abc   2        false - false ->  0      false -> v1.toString().localeCompare(v2)     1
 2   abc       false - false ->  0      false -> v1.toString().localeCompare(v2)    -1
 2    2        false - false ->  0       true -> v1 - v2                             0

Код:

const comparer = (idx, asc) => (a, b) => ((v1, v2) =>
    (v1 === null) - (v2 === null) ||
    (isFinite(v1) && isFinite(v2)
        ? v1 - v2
        : v1.toString().localeCompare(v2)
    )
)(getCellValue(asc ? a : b, idx), getCellValue(asc ? b : a, idx));

Рабочий пример:

var array = [null, 2, 1, 20, 11, 'b', 'aaa', 'a', null];

array.sort((v1, v2) => 
    (v1 === null) - (v2 === null) ||
    (isFinite(v1) && isFinite(v2)
        ? v1 - v2
        : v1.toString().localeCompare(v2)
    )
);

console.log(...array);
6 голосов
/ 18 марта 2020

Считая значение null как -Infinity, следует исправить сортировку. Я могу предложить использовать свойство as c для элемента th, чтобы избежать использования this.

// don't know if theres any other listeners on the th element so i clear them before use my code ( just for testing )
document.querySelectorAll('th').forEach((th, idx) => th.removeEventListener('click', () => {}, true));
document.querySelectorAll('th').forEach((th, idx) => th.addEventListener('click', (() => {
    const table = th.closest('table');
    th.asc = !th.asc;
    [...table.querySelectorAll('tr:nth-child(n+2)')]
        .sort((a, b) => +((th.asc ? a : b).children[idx].innerText || -Infinity) - +((th.asc ? b : a).children[idx].innerText || -Infinity))
        .forEach(tr => table.appendChild(tr));
})));
1 голос
/ 24 марта 2020

Вот пример, который не приводит к нулю -1 и не полагается на трюк до infty, который по-прежнему позволяет ставить нулевые значения в конец.

  • нагрузка loadData должна отражать вашу json (я скопировал с него)
  • обработчик сортировки, возможно, сохраняет последнюю сортировку (asc / decs) на самом th-ом. Очевидно, вы можете рассматривать массив в качестве альтернативы, если вам нравится

Прямой путь

  • Мы храним строки в памяти
  • Сортируем строки ( в памяти)
  • Мы переназначим таблицу, воссоздав все tr / tds

function loadData () {
  return fetch('https://brendonbrewer.com/lbrynomics/subscriber_counts.json').then(r => r.json())
  return Promise.resolve({
    rank_change: [null, null, 5,    6, 8, 2],
    views_change: [null, 5,    null, 6, 7, 2],
  })
}
let rows = []
const myTable = document.querySelector('table')

function render(myTable, rows) {
  myTable.tBodies[0].innerHTML = ''
  rows.forEach(row => {
    const tr = document.createElement('tr')
    row.forEach(f => {
      const td = document.createElement('td')
      td.innerText = f == null ? '--' : f
      tr.appendChild(td)
    })
    myTable.tBodies[0].appendChild(tr)
  })
}

loadData().then(iRows => {
  // store the fields in order of your DOM nodes
  rows = iRows.views_change.map((_,i) => [iRows.views_change[i], iRows.rank_change[i]])
  render(myTable, rows)
})

// sort the rows
document.querySelector('table thead').onclick = e => {
  const th = e.target
  if (th.nodeName !== 'TH') { return }
  const order = th.getAttribute('data-sort') === '-1' ? -1 : 1
  th.setAttribute('data-sort', order * -1)
  const fieldIndex = [...th.parentNode.children].findIndex(other => other === th)
  const cmp = (r, s) => {
    const a = r[fieldIndex]
    const b = s[fieldIndex]
    return a === null && b === null ? 0 :
      a === null && b !== null ? 1 :
      b === null && a !== null ? -1 :
      a - b
  }
  rows.sort(order === 1 ? cmp : (a,b) => -1 * cmp(a,b))
  console.time('render')
  render(myTable, rows)
  console.timeEnd('render')
}
<table id="sub-stats">
  <thead> <!-- first fix your th. They go to thead. -->
    <tr>
      <th class="views-change">Views Change</th>  
      <th class="rank-change">Rank Change</th>
    </tr>
  </thead>
  <tbody>
  </tbody>
</table>

То же самое, но мы повторно используем существующие строки

Мы в основном сопоставляем каждую строку в памяти с ее узлом DOM (то есть tr) , См rowToNode в коде ниже

function loadData () {
  return fetch('https://brendonbrewer.com/lbrynomics/subscriber_counts.json').then(r => r.json())
  return Promise.resolve({
    rank_change: [null, null, 5,    6, 8, 2],
    views_change: [null, 5,    null, 6, 7, 2],
  })
}
let rows = []
let rowToNode = new Map()
const myTable = document.querySelector('table')

function init(myTable, rows) {
  const rowToNode = new Map()
  rows.forEach(row => {
    const tr = document.createElement('tr')
    row.forEach(f => {
      const td = document.createElement('td')
      td.innerText = f == null ? '--' : f
      tr.appendChild(td)
    })
    myTable.tBodies[0].appendChild(tr)
    rowToNode.set(row, tr)
  })
  return rowToNode
}

loadData().then(iRows => {
  // store the fields in order of your DOM nodes
  rows = iRows.views_change.map((_,i) => [iRows.views_change[i], iRows.rank_change[i]])
  rowToNode = init(myTable, rows)
})

// sort the rows
document.querySelector('table thead').onclick = e => {
  const th = e.target
  if (th.nodeName !== 'TH') { return }
  const order = th.getAttribute('data-sort') === '-1' ? -1 : 1
  th.setAttribute('data-sort', order * -1)
  const fieldIndex = [...th.parentNode.children].findIndex(other => other === th)
  const cmp = (r, s) => {
    const a = r[fieldIndex]
    const b = s[fieldIndex]
    return a === null && b === null ? 0 :
      a === null && b !== null ? 1 :
      b === null && a !== null ? -1 :
      a - b
  }
  rows.sort(order === 1 ? cmp : (a,b) => -1 * cmp(a,b))
  console.time('render')
  myTable.tBodies[0].innerHTML = ''
  const tpl = new DocumentFragment()
  rows.forEach(r => {
    tpl.appendChild(rowToNode.get(r))
  })
  myTable.tBodies[0].appendChild(tpl)
  console.timeEnd('render')
}
<!DOCTYPE html>
<html>
<body>
<table id="sub-stats">
  <thead> <!-- first fix your th. They go to thead. -->
    <tr>
      <th class="views-change">Views Change</th>  
      <th class="rank-change">Rank Change</th>
    </tr>
  </thead>
  <tbody>
  </tbody>
</table>
</body>
</html>

Немного рефакторинга

Глобальные переменные не самые лучшие, надоедливые переменные rows и связанные с ними rowToNode не являются более гламурный и получит область, большую, чем им нужно.

Возможный способ - создать Компонент, который внутренне управляет этими переменными, и в конечном итоге поместить его в свой собственный файл. Будучи немного напуганным, мы могли бы написать WebComponent

class MyTable extends HTMLTableElement {
  constructor () {
    super ()
    this._shadowRoot = this.attachShadow({ 'mode': 'open' });
    this._rows = []
    this.rowToNode = new Map()
    const template = document.createElement('template')
    template.innerHTML = `
      <thead>
        <tr>
          <th class="views-change">Views Change</th>  
          <th class="rank-change">Rank Change</th>
        </tr>
      </thead>
      <tbody>
      </tbody>
      <style>th{cursor:pointer;}</style>
    `
    this._shadowRoot.appendChild(template.content)
    this.tbody = this._shadowRoot.querySelector('tbody')
    this.render();
    this._shadowRoot.querySelector('thead').onclick = this.handleSort.bind(this)
  }
  loadRows (rows) {
    this.rowToNode = new Map()
    this._rows = rows
    rows.forEach(row => {
      const tr = document.createElement('tr')
      row.forEach(f => {
        const td = document.createElement('td')
        td.innerText = f == null ? '--' : f
        tr.appendChild(td)
      })
      this.tbody.appendChild(tr)
      this.rowToNode.set(row, tr)
    })
  }
  render () {
    this.tbody.innerHTML = ''
    const tpl = new DocumentFragment()
    this._rows.forEach(r => {
      tpl.appendChild(this.rowToNode.get(r))
    })
    this._shadowRoot.querySelector('tbody').appendChild(tpl)
  }
  handleSort (e) {
    const th = e.target
    if (th.nodeName !== 'TH') { return }
    const order = th.getAttribute('data-sort') === '-1' ? -1 : 1
    th.setAttribute('data-sort', order * -1)
    const fieldIndex = [...th.parentNode.children].findIndex(other => other === th)
    const cmp = (r, s) => {
      const a = r[fieldIndex]
      const b = s[fieldIndex]
      return a === null && b === null ? 0 :
        a === null && b !== null ? 1 :
        b === null && a !== null ? -1 :
        a - b
    }
    this._rows.sort(order === 1 ? cmp : (a,b) => -1 * cmp(a,b))
    console.time('render')
    this.render()
    console.timeEnd('render')
  }
}
customElements.define('my-table', MyTable, { extends: 'table' })



function loadData () {
  return fetch('https://brendonbrewer.com/lbrynomics/subscriber_counts.json').then(r => r.json())
  return Promise.resolve({
    rank_change: [null, null, 5,    6, 8, 2],
    views_change: [null, 5,    null, 6, 7, 2],
  })
}
loadData().then(iRows => {
  // store the fields in order of your DOM nodes
  rows = iRows.views_change.map((_,i) => [iRows.views_change[i], iRows.rank_change[i]])
  document.querySelector('my-table').loadRows(rows)
})
<my-table id="sub-stats"></my-table>

Альтернатива веб-компонентам

Наконец, если нам нужны не веб-компоненты, а сырой объект, мы можем создать сырой объект. Создайте некоторый класс, который принимает (табличный) узел в качестве аргумента, и работайте с ним так же, как мы это делали.

Ниже кода ближе (по DOM) к живому производству.

class MyTable {
  constructor (node) {
    this.node = node
    this._rows = []
    this.rowToNode = new Map()
    const template = document.createElement('template')
    template.innerHTML = `
      <thead>
        <tr>
          <th class="ranks">Channel rank</th>
          <th class="vanity_names">Top 200 LBRY Channels</th>
          <th class="subscribers">followers</th>
          <th class="views">Content view</th>
          <th class="views_change">Views Change</th>  
          <th class="rank_change">Rank Change</th>
        </tr>
      </thead>
      <tbody>
      </tbody>
    `
    this.node.appendChild(template.content)
    this.tbody = this.node.querySelector('tbody')
    this.render();
    this.node.querySelector('thead').onclick = this.handleSort.bind(this)
  }
  loadRows (rows) {
    this.rowToNode = new Map()
    this._rows = rows
    rows.forEach(row => {
      const tr = document.createElement('tr')
      row.forEach(f => {
        const td = document.createElement('td')
        td.innerText = f == null ? '--' : f
        tr.appendChild(td)
      })
      this.tbody.appendChild(tr)
      this.rowToNode.set(row, tr)
    })
  }
  render () {
    this.tbody.innerHTML = ''
    const tpl = new DocumentFragment()
    this._rows.forEach(r => {
      tpl.appendChild(this.rowToNode.get(r))
    })
    this.tbody.appendChild(tpl)
  }
  handleSort (e) {
    const th = e.target
    if (th.nodeName !== 'TH') { return }
    const order = th.getAttribute('data-sort') === '-1' ? -1 : 1
    th.setAttribute('data-sort', order * -1)
    const fieldIndex = [...th.parentNode.children].findIndex(other => other === th)
    const cmp = (r, s) => {
      const a = r[fieldIndex]
      const b = s[fieldIndex]
      return a === null && b === null ? 0 :
        a === null && b !== null ? 1 :
        b === null && a !== null ? -1 :
        a - b
    }
    this._rows.sort(order === 1 ? cmp : (a,b) => -1 * cmp(a,b))
    console.time('render')
    this.render()
    console.timeEnd('render')
  }
}

function loadData () {
  return fetch('https://brendonbrewer.com/lbrynomics/subscriber_counts.json').then(r => r.json())
  return Promise.resolve({
    rank_change: [null, null, 5,    6, 8, 2],
    views_change: [null, 5,    null, 6, 7, 2],
  })
}
console.log('go')

const table = new MyTable(document.querySelector('table'))
table.render()
loadData().then(iRows => {
  // store the fields in order of your DOM nodes
  const fields = ['ranks', 'vanity_names', 'subscribers', 'views', 'views_change', 'rank_change']
  rows = iRows.views_change.map((_,i) => fields.map(f => iRows[f][i]))
  console.log('go')
  table.loadRows(rows)
})
<link href="https://lbrynomics.com/wp-content/themes/Divi/style.dev.css" rel="stylesheet"/>

<body class="page-template-default page page-id-12177 custom-background et-tb-has-template et-tb-has-footer et_pb_button_helper_class et_fullwidth_nav et_fixed_nav et_show_nav et_primary_nav_dropdown_animation_fade et_secondary_nav_dropdown_animation_fade et_header_style_left et_cover_background et_pb_gutter windows et_pb_gutters3 et_pb_pagebuilder_layout et_smooth_scroll et_no_sidebar et_divi_theme et-db gecko">
<div id="et-main-area">
<div id="main-content">
<div class="entry-content">
<div class="et-l et-l--post">
<div class="et_builder_inner_content et_pb_gutters3">
<div class="et_pb_section et_pb_section_0 et_pb_with_background et_section_regular">
<div class="et_pb_row et_pb_row_0">
<div class="et_pb_column et_pb_column_4_4 et_pb_column_0  et_pb_css_mix_blend_mode_passthrough et-last-child">
<div class="et_pb_module et_pb_code et_pb_code_0">
<div class="et_pb_module et_pb_tabs et_pb_tabs_0 et_slide_transition_to_1">
<div class="et_pb_tab et_pb_tab_1 clearfix et-pb-active-slide">
<div class="et_pb_tab_content">
<div class="et_pb_section et_pb_section_4 et_pb_with_background et_section_regular">
<div class="et_pb_row et_pb_row_3">
<div class="et_pb_column et_pb_column_4_4 et_pb_column_3  et_pb_css_mix_blend_mode_passthrough et-last-child">
<div class="et_pb_module et_pb_code et_pb_code_1">
<div class="et_pb_code_inner">


<table id="sub-stats"></table>









<div/>
<div/>
<div/>
<div/>
<div/>
<div/>
<div/>
<div/>
<div/>
<div/>
<div/>
<div/>
<div/>
<div/>
<div/>
<div/>
<div/>

</body>
1 голос
/ 18 марта 2020

Если это NaN, вы должны установить изменение на 0 или + -Infinity в зависимости от того, где вы хотите, чтобы оно было сверху вниз или на 0. (изменение значения применяется только при сравнении)

Я сделал расширенный код для простоты понимания.

Проверьте это сравнениеNew и измените его в соответствии со своими потребностями.


const comparerNew = (idx, asc) => (a, b) => ((v1, v2) => {      
    if (isNaN(v1)) {
      v1=0; // or Infinity 
    }
    if (isNaN(v2)) {
      v2=0; // or Infinity 
    }
    return v1 !=='' && v2 !=='' ? v1 - v2 : v1.toString().localeCompare(v2)
}

   )(getCellValue(asc ? a : b, idx), getCellValue(asc ? b : a, idx));

document.querySelectorAll('th').forEach(th => th.addEventListener('click', (() => {
    const table = th.closest('table');
    Array.from(table.querySelectorAll('tr:nth-child(n+2)'))
        .sort(comparerNew(Array.from(th.parentNode.children).indexOf(th), this.asc = !this.asc))
        .forEach(tr => table.appendChild(tr) );
  console.log("sorded");
})));

0 голосов
/ 24 марта 2020

Я проверил ваш сайт, и я думаю, что ваши изменения ранга номер. Таким образом, getCellValue должен возвращать только число и сравнивать с числом. Пожалуйста, проверьте следующий код.

function getCellValue (tr, idx) {
    const val = tr.children[idx].innerText.trim() || tr.children[idx].textContent.trim()
    return !isNaN(val)?val:0;
}

function comparer (idx, asc) {
    asc = asc ? 1 : -1

    return function (a, b) {
        a = getCellValue(a, idx)
        b = getCellValue(b, idx)
        return asc * ( a - b);
    }
}
0 голосов
/ 23 марта 2020

Ваш код выглядит недружелюбно. Трудно читать. Я делаю небольшой рефакторинг, а также решаю вашу проблему:

function getCellValue (tr, idx) {
  const val = tr.children[idx].innerText.trim() || tr.children[idx].textContent.trim()

  if (!val || val === '-' || val.toLowerCase() === 'new') {
    return null
  }

  return val
}

function comparer (idx, asc) {
  asc = asc ? 1 : -1

  return function (a, b) {
    a = getCellValue(a, idx)
    b = getCellValue(b, idx)

    if (b === null) {
      return asc
    }

    if (a === null) {
      return -asc
    }

    if (isFinite(Number(a)) && isFinite(Number(b))) {
      return asc * (parseInt(a, 10) - parseInt(b, 10))
    }


    return asc * a.toString().localeCompare(b)
  }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...