Tableの表をソート

開始行をAlt+左/Alt+右クリック:表を↑/↓順に並べ替え 縦罫線をドラッグ:左右に調節 Ctrl+ドラッグ:何でもリサイズ

// ==UserScript==
// @name Tableの表をソート
// @description 開始行をAlt+左/Alt+右クリック:表を↑/↓順に並べ替え 縦罫線をドラッグ:左右に調節 Ctrl+ドラッグ:何でもリサイズ
// @match *://*/*
// @match file:///*.html
// @exclude *://*.2chan.net/*
// @version 0.1.2
// @run-at document-idle
// @namespace https://greasyfork.org/users/181558
// ==/UserScript==

(function() {

  const SORT_ASCENDING_ACTION = "Alt+0"; // Shift+ Alt+ Ctrl+ 0:左ボタン 1:中ボタン 2:右ボタン 3:X1ボタン 4:X2ボタン
  const SORT_DESCENDING_ACTION = "Alt+2";
  const GRABBABLE_WIDTH_PX = 8; // 機能2で縦罫線を掴める幅
  const DRAG_TARGET = "td,th"; // 左ドラッグで幅を動かせる要素
  const DRAG_TARGET_CTRL = "table,div,li,ul"; // Ctrl+左ドラッグで幅を動かせる要素

  function dragTarget(e) { return !e.ctrlKey ? DRAG_TARGET : DRAG_TARGET_CTRL }

  let ingrab = 0

  setSort();
  setDragCol();

  function setSort() {
    document.body.addEventListener("mousedown", e => {
      let key = (e?.shiftKey ? "Shift+" : "") + (e?.altKey ? "Alt+" : "") + (e?.ctrlKey ? "Ctrl+" : "") + e?.button;
      if (key == SORT_ASCENDING_ACTION || key == SORT_DESCENDING_ACTION) {
        //if (e?.target?.matches('table th,table thead td,table>tbody>tr:first-child>td')) {
        if (e?.target?.matches('table th,table td')) {
          let th = e?.target //?.closest('table th,table thead td,table tr:first-child td')
          let table = e?.target?.closest("table");

          if (!table.querySelector('[rowspan],[colspan]')) {
            let body = table?.querySelector('tbody')
            let column = th?.cellIndex;

            [...table.querySelectorAll('td')].forEach(e => e.asnum = parseFloat(e?.textContent?.trim()?.replace(/(\d)\,(\d)/gm, "$1$2"))); // ^1,000$などの数値に関しては,を除去
            [...table.querySelectorAll('td')].forEach(e => { // セル内文字列が「abc100」「100」「100abc」のどれかなら数値が主体のデータだろうと判断し数値データとして見る。「abc100abc」なら文字列と判断。,は無視。
              if (e?.textContent?.trim()?.match(/^[0-9\,\.\–\-]+\D+$|^[0-9\,\.\-\–]+$|^\D+[0-9\,\.\-\–]+$/gm)) {
                e.asnum = parseFloat(e?.textContent?.trim()?.replace(/–/gm, "-")?.replace(/^[^0-9\.\,\-\–]*([0-9\,\.\-\–])/, "$1")?.replace(/(\d)\,(\d)/g, "$1$2")) // なぜか「–」ダッシュをマイナスとして使うサイトもあるのでマイナスに置き換える
              }
            })
            let startrow = Array.from(table.rows).findIndex(r => r.contains(e.target)); //alert(startrow)

            let rows = Array.from(table.rows).slice(0, startrow).concat(
              key == SORT_ASCENDING_ACTION ?
              Array.from(table.rows).slice(startrow).sort((a, b) => a?.cells?.[column]?.asnum && b?.cells?.[column]?.asnum ? a?.cells?.[column]?.asnum == b?.cells?.[column]?.asnum ? 0 : a?.cells?.[column]?.asnum - b?.cells?.[column]?.asnum : new Intl.Collator("ja", { numeric: true, sensitivity: 'base' }).compare(a?.cells?.[column]?.textContent?.trim(), b?.cells?.[column]?.textContent?.trim())) :
              Array.from(table.rows).slice(startrow).sort((a, b) => a?.cells?.[column]?.asnum && b?.cells?.[column]?.asnum ? a?.cells?.[column]?.asnum == b?.cells?.[column]?.asnum ? 0 : b?.cells?.[column]?.asnum - a?.cells?.[column]?.asnum : new Intl.Collator("ja", { numeric: true, sensitivity: 'base' }).compare(b?.cells?.[column]?.textContent?.trim(), a?.cells?.[column]?.textContent?.trim()))
            )
            let fragment = new DocumentFragment(); // prependを使っても順番を維持するためにDocumentFragmentを使う
            rows.forEach(tr => fragment.appendChild(tr))
            body.prepend(fragment)
          } else {
            [...table.querySelectorAll('[colspan]')].forEach(e => {
              let c = e.getAttribute("colspan");
              e.removeAttribute("colspan");
              e.insertAdjacentHTML("afterend", "<td></td>".repeat(c - 1))
            });
            [...table.querySelectorAll('[rowspan]')].forEach(e => { e.removeAttribute("rowspan"); });
            [...table.querySelectorAll('tr')].filter(e => !e.hasChildNodes()).forEach(e => e.remove());
          }
        }
      }
    })
  }

  // Tableの縦罫線をドラッグで動かす
  function setDragCol() {
    document.addEventListener('mousedown', startResize);
    document.addEventListener('mousemove', e => {
      let pare = e.target.closest(dragTarget(e))
      if (pare && (e.clientX - pare.getBoundingClientRect().right > -GRABBABLE_WIDTH_PX)) {
        if (document.body.style.cursor != 'col-resize') document.body.style.cursor = 'col-resize';
      } else {
        if (!ingrab && document.body.style.cursor != 'default') document.body.style.cursor = 'default'
      }
    });

    function startResize(e) {
      let pare = e.target.closest(dragTarget(e))
      if (!pare) return;
      if (pare?.getBoundingClientRect()?.right - e?.clientX > GRABBABLE_WIDTH_PX) return;

      e.preventDefault();
      e.stopPropagation();
      let startX = e.clientX;
      let startWidth = parseFloat(getComputedStyle(pare).getPropertyValue('width'))
      ingrab = 1;
      document.addEventListener('mousemove', dragColumn);
      document.addEventListener('mouseup', endDrag);
      return false;

      function dragColumn(em) {
        //let pare=e.target.closest(dragTarget(e))
        if (["TD", "TH"].includes(pare.tagName)) {
          [...pare.closest("table").querySelectorAll(`:is(td,th):nth-child(${pare.cellIndex + 1})`)].forEach(cell => {
            cell.style.width = `${startWidth + em.clientX - startX}px`;
            cell.style.overflowWrap = "anywhere";
            //cell.style.whiteSpace = "break-spaces";
            cell.style.whiteSpace = "initial";
            cell.style.wordWrap = "break-word";
            cell.style.minWidth = `0px`;
            cell.style.maxWidth = `${Number.MAX_SAFE_INTEGER}px`;
          })
        } else {
          pare.style.resize = "both";
          pare.style.overflow = "auto"
          pare.style.width = `${startWidth + em.clientX - startX}px`;
          pare.style.minWidth = `0px`;
          pare.style.maxWidth = `${Number.MAX_SAFE_INTEGER}px`;
        }
      }

      function endDrag() {
        document.removeEventListener('mousemove', dragColumn);
        document.removeEventListener('mouseup', endDrag);
        ingrab = 0;
      };
    }
  }

  function sani(s) { return s?.replace(/&/g, "&amp;")?.replace(/"/g, "&quot;")?.replace(/'/g, "&#39;")?.replace(/`/g, '&#x60;')?.replace(/</g, "&lt;")?.replace(/>/g, "&gt;") || "" }
})();