用于基于电子的应用程序的类似 Wiki 的表格排序

Posted

技术标签:

【中文标题】用于基于电子的应用程序的类似 Wiki 的表格排序【英文标题】:Wiki-like table sorting for electron-based app 【发布时间】:2021-07-09 19:41:45 【问题描述】:

我正在为Obsidian写一个插件(使用他们的API),希望实现wiki-like table sorting,即可以按升序(第一次点击),然后降序(第二次点击)对表格进行排序的可点击标题,然后终于恢复了原来的顺序(第三次点击)。

我编写了以下代码,该代码使用回调注册 click DOM 事件,该回调检查是否在表格内执行了点击,如果是,则对表格进行排序,主要是通过重新排序 tr 元素。但是,我不确定如何在第三次点击时恢复表格到原始状态。

this.registerDomEvent(document, 'click', (evt: MouseEvent) => 
    const htmlEl = (<HTMLInputElement>evt.target);

    const th = htmlEl.closest('thead th');
    if (th == null)  return; 

    const table = htmlEl.closest('table');
    const tbody = table.querySelector('tbody');
    const thArray = Array.from(th.parentNode.children);
    const thIdx = thArray.indexOf(th);

    // all other th's are reset
    thArray.forEach((th, i) => 
        if (i != thIdx) 
            th.removeAttribute("class");
        
    );

    // set clicked th class
    th.className = this.classNames[
        (this.classNames.indexOf(th.className) + 1) % this.classNames.length
    ];

    const ascending = th.className === "header-sort-up";

    Array.from(tbody.querySelectorAll('tr:nth-child(n)'))
        .sort(this.compareFn(thIdx, ascending))
        .forEach(tr => tbody.appendChild(tr));
);

我的一个想法是使用localStorage 来存储表的原始innerHTML,但我认为这对于持久、长期使用和大量表来说不能很好地扩展。此外,如果可能的话,我不想添加额外的依赖项。

【问题讨论】:

您能否使用相关代码(minimal, working example)更新您的问题? 您是否只能访问 html 表格,还是可以选择在 javascript 中对数据进行排序,然后从那里生成表格? @Emaro - 我可以立即访问 HTML 表格。正如我所提到的,我的第一个解决方案是重新排序 tr 元素。但是,我可以用表中的数据构造一个JavaScript对象,然后为排序后的表生成HTML,但是我仍然没有看到如何恢复原始顺序。 我不知道你的tr的结构,但也许你可以导出一个id,只存储id的原始顺序。 【参考方案1】:

对于这个问题,我将假设表格将有一个&lt;thead&gt;(用于可点击元素)和一个&lt;tbody&gt;

&lt;table&gt;交互的原生JS API

要知道的一件好事是,JS 中的&lt;table&gt; 元素有一个特殊的接口HTMLTableElement,它允许我们轻松地执行一些选择/操作。所以一旦我们选择了我们的元素:

const table = document.querySelector('table')

我们可以访问它的整个结构。例如:

table.tHead 返回 &lt;thead&gt; table.tBodies[0] 返回 &lt;tbody&gt; table.tBodies[0].deleteRow(-1) 从正文中删除最后一个 &lt;tr&gt; table.tBodies[0].rows[0].cells[0].cellIndex 返回tbody 的第一行(tr)中第一个单元格(thtd)的索引 ...

定义组件状态

此外,由于您描述的行为,我们知道我们将需要存储一些“状态”变量:

initialList行的初始顺序

这很容易通过HTMLTableSectionElement 接口.rows 存储到一个数组中,我们再也不会碰它了

Array.from(tBody.rows)

currentOrder 对行进行排序的当前顺序(初始顺序、升序或降序)

为此,我将根据三个可能的顺序简单地使用012。这使得迭代变得容易:我们只需要在每次订单更改时运行以下行:

currentOrder = (currentOrder + 1) % 3

currentSortReference我们要用于对行进行排序的当前列

对于这个,我将存储用于排序的列的索引。要获得该值,我们只需要知道在&lt;thead&gt; 中单击的&lt;tr&gt; 的索引。 HTMLTableCellElement 接口 cellIndex 使这变得容易:

const index = currentTarget.cellIndex

基本实用功能

对于这样的问题,我喜欢从编写一些简单的实用函数开始。它们并不复杂,只需使用标准 JS(以及原生 HTMLTableElement 接口)。这里我们需要

emptyTable清空表体

function emptyTable(tBody, rows) 
    rows.forEach(() => tBody.deleteRow(-1))

fillTable 用有序行填充表体

function fillTable(tBody, rows) 
    rows.forEach((row) => tBody.appendChild(row))

valueFromCell 从单元格中的文本中解析一个值

function valueFromCell(element) 
    return Number(element.textContent)

compareRows 比较 2 行的值(基于特定列中的值)。这是 Array.sort() 的比较函数,这就是它返回这些值的原因

function compareRows(a, b, index) 
    const valueA = valueFromCell(a.cells[index])
    const valueB = valueFromCell(b.cells[index])
    return valueA >= valueB
        ? 1
        : -1

组件逻辑

现在我们已经有了所有这些部分,只需将它们放在一起即可:

    点击事件onHeadClick

    获取被点击的列(这是event.currentTarget.cellIndex,如上所述) 如果与之前点击的相同(currentSortReference),只需迭代顺序(currentOrder) 如果不是,请重置订单 根据当前状态对表格进行排序(见下文)
    function onHeadClick(currentTarget) 
        const index = currentTarget.cellIndex
        if (currentSortReference === index) 
            currentOrder = (currentOrder + 1) % 3
         else 
            currentSortReference = index
            currentOrder = 1
        
        sortTable(currentSortReference, currentOrder, tBody, initialList)
    
    

    对表格进行排序sortTable

    emptyTable清空表 如果是初始订单,只需在表格 (fillTable) 中填写 initialList 如果不是,则从initialList 创建一个新数组,并使用原生Array.sort 和我们在compareRows 之前定义的简单函数对其进行排序,然后填充表格(fillTable)。
    function sortTable(sort, order, tBody, rows) 
        emptyTable(tBody, rows)
        if (order === 0) 
            fillTable(tBody, rows)
         else 
            const newList = [...rows]
            newList.sort((a, b) => compareRows(a, b, sort))
            if(order === 2) 
                newList.reverse()
            
            fillTable(tBody, newList)
        
    
    

解决方案

就是这样!代码有点多,但是一点点看,就很简单了。

const table = document.querySelector('table')
const heads = table.tHead.rows[0].cells
const tBody = table.tBodies[0]

let currentSortReference = null
let currentOrder = 0
const initialList = Array.from(tBody.rows)

Array.from(heads).forEach(th => 
    th.addEventListener('click', onHeadClick)
)

/**
 * @param MouseEvent event
 */
function onHeadClick(currentTarget) 
    const index = currentTarget.cellIndex
    if (currentSortReference === index) 
        currentOrder = (currentOrder + 1) % 3
     else 
        currentSortReference = index
        currentOrder = 1
    
    sortTable(currentSortReference, currentOrder, tBody, initialList)


/**
 * @param Number sort index of head to base sort on
 * @param 0 | 1 | 2 order 0: initial, 1: descending, 2: ascending
 * @param HTMLTableSectionElement tBody 
 * @param HTMLTableRowElement[] rows all rows, in initial order
 */
function sortTable(sort, order, tBody, rows) 
    emptyTable(tBody, rows)
    if (order === 0) 
        fillTable(tBody, rows)
     else 
        const newList = [...rows]
        newList.sort((a, b) => compareRows(a, b, sort))
        if(order === 2) 
            newList.reverse()
        
        fillTable(tBody, newList)
    



/**
 * @param HTMLTableRowElement a 
 * @param HTMLTableRowElement b 
 * @param Number index of column to use in comparison
 */
function compareRows(a, b, index) 
    const valueA = valueFromCell(a.cells[index])
    const valueB = valueFromCell(b.cells[index])
    return valueA >= valueB
        ? 1
        : -1


/**
 * @param HTMLTableCellElement element 
 */
function valueFromCell(element) 
    return Number(element.textContent)


/**
 * @param HTMLTableSectionElement tBody 
 * @param HTMLTableRowElement[] rows
 */
function emptyTable(tBody, rows) 
    rows.forEach(() => tBody.deleteRow(-1))


/**
 * @param HTMLTableSectionElement tBody 
 * @param HTMLTableRowElement[] rows
 */
function fillTable(tBody, rows) 
    rows.forEach((row) => tBody.appendChild(row))
<table>
    <caption>Council budget (in £) 2018</caption>
    <thead>
        <tr>
            <th scope="col">Items</th>
            <th scope="col">Expenditure</th>
            <th scope="col">Qty</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <th scope="row">Donuts</th>
            <td>3000</td>
            <td>42</td>
        </tr>
        <tr>
            <th scope="row">Stationery</th>
            <td>18000</td>
            <td>3</td>
        </tr>
        <tr>
            <th scope="row">Chairs</th>
            <td>100</td>
            <td>248</td>
        </tr>
    </tbody>
</table>

【讨论】:

非常感谢您全面而清晰的解释!关于initialList,我认为可以将其存储到localStorage 中?在这种情况下,大量的表不会有问题吗?或者如果有人以某种方式篡改了localStorage initialList 是一个 DOM 节点数组,而 localStorage 只接受字符串,因此将其存储在 localStorage 中是行不通的。但是您可以编写一对方法来执行arrayOfDomNodesToString(initialList)initialList = stringToArrayOfDomNodes(),以便您可以将其放入并从localStorage 中解析。 我明白了。因此,通常可以将localStorage 用于此类目的。不过,我想知道***是如何做到的,因为它似乎没有使用localStorage 不,只有在需要跨页面加载保持状态时才使用 localStorage。在这种情况下,将initialList 保留为变量可能没问题。您无需将其存储在任何地方。 啊,对..那是我的困惑!因此,在我的例子中,将有一个 initialList 数组,它将简单地驻留在 RAM 中。

以上是关于用于基于电子的应用程序的类似 Wiki 的表格排序的主要内容,如果未能解决你的问题,请参考以下文章

Oracle SQL 分析查询 - 类似电子表格的递归运行总计

smratbi电子表格加序号

用于 50000 及以上数据集的 ui 网格等电子表格

Angular:Mat Sort 不适用于类似扩展的表格

基于Web的JavaScript电子表格

用于基于三个参数查找唯一行的 SQL 查询 - 类似于“在已排序的分组集中获取第一行”