From d678d5786b17eb2050737a85d346ae765339b564 Mon Sep 17 00:00:00 2001 From: Lee Wannacott Date: Sun, 21 May 2023 15:35:46 +1200 Subject: [PATCH] Refactorings and fix file-size-sort sorting by cell rather than row (#98) * Move dataattributes function out of make column sortable * Move sortfilezize, sortdates, sortruntime out of makecolumnsortable so not defined multiple times. * Move getTableData out of make each column sortable. * Move updateTable out of makeeachcolumn sortable. * Move getColumn function definition onto column object. * Move updateTable stuff related to fileSize into its own function to be less confusing. * WIP: fix file-size-sort useing innerhtml. * WIP 2: fix file-size-sort useing innerhtml. * Fixed file-size-sort sorting cells rather than rows. * Remove remember sort from declared for each column. --- public/table-sort.js | 626 +++++++++++++++++++++++-------------------- 1 file changed, 331 insertions(+), 295 deletions(-) diff --git a/public/table-sort.js b/public/table-sort.js index 4b19ea0..be7e136 100644 --- a/public/table-sort.js +++ b/public/table-sort.js @@ -28,7 +28,6 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { const [getTagTable] = getHTMLTables(); const columnIndexAndTableRow = {}; - const fileSizeColumnTextAndRow = {}; for (let table of getTagTable) { if (table.classList.contains("table-sort")) { makeTableSortable(table); @@ -136,348 +135,376 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { } } - function makeEachColumnSortable( - th, - columnIndex, - table, - sortableTable, - columnIndexesClicked - ) { - const desc = th.classList.contains("order-by-desc"); - const tableArrows = sortableTable.classList.contains("table-arrows"); - const [arrowUp, arrowDown] = [" ▲", " ▼"]; - const fillValue = "!X!Y!Z!"; - - if (desc && tableArrows) { - th.insertAdjacentText("beforeend", arrowDown); - } else if (tableArrows) { - th.insertAdjacentText("beforeend", arrowUp); + function sortDataAttributes(tableRows, column) { + for (let [i, tr] of tableRows.entries()) { + let dataAttributeTd = column.getColumn(tr, column.spanSum, column.span) + .dataset.sort; + column.toBeSorted.push(`${dataAttributeTd}#${i}`); + columnIndexAndTableRow[column.toBeSorted[i]] = tr.outerHTML; } + } - function sortDataAttributes(tableRows, column) { - for (let [i, tr] of tableRows.entries()) { - let dataAttributeTd = getColumn(tr, column.spanSum, column.span).dataset - .sort; - column.toBeSorted.push(`${dataAttributeTd}#${i}`); + function sortFileSize(tableRows, column, columnIndex) { + let unitToMultiplier = { + b: 1, + kb: 1000, + kib: 2 ** 10, + mb: 1e6, + mib: 2 ** 20, + gb: 1e9, + gib: 2 ** 30, + tb: 1e12, + tib: 2 ** 40, + }; + const numberWithUnitType = /([.0-9]+)\s?(B|KB|KiB|MB|MiB|GB|GiB|TB|TiB)/i; + for (let [i, tr] of tableRows.entries()) { + let fileSizeTd = tr.querySelectorAll("td").item(columnIndex).textContent; + let match = fileSizeTd.match(numberWithUnitType); + if (match) { + let number = parseFloat(match[1]); + let unit = match[2].toLowerCase(); + let multiplier = unitToMultiplier[unit]; + column.toBeSorted.push(`${number * multiplier}#${i}`); columnIndexAndTableRow[column.toBeSorted[i]] = tr.outerHTML; } } + } - function sortFileSize(tableRows, column) { - let unitToMultiplier = { - b: 1, - kb: 1000, - kib: 2 ** 10, - mb: 1e6, - mib: 2 ** 20, - gb: 1e9, - gib: 2 ** 30, - tb: 1e12, - tib: 2 ** 40, - }; - const numberWithUnitType = /([.0-9]+)\s?(B|KB|KiB|MB|MiB|GB|GiB|TB|TiB)/i; + function sortDates(datesFormat, tableRows, column) { + try { for (let [i, tr] of tableRows.entries()) { - let fileSizeTd = tr - .querySelectorAll("td") - .item(columnIndex).textContent; - let match = fileSizeTd.match(numberWithUnitType); - if (match) { - let number = parseFloat(match[1]); - let unit = match[2].toLowerCase(); - let multiplier = unitToMultiplier[unit]; - column.toBeSorted.push(`${number * multiplier}#${i}`); - } else { - column.toBeSorted.push(`${fillValue}#${i}`); + let columnOfTd, datesRegex; + if (datesFormat === "mdy" || datesFormat === "dmy") { + datesRegex = /^(\d\d?)[./-](\d\d?)[./-]((\d\d)?\d\d)/; + } else if (datesFormat === "ymd") { + datesRegex = /^(\d\d\d\d)[./-](\d\d?)[./-](\d\d?)/; } - } - } - - function sortByRuntime(tableRows, column) { - try { - for (let [i, tr] of tableRows.entries()) { - const regexMinutesAndSeconds = /^(\d+h)?\s?(\d+m)?\s?(\d+s)?$/i; - let columnOfTd = ""; - // TODO: github actions runtime didn't like textContent, tests didn't like innerText? - if (testingTableSortJS) { - columnOfTd = getColumn(tr, column.spanSum, column.span).textContent; - } else { - columnOfTd = getColumn(tr, column.spanSum, column.span).innerText; - } - let match = columnOfTd.match(regexMinutesAndSeconds); - let [minutesInSeconds, hours, seconds] = [0, 0, 0]; - let timeinSeconds = columnOfTd; - if (match) { - const regexHours = match[1]; - if (regexHours) { - hours = Number(regexHours.replace("h", "")) * 60 * 60; - } - const regexMinutes = match[2]; - if (regexMinutes) { - minutesInSeconds = Number(regexMinutes.replace("m", "")) * 60; - } - const regexSeconds = match[3]; - if (regexSeconds) { - seconds = Number(regexSeconds.replace("s", "")); + columnOfTd = column.getColumn( + tr, + column.spanSum, + column.span + ).textContent; + let match = columnOfTd.match(datesRegex); + let [years, days, months] = [0, 0, 0]; + let numberToSort = columnOfTd; + if (match) { + const [regPos1, regPos2, regPos3] = [match[1], match[2], match[3]]; + if (regPos1 && regPos2 && regPos3) { + if (datesFormat === "mdy") { + [months, days, years] = [regPos1, regPos2, regPos3]; + } else if (datesFormat === "ymd") { + [years, months, days] = [regPos1, regPos2, regPos3]; + } else { + [days, months, years] = [regPos1, regPos2, regPos3]; } - timeinSeconds = hours + minutesInSeconds + seconds; } - column.toBeSorted.push(`${timeinSeconds}#${i}`); - columnIndexAndTableRow[column.toBeSorted[i]] = tr.outerHTML; + numberToSort = Number( + years + + String(months).padStart(2, "0") + + String(days).padStart(2, "0") + ); } - } catch (e) { - console.log(e); + column.toBeSorted.push(`${numberToSort}#${i}`); + columnIndexAndTableRow[column.toBeSorted[i]] = tr.outerHTML; } + } catch (e) { + console.log(e); } + } - function sortDates(datesFormat, tableRows, column) { - try { - for (let [i, tr] of tableRows.entries()) { - let columnOfTd, datesRegex; - if (datesFormat === "mdy" || datesFormat === "dmy") { - datesRegex = /^(\d\d?)[./-](\d\d?)[./-]((\d\d)?\d\d)/; - } else if (datesFormat === "ymd") { - datesRegex = /^(\d\d\d\d)[./-](\d\d?)[./-](\d\d?)/; + function sortByRuntime(tableRows, column) { + try { + for (let [i, tr] of tableRows.entries()) { + const regexMinutesAndSeconds = /^(\d+h)?\s?(\d+m)?\s?(\d+s)?$/i; + let columnOfTd = ""; + // TODO: github actions runtime didn't like textContent, tests didn't like innerText? + if (testingTableSortJS) { + columnOfTd = column.getColumn( + tr, + column.spanSum, + column.span + ).textContent; + } else { + columnOfTd = column.getColumn( + tr, + column.spanSum, + column.span + ).innerText; + } + let match = columnOfTd.match(regexMinutesAndSeconds); + let [minutesInSeconds, hours, seconds] = [0, 0, 0]; + let timeinSeconds = columnOfTd; + if (match) { + const regexHours = match[1]; + if (regexHours) { + hours = Number(regexHours.replace("h", "")) * 60 * 60; } - columnOfTd = getColumn(tr, column.spanSum, column.span).textContent; - let match = columnOfTd.match(datesRegex); - let [years, days, months] = [0, 0, 0]; - let numberToSort = columnOfTd; - if (match) { - const [regPos1, regPos2, regPos3] = [match[1], match[2], match[3]]; - if (regPos1 && regPos2 && regPos3) { - if (datesFormat === "mdy") { - [months, days, years] = [regPos1, regPos2, regPos3]; - } else if (datesFormat === "ymd") { - [years, months, days] = [regPos1, regPos2, regPos3]; - } else { - [days, months, years] = [regPos1, regPos2, regPos3]; - } - } - numberToSort = Number( - years + - String(months).padStart(2, "0") + - String(days).padStart(2, "0") - ); + const regexMinutes = match[2]; + if (regexMinutes) { + minutesInSeconds = Number(regexMinutes.replace("m", "")) * 60; + } + const regexSeconds = match[3]; + if (regexSeconds) { + seconds = Number(regexSeconds.replace("s", "")); } - column.toBeSorted.push(`${numberToSort}#${i}`); - columnIndexAndTableRow[column.toBeSorted[i]] = tr.outerHTML; + timeinSeconds = hours + minutesInSeconds + seconds; } - } catch (e) { - console.log(e); + column.toBeSorted.push(`${timeinSeconds}#${i}`); + columnIndexAndTableRow[column.toBeSorted[i]] = tr.outerHTML; } + } catch (e) { + console.log(e); } + } - function rememberSort() { - // if user clicked different column from first column reset times clicked. - columnIndexesClicked.push(columnIndex); - if (timesClickedColumn === 1 && columnIndexesClicked.length > 1) { - const lastColumnClicked = - columnIndexesClicked[columnIndexesClicked.length - 1]; - const secondLastColumnClicked = - columnIndexesClicked[columnIndexesClicked.length - 2]; - if (lastColumnClicked !== secondLastColumnClicked) { - columnIndexesClicked.shift(); - timesClickedColumn = 0; + function getTableData(tableProperties, timesClickedColumn) { + const { + tableRows, + fillValue, + column, + th, + hasThClass, + isSortDates, + desc, + arrow, + tableArrows, + } = tableProperties; + for (let [i, tr] of tableRows.entries()) { + let tdTextContent = column.getColumn( + tr, + column.spanSum, + column.span + ).textContent; + if (tdTextContent.length === 0) { + tdTextContent = ""; + } + if (tdTextContent.trim() !== "") { + if ( + !hasThClass.fileSize && + !hasThClass.dataSort && + !hasThClass.runtime && + !hasThClass.filesize && + !isSortDates.dayMonthYear && + !isSortDates.yearMonthDay && + !isSortDates.monthDayYear + ) { + column.toBeSorted.push(`${tdTextContent}#${i}`); + columnIndexAndTableRow[`${tdTextContent}#${i}`] = tr.outerHTML; } + } else { + // Fill in blank table cells dict key with filler value. + column.toBeSorted.push(`${fillValue}#${i}`); + columnIndexAndTableRow[`${fillValue}#${i}`] = tr.outerHTML; } - return timesClickedColumn; } - function getColSpanData(headers, column) { - headers.forEach((th, index) => { - column.span[index] = th.colSpan; - if (index === 0) column.spanSum[index] = th.colSpan; - else column.spanSum[index] = column.spanSum[index - 1] + th.colSpan; - }); - } + const isPunctSort = th.classList.contains("punct-sort"); + const isAlphaSort = th.classList.contains("alpha-sort"); + const isNumericSort = th.classList.contains("numeric-sort"); - function getColumn(tr, colSpanSum, colSpanData) { - return tr - .querySelectorAll("td") - .item( - colSpanData[columnIndex] === 1 - ? colSpanSum[columnIndex] - 1 - : colSpanSum[columnIndex] - colSpanData[columnIndex] - ); + function parseNumberFromString(str) { + let num; + str = str.slice(0, str.indexOf("#")); + if (str.match(/^\((\d+(?:\.\d+)?)\)$/)) { + num = -1 * Number(str.slice(1, -1)); + } else { + num = Number(str); + } + return num; } - function getTableData(tableProperties) { - const { tableRows, column, hasThClass, isSortDates } = tableProperties; - for (let [i, tr] of tableRows.entries()) { - let tdTextContent = getColumn( - tr, - column.spanSum, - column.span - ).textContent; - if (tdTextContent.length === 0) { - tdTextContent = ""; - } - if (tdTextContent.trim() !== "") { - if (hasThClass.fileSize) { - fileSizeColumnTextAndRow[column.toBeSorted[i]] = tr.outerHTML; - } - // These classes already handle pushing to column and setting the tr html. - if ( - !hasThClass.fileSize && - !hasThClass.dataSort && - !hasThClass.runtime && - !isSortDates.dayMonthYear && - !isSortDates.yearMonthDay && - !isSortDates.monthDayYear - ) { - column.toBeSorted.push(`${tdTextContent}#${i}`); - columnIndexAndTableRow[`${tdTextContent}#${i}`] = tr.outerHTML; - } - } else { - // Fill in blank table cells dict key with filler value. - column.toBeSorted.push(`${fillValue}#${i}`); - columnIndexAndTableRow[`${fillValue}#${i}`] = tr.outerHTML; - } - } + function strLocaleCompare(str1, str2) { + return str1.localeCompare( + str2, + navigator.languages[0] || navigator.language, + { numeric: !isAlphaSort, ignorePunctuation: !isPunctSort } + ); + } - const isPunctSort = th.classList.contains("punct-sort"); - const isAlphaSort = th.classList.contains("alpha-sort"); - const isNumericSort = th.classList.contains("numeric-sort"); + function handleNumbers(str1, str2) { + let num1, num2; + num1 = parseNumberFromString(str1); + num2 = parseNumberFromString(str2); - function parseNumberFromString(str) { - let num; - str = str.slice(0, str.indexOf("#")); - if (str.match(/^\((\d+(?:\.\d+)?)\)$/)) { - num = -1 * Number(str.slice(1, -1)); - } else { - num = Number(str); - } - return num; + if (!isNaN(num1) && !isNaN(num2)) { + return num1 - num2; + } else { + return strLocaleCompare(str1, str2); } + } - function strLocaleCompare(str1, str2) { - return str1.localeCompare( - str2, - navigator.languages[0] || navigator.language, - { numeric: !isAlphaSort, ignorePunctuation: !isPunctSort } - ); + function sortAscending(a, b) { + if (a.includes(`${fillValue}#`)) { + return 1; + } else if (b.includes(`${fillValue}#`)) { + return -1; + } else if (isNumericSort) { + return handleNumbers(a, b); + } else { + return strLocaleCompare(a, b); } + } - function handleNumbers(str1, str2) { - let num1, num2; - num1 = parseNumberFromString(str1); - num2 = parseNumberFromString(str2); + function sortDescending(a, b) { + return sortAscending(b, a); + } - if (!isNaN(num1) && !isNaN(num2)) { - return num1 - num2; - } else { - return strLocaleCompare(str1, str2); - } - } + function clearArrows(arrowUp = "▲", arrowDown = "▼") { + th.innerHTML = th.innerHTML.replace(arrowUp, ""); + th.innerHTML = th.innerHTML.replace(arrowDown, ""); + } - function sortAscending(a, b) { - if (a.includes(`${fillValue}#`)) { - return 1; - } else if (b.includes(`${fillValue}#`)) { - return -1; - } else if (isNumericSort) { - return handleNumbers(a, b); - } else { - return strLocaleCompare(a, b); - } - } + if (column.toBeSorted[0] === undefined) { + return; + } - function sortDescending(a, b) { - return sortAscending(b, a); + function changeTableArrow(arrowDirection) { + if (tableArrows) { + clearArrows(arrow.up, arrow.down); + th.insertAdjacentText("beforeend", arrowDirection); } + } - function clearArrows(arrowUp = "▲", arrowDown = "▼") { - th.innerHTML = th.innerHTML.replace(arrowUp, ""); - th.innerHTML = th.innerHTML.replace(arrowDown, ""); - } + function sortColumn(sortDirection) { + column.toBeSorted.sort(sortDirection, { + numeric: !isAlphaSort, + ignorePunctuation: !isPunctSort, + }); + } - if (column.toBeSorted[0] === undefined) { - return; + if (timesClickedColumn === 1) { + if (desc) { + changeTableArrow(arrow.down); + sortColumn(sortDescending); + } else { + changeTableArrow(arrow.up); + sortColumn(sortAscending); } + } else if (timesClickedColumn === 2) { + timesClickedColumn = 0; + if (desc) { + changeTableArrow(arrow.up); + sortColumn(sortAscending); + } else { + changeTableArrow(arrow.down); + sortColumn(sortDescending); + } + } + return timesClickedColumn; + } - function changeTableArrow(arrowDirection) { - if (tableArrows) { - clearArrows(arrowUp, arrowDown); - th.insertAdjacentText("beforeend", arrowDirection); - } + function updateFilesize(i, tr, column, columnIndex) { + // We do this to sort rows rather than cells: + const template = document.createElement("template"); + template.innerHTML = columnIndexAndTableRow[column.toBeSorted[i]]; + tr = template.content.firstChild; + let fileSizeInBytesHTML = column.getColumn( + tr, + column.spanSum, + column.span + ).outerHTML; + const fileSizeInBytesText = column.getColumn( + tr, + column.spanSum, + column.span + ).textContent; + + const fileSize = column.toBeSorted[i].replace(/#[0-9]*/, ""); + let prefixes = ["", "Ki", "Mi", "Gi", "Ti", "Pi"]; + let replaced = false; + for (let i = 0; i < prefixes.length; ++i) { + let nextPrefixMultiplier = 2 ** (10 * (i + 1)); + if (fileSize < nextPrefixMultiplier) { + let prefixMultiplier = 2 ** (10 * i); + fileSizeInBytesHTML = fileSizeInBytesHTML.replace( + fileSizeInBytesText, + `${(fileSize / prefixMultiplier).toFixed(2)} ${prefixes[i]}B` + ); + replaced = true; + break; } + } + if (!replaced) { + fileSizeInBytesHTML = fileSizeInBytesHTML.replace( + fileSizeInBytesText, + "NaN" + ); + } + tr.querySelectorAll("td").item(columnIndex).innerHTML = fileSizeInBytesHTML; + return tr.outerHTML; + } - function sortColumn(sortDirection) { - column.toBeSorted.sort(sortDirection, { - numeric: !isAlphaSort, - ignorePunctuation: !isPunctSort, - }); + function updateTable(tableProperties) { + const { tableRows, column, columnIndex, hasThClass } = tableProperties; + for (let [i, tr] of tableRows.entries()) { + if (hasThClass.fileSize) { + tr.outerHTML = updateFilesize(i, tr, column, columnIndex); + // console.log(9, tr.outerHTML); + } else if (!hasThClass.fileSize) { + tr.outerHTML = columnIndexAndTableRow[column.toBeSorted[i]]; } + } + } - if (timesClickedColumn === 1) { - if (desc) { - changeTableArrow(arrowDown); - sortColumn(sortDescending); - } else { - changeTableArrow(arrowUp); - sortColumn(sortAscending); - } - } else if (timesClickedColumn === 2) { + function getColSpanData(headers, column) { + headers.forEach((th, index) => { + column.span[index] = th.colSpan; + if (index === 0) column.spanSum[index] = th.colSpan; + else column.spanSum[index] = column.spanSum[index - 1] + th.colSpan; + }); + } + + function rememberSort(columnIndexesClicked, timesClickedColumn, columnIndex) { + // if user clicked different column from first column reset times clicked. + columnIndexesClicked.push(columnIndex); + if (timesClickedColumn === 1 && columnIndexesClicked.length > 1) { + const lastColumnClicked = + columnIndexesClicked[columnIndexesClicked.length - 1]; + const secondLastColumnClicked = + columnIndexesClicked[columnIndexesClicked.length - 2]; + if (lastColumnClicked !== secondLastColumnClicked) { + columnIndexesClicked.shift(); timesClickedColumn = 0; - if (desc) { - changeTableArrow(arrowUp); - sortColumn(sortAscending); - } else { - changeTableArrow(arrowDown); - sortColumn(sortDescending); - } } } + return timesClickedColumn; + } - function updateTable(tableProperties) { - const { tableRows, column, hasThClass } = tableProperties; - for (let [i, tr] of tableRows.entries()) { - if (hasThClass.fileSize) { - tr.innerHTML = fileSizeColumnTextAndRow[column.toBeSorted[i]]; - let fileSizeInBytesHTML = tr - .querySelectorAll("td") - .item(columnIndex).innerHTML; - const fileSizeInBytesText = tr - .querySelectorAll("td") - .item(columnIndex).textContent; - // Remove the unique identifyer for duplicate values(#number). - column.toBeSorted[i] = column.toBeSorted[i].replace(/#[0-9]*/, ""); - const fileSize = parseFloat(column.toBeSorted[i]); - let prefixes = ["", "Ki", "Mi", "Gi", "Ti", "Pi"]; - let replaced = false; - for (let i = 0; i < prefixes.length; ++i) { - let nextPrefixMultiplier = 2 ** (10 * (i + 1)); - if (fileSize < nextPrefixMultiplier) { - let prefixMultiplier = 2 ** (10 * i); - fileSizeInBytesHTML = fileSizeInBytesHTML.replace( - fileSizeInBytesText, - `${(fileSize / prefixMultiplier).toFixed(2)} ${prefixes[i]}B` - ); - replaced = true; - break; - } - } - if (!replaced) { - fileSizeInBytesHTML = fileSizeInBytesHTML.replace( - fileSizeInBytesText, - "NaN" - ); - } - tr.querySelectorAll("td").item(columnIndex).innerHTML = - fileSizeInBytesHTML; - } else if (!hasThClass.fileSize) { - tr.outerHTML = columnIndexAndTableRow[column.toBeSorted[i]]; - } - } + function makeEachColumnSortable( + th, + columnIndex, + table, + sortableTable, + columnIndexesClicked + ) { + const desc = th.classList.contains("order-by-desc"); + const tableArrows = sortableTable.classList.contains("table-arrows"); + const arrow = { up: " ▲", down: " ▼" }; + const fillValue = "!X!Y!Z!"; + + if (desc && tableArrows) { + th.insertAdjacentText("beforeend", arrow.down); + } else if (tableArrows) { + th.insertAdjacentText("beforeend", arrow.up); } let timesClickedColumn = 0; + const column = { + getColumn: function getColumn(tr, colSpanSum, colSpanData) { + return tr + .querySelectorAll("td") + .item( + colSpanData[columnIndex] === 1 + ? colSpanSum[columnIndex] - 1 + : colSpanSum[columnIndex] - colSpanData[columnIndex] + ); + }, + }; th.addEventListener("click", function () { - const column = { - toBeSorted: [], - span: {}, - spanSum: {}, - }; + column.toBeSorted = []; + column.span = {}; + column.spanSum = {}; + getColSpanData(table.headers, column); table.visibleRows = Array.prototype.filter.call( table.body.querySelectorAll("tr"), @@ -486,11 +513,13 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { } ); - getColSpanData(table.headers, column); - const isRememberSort = sortableTable.classList.contains("remember-sort"); if (!isRememberSort) { - timesClickedColumn = rememberSort(); + timesClickedColumn = rememberSort( + columnIndexesClicked, + timesClickedColumn, + columnIndex + ); } timesClickedColumn += 1; @@ -504,7 +533,7 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { sortDataAttributes(table.visibleRows, column); } if (hasThClass.fileSize) { - sortFileSize(table.visibleRows, column); + sortFileSize(table.visibleRows, column, columnIndex, fillValue); } if (hasThClass.runtime) { sortByRuntime(table.visibleRows, column); @@ -526,11 +555,18 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { const tableProperties = { tableRows: table.visibleRows, + fillValue, column, + columnIndex, + th, hasThClass, isSortDates, + desc, + timesClickedColumn, + arrow, + tableArrows, }; - getTableData(tableProperties); + timesClickedColumn = getTableData(tableProperties, timesClickedColumn); updateTable(tableProperties); });