開始行をAlt+左/Alt+右クリック:表を↑/↓順に並べ替え 縦罫線をドラッグ:左右に調節 Ctrl+ドラッグ:何でもリサイズ
// ==UserScript== // @name Tableの表をソート // @description 開始行をAlt+左/Alt+右クリック:表を↑/↓順に並べ替え 縦罫線をドラッグ:左右に調節 Ctrl+ドラッグ:何でもリサイズ // @match *://*/* // @match file:///*.html // @exclude *://*.2chan.net/* // @version 0.1.5 // @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 document.head.insertAdjacentHTML("beforeend", `<style>.tsMovingAttract{outline:1px solid #8800ffe0 !important;}</style>`) 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"); e.stopPropagation() e.preventDefault() if (!table.querySelector('[rowspan],[colspan]')) { let body = table?.querySelector('tbody') let column = th?.cellIndex; // [...table.querySelectorAll('td:not([data-sortkey])')].forEach(e => e.asnum = parseFloat(e?.textContent?.trim()?.replace(/(\d)\,(\d)/gm, "$1$2"))); // ^1,000$などの数値に関しては,を除去 [...table.querySelectorAll('td:not([data-sortkey])')].filter(e => e?.textContent?.trim()?.match(/^[0-9\,\.]+$/gm)).forEach(e => { let asnum = parseFloat(e?.textContent?.trim()?.replace(/^(\d)\,(\d)$/gm, "$1$2")) if (!Number.isNaN(asnum)) e.asnum = asnum; }); // ^1,000$などの数値に関しては,を除去 [...table.querySelectorAll('td:not([data-sortkey])')].forEach(e => { // セル内文字列が「abc100」「100」「100abc」のどれかなら数値が主体のデータだろうと判断し数値データとして見る。「abc100abc」なら文字列と判断。,は無視。 if (e?.textContent?.trim()?.match(/^[0-9\,\.\–\-]+\D+$|^[0-9\,\.\-\–]+$|^\D+[0-9\,\.\-\–]+$/gm)) { let asnum = parseFloat(e?.textContent?.trim()?.replace(/–/gm, "-")?.replace(/^[^0-9\.\,\-\–]*([0-9\,\.\-\–])/, "$1")?.replace(/(\d)\,(\d)/g, "$1$2")) // なぜか「–」ダッシュをマイナスとして使うサイトもあるのでマイナスに置き換える if (!Number.isNaN(asnum)) e.asnum = asnum; } }) let startrow = Array.from(table.rows).findIndex(r => r.contains(e.target)); //alert(startrow) let col = new Intl.Collator("ja", { numeric: true, sensitivity: 'base' }) 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 !== undefined && b?.cells?.[column]?.asnum !== undefined ? a?.cells?.[column]?.asnum == b?.cells?.[column]?.asnum ? 0 : a?.cells?.[column]?.asnum - b?.cells?.[column]?.asnum : col.compare(a?.cells?.[column]?.dataset?.sortkey || a?.cells?.[column]?.textContent?.trim(), b?.cells?.[column]?.dataset?.sortkey || b?.cells?.[column]?.textContent?.trim())) : Array.from(table.rows).slice(startrow).sort((a, b) => a?.cells?.[column]?.asnum !== undefined && b?.cells?.[column]?.asnum !== undefined ? a?.cells?.[column]?.asnum == b?.cells?.[column]?.asnum ? 0 : b?.cells?.[column]?.asnum - a?.cells?.[column]?.asnum : col.compare(b?.cells?.[column]?.dataset?.sortkey || b?.cells?.[column]?.textContent?.trim(), a?.cells?.[column]?.dataset?.sortkey || a?.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 : col.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()); } return false } } }) } // Tableの縦罫線をドラッグで動かす function setDragCol() { document.addEventListener('mousedown', startResize); document.addEventListener('mousemove', e => { let pare = e?.target?.nodeType == 1 && 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?.nodeType == 1 && e?.target?.closest(dragTarget(e)) let pare = (e?.ctrlKey && e?.target?.matches("td,th,tr") && e?.target?.closest("table")) || 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 startY = e.clientY; let startWidth = parseFloat(getComputedStyle(pare).getPropertyValue('width')); [...pare.querySelectorAll(':is(TR , TH , TD , LI)')].forEach(cell => { cell.dataset.tspadding = (parseFloat(getComputedStyle(cell).getPropertyValue('padding-top')) + parseFloat(getComputedStyle(cell).getPropertyValue('padding-bottom'))) / 2; }); 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`; let movedY = startY - em.clientY; let offsetY = ((movedY) - Math.sign(movedY) * Math.min(32, Math.abs(movedY))) / 16; // 縦に32ピクセル以上動いたらセルの縦paddingも調節する [...pare.querySelectorAll(':is(TR , TH , TD , LI)')].forEach(cell => { offsetY != 0 && cell.classList.add("tsMovingAttract"); setTimeout(() => cell?.classList?.remove("tsMovingAttract"), 17, cell) cell.style.paddingTop = `${+cell.dataset.tspadding + offsetY}px`; cell.style.paddingBottom = `${+cell.dataset.tspadding + offsetY}px`; }); } } function endDrag() { document.removeEventListener('mousemove', dragColumn); document.removeEventListener('mouseup', endDrag); ingrab = 0; pare.querySelectorAll('[data-tspadding]').forEach(e => delete e.dataset.tspadding); }; } } function sani(s) { return s?.replace(/&/g, "&")?.replace(/"/g, """)?.replace(/'/g, "'")?.replace(/`/g, '`')?.replace(/</g, "<")?.replace(/>/g, ">") || "" } })();