diff --git a/README.md b/README.md index 10648b7..37ca0b9 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,6 @@ Or Minified (smaller size, but harder to debug!): Example on how to use table-sort-js with [HTML](https://leewannacott.github.io/table-sort-js/docs/html5.html) - - Option 2: Install from npm: ```javascript diff --git a/browser-extensions/chrome/manifest.json b/browser-extensions/chrome/manifest.json index aa5aedc..251c72e 100644 --- a/browser-extensions/chrome/manifest.json +++ b/browser-extensions/chrome/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "author": "Lee Wannacott", "name": "table-sort-js", - "version": "1.16.0", + "version": "1.17.0", "description": "Makes tables sortable using table-sort-js: https://github.com/LeeWannacott/table-sort-js", "icons": { "48": "icons/t.png" }, "browser_action": { diff --git a/browser-extensions/chrome/table-sort-js.zip b/browser-extensions/chrome/table-sort-js.zip index ed4be8a..43adff3 100644 Binary files a/browser-extensions/chrome/table-sort-js.zip and b/browser-extensions/chrome/table-sort-js.zip differ diff --git a/browser-extensions/chrome/table-sort.js b/browser-extensions/chrome/table-sort.js index 4b19ea0..a9a7e31 100644 --- a/browser-extensions/chrome/table-sort.js +++ b/browser-extensions/chrome/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); @@ -46,16 +45,18 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { sortableTable.insertBefore(createTableHead, sortableTable.firstChild); } - function getTableBody(sortableTable) { + function getTableBodies(sortableTable) { if (sortableTable.getElementsByTagName("thead").length === 0) { createMissingTableHead(sortableTable); if (sortableTable.querySelectorAll("tbody").length > 1) { + // Why index 1?; I don't remember return sortableTable.querySelectorAll("tbody")[1]; } else { - return sortableTable.querySelector("tbody"); + return sortableTable.querySelectorAll("tbody"); } } else { - return sortableTable.querySelector("tbody"); + // if or exists below the browser will make + return sortableTable.querySelectorAll("tbody"); } } @@ -108,389 +109,455 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { function makeTableSortable(sortableTable) { const table = { - body: getTableBody(sortableTable), - head: sortableTable.querySelector("thead"), + bodies: getTableBodies(sortableTable), + theads: sortableTable.querySelectorAll("thead"), + rows: [], + headers: [], }; - table.headers = table.head.querySelectorAll("th"); - table.rows = table.body.querySelectorAll("tr"); - - let columnIndexesClicked = []; + for (let index of table.bodies.keys()) { + if (table.bodies.item(index) == null) { + return; + } + table.headers.push(table.theads.item(index).querySelectorAll("th")); + table.rows.push(table.bodies.item(index).querySelectorAll("tr")); + } - const isNoSortClassInference = - sortableTable.classList.contains("no-class-infer"); + table.hasClass = { + noClassInfer: sortableTable.classList.contains("no-class-infer"), + cellsSort: sortableTable.classList.contains("cells-sort"), + tableArrows: sortableTable.classList.contains("table-arrows"), + rememberSort: sortableTable.classList.contains("remember-sort"), + }; - for (let [columnIndex, th] of table.headers.entries()) { - if (!th.classList.contains("disable-sort")) { - th.style.cursor = "pointer"; - if (!isNoSortClassInference) { - inferSortClasses(table.rows, columnIndex, th); + for ( + let headerIndex = 0; + headerIndex < table.theads.length; + headerIndex++ + ) { + let columnIndexesClicked = []; + for (let [columnIndex, th] of table.headers[headerIndex].entries()) { + if (!th.classList.contains("disable-sort")) { + th.style.cursor = "pointer"; + if (!table.hasClass.noClassInfer) { + inferSortClasses(table.rows[headerIndex], columnIndex, th); + } + makeEachColumnSortable( + th, + headerIndex, + columnIndex, + table, + columnIndexesClicked + ); } - makeEachColumnSortable( - th, - columnIndex, - table, - sortableTable, - columnIndexesClicked - ); } } } - 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 cellsOrRows(table, tr) { + if (table.hasClass.cellsSort) { + return tr.innerHTML; + } else { + return 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}`); - columnIndexAndTableRow[column.toBeSorted[i]] = tr.outerHTML; - } + function sortDataAttributes(table, column) { + for (let [i, tr] of table.visibleRows.entries()) { + let dataAttributeTd = column.getColumn(tr, column.spanSum, column.span) + .dataset.sort; + column.toBeSorted.push(`${dataAttributeTd}#${i}`); + columnIndexAndTableRow[column.toBeSorted[i]] = cellsOrRows(table, tr); } + } - 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; - 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}`); - } + function sortFileSize(table, 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 table.visibleRows.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]] = cellsOrRows(table, tr); } } + } - 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", "")); + function sortDates(datesFormat, table, column) { + try { + for (let [i, tr] of table.visibleRows.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?)/; + } + 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]] = cellsOrRows(table, tr); } + } 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(table, column) { + try { + for (let [i, tr] of table.visibleRows.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]] = cellsOrRows(table, tr); } + } 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 { + table, + tableRows, + fillValue, + column, + th, + hasThClass, + isSortDates, + desc, + arrow, + } = 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}`] = cellsOrRows( + table, + tr + ); } + } else { + // Fill in blank table cells dict key with filler value. + column.toBeSorted.push(`${fillValue}#${i}`); + columnIndexAndTableRow[`${fillValue}#${i}`] = cellsOrRows(table, tr); } - 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 (table.hasClass.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); } - - function changeTableArrow(arrowDirection) { - if (tableArrows) { - clearArrows(arrowUp, arrowDown); - th.insertAdjacentText("beforeend", arrowDirection); - } + } else if (timesClickedColumn === 2) { + timesClickedColumn = 0; + if (desc) { + changeTableArrow(arrow.up); + sortColumn(sortAscending); + } else { + changeTableArrow(arrow.down); + sortColumn(sortDescending); } + } + return timesClickedColumn; + } - function sortColumn(sortDirection) { - column.toBeSorted.sort(sortDirection, { - numeric: !isAlphaSort, - ignorePunctuation: !isPunctSort, - }); + function updateFilesize(i, table, tr, column, columnIndex) { + if (table.hasClass.cellsSort) { + tr.innerHTML = columnIndexAndTableRow[column.toBeSorted[i]]; + } else { + // 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 table.hasClass.cellsSort ? tr.innerHTML : tr.outerHTML; + } - if (timesClickedColumn === 1) { - if (desc) { - changeTableArrow(arrowDown); - sortColumn(sortDescending); + function updateTable(tableProperties) { + const { column, table, columnIndex, hasThClass } = tableProperties; + for (let [i, tr] of table.visibleRows.entries()) { + if (hasThClass.fileSize) { + if (table.hasClass.cellsSort) { + tr.innerHTML = updateFilesize(i, table, tr, column, columnIndex); } else { - changeTableArrow(arrowUp); - sortColumn(sortAscending); + tr.outerHTML = updateFilesize(i, table, tr, column, columnIndex); } - } else if (timesClickedColumn === 2) { - timesClickedColumn = 0; - if (desc) { - changeTableArrow(arrowUp); - sortColumn(sortAscending); + } else if (!hasThClass.fileSize) { + if (table.hasClass.cellsSort) { + tr.innerHTML = columnIndexAndTableRow[column.toBeSorted[i]]; } else { - changeTableArrow(arrowDown); - sortColumn(sortDescending); + tr.outerHTML = columnIndexAndTableRow[column.toBeSorted[i]]; } } } + } - 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 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; } } + return timesClickedColumn; + } + + function makeEachColumnSortable( + th, + headerIndex, + columnIndex, + table, + columnIndexesClicked + ) { + const desc = th.classList.contains("order-by-desc"); + const arrow = { up: " ▲", down: " ▼" }; + const fillValue = "!X!Y!Z!"; + + if (desc && table.hasClass.tableArrows) { + th.insertAdjacentText("beforeend", arrow.down); + } else if (table.hasClass.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[headerIndex], column); table.visibleRows = Array.prototype.filter.call( - table.body.querySelectorAll("tr"), + table.bodies.item(headerIndex).querySelectorAll("tr"), (tr) => { return tr.style.display !== "none"; } ); - getColSpanData(table.headers, column); - - const isRememberSort = sortableTable.classList.contains("remember-sort"); - if (!isRememberSort) { - timesClickedColumn = rememberSort(); + if (!table.hasClass.rememberSort) { + timesClickedColumn = rememberSort( + columnIndexesClicked, + timesClickedColumn, + columnIndex + ); } timesClickedColumn += 1; @@ -501,13 +568,13 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { }; if (hasThClass.dataSort) { - sortDataAttributes(table.visibleRows, column); + sortDataAttributes(table, column); } if (hasThClass.fileSize) { - sortFileSize(table.visibleRows, column); + sortFileSize(table, column, columnIndex, fillValue); } if (hasThClass.runtime) { - sortByRuntime(table.visibleRows, column); + sortByRuntime(table, column); } const isSortDates = { @@ -517,20 +584,27 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { }; // pick mdy first to override the inferred default class which is dmy. if (isSortDates.monthDayYear) { - sortDates("mdy", table.visibleRows, column); + sortDates("mdy", table, column); } else if (isSortDates.yearMonthDay) { - sortDates("ymd", table.visibleRows, column); + sortDates("ymd", table, column); } else if (isSortDates.dayMonthYear) { - sortDates("dmy", table.visibleRows, column); + sortDates("dmy", table, column); } const tableProperties = { + table, tableRows: table.visibleRows, + fillValue, column, + columnIndex, + th, hasThClass, isSortDates, + desc, + timesClickedColumn, + arrow, }; - getTableData(tableProperties); + timesClickedColumn = getTableData(tableProperties, timesClickedColumn); updateTable(tableProperties); }); diff --git a/browser-extensions/firefox/manifest.json b/browser-extensions/firefox/manifest.json index 5dbff19..5efa850 100644 --- a/browser-extensions/firefox/manifest.json +++ b/browser-extensions/firefox/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "author": "Lee Wannacott", "name": "table-sort-js", - "version": "1.16.0", + "version": "1.17.0", "description": "Makes tables sortable using table-sort-js: https://github.com/LeeWannacott/table-sort-js", "icons": { "48": "icons/t.png" }, "browser_action": { diff --git a/browser-extensions/firefox/table-sort-js.zip b/browser-extensions/firefox/table-sort-js.zip index fc637c6..91c195d 100644 Binary files a/browser-extensions/firefox/table-sort-js.zip and b/browser-extensions/firefox/table-sort-js.zip differ diff --git a/browser-extensions/firefox/table-sort.js b/browser-extensions/firefox/table-sort.js index 4b19ea0..a9a7e31 100644 --- a/browser-extensions/firefox/table-sort.js +++ b/browser-extensions/firefox/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); @@ -46,16 +45,18 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { sortableTable.insertBefore(createTableHead, sortableTable.firstChild); } - function getTableBody(sortableTable) { + function getTableBodies(sortableTable) { if (sortableTable.getElementsByTagName("thead").length === 0) { createMissingTableHead(sortableTable); if (sortableTable.querySelectorAll("tbody").length > 1) { + // Why index 1?; I don't remember return sortableTable.querySelectorAll("tbody")[1]; } else { - return sortableTable.querySelector("tbody"); + return sortableTable.querySelectorAll("tbody"); } } else { - return sortableTable.querySelector("tbody"); + // if or exists below the browser will make + return sortableTable.querySelectorAll("tbody"); } } @@ -108,389 +109,455 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { function makeTableSortable(sortableTable) { const table = { - body: getTableBody(sortableTable), - head: sortableTable.querySelector("thead"), + bodies: getTableBodies(sortableTable), + theads: sortableTable.querySelectorAll("thead"), + rows: [], + headers: [], }; - table.headers = table.head.querySelectorAll("th"); - table.rows = table.body.querySelectorAll("tr"); - - let columnIndexesClicked = []; + for (let index of table.bodies.keys()) { + if (table.bodies.item(index) == null) { + return; + } + table.headers.push(table.theads.item(index).querySelectorAll("th")); + table.rows.push(table.bodies.item(index).querySelectorAll("tr")); + } - const isNoSortClassInference = - sortableTable.classList.contains("no-class-infer"); + table.hasClass = { + noClassInfer: sortableTable.classList.contains("no-class-infer"), + cellsSort: sortableTable.classList.contains("cells-sort"), + tableArrows: sortableTable.classList.contains("table-arrows"), + rememberSort: sortableTable.classList.contains("remember-sort"), + }; - for (let [columnIndex, th] of table.headers.entries()) { - if (!th.classList.contains("disable-sort")) { - th.style.cursor = "pointer"; - if (!isNoSortClassInference) { - inferSortClasses(table.rows, columnIndex, th); + for ( + let headerIndex = 0; + headerIndex < table.theads.length; + headerIndex++ + ) { + let columnIndexesClicked = []; + for (let [columnIndex, th] of table.headers[headerIndex].entries()) { + if (!th.classList.contains("disable-sort")) { + th.style.cursor = "pointer"; + if (!table.hasClass.noClassInfer) { + inferSortClasses(table.rows[headerIndex], columnIndex, th); + } + makeEachColumnSortable( + th, + headerIndex, + columnIndex, + table, + columnIndexesClicked + ); } - makeEachColumnSortable( - th, - columnIndex, - table, - sortableTable, - columnIndexesClicked - ); } } } - 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 cellsOrRows(table, tr) { + if (table.hasClass.cellsSort) { + return tr.innerHTML; + } else { + return 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}`); - columnIndexAndTableRow[column.toBeSorted[i]] = tr.outerHTML; - } + function sortDataAttributes(table, column) { + for (let [i, tr] of table.visibleRows.entries()) { + let dataAttributeTd = column.getColumn(tr, column.spanSum, column.span) + .dataset.sort; + column.toBeSorted.push(`${dataAttributeTd}#${i}`); + columnIndexAndTableRow[column.toBeSorted[i]] = cellsOrRows(table, tr); } + } - 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; - 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}`); - } + function sortFileSize(table, 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 table.visibleRows.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]] = cellsOrRows(table, tr); } } + } - 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", "")); + function sortDates(datesFormat, table, column) { + try { + for (let [i, tr] of table.visibleRows.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?)/; + } + 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]] = cellsOrRows(table, tr); } + } 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(table, column) { + try { + for (let [i, tr] of table.visibleRows.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]] = cellsOrRows(table, tr); } + } 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 { + table, + tableRows, + fillValue, + column, + th, + hasThClass, + isSortDates, + desc, + arrow, + } = 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}`] = cellsOrRows( + table, + tr + ); } + } else { + // Fill in blank table cells dict key with filler value. + column.toBeSorted.push(`${fillValue}#${i}`); + columnIndexAndTableRow[`${fillValue}#${i}`] = cellsOrRows(table, tr); } - 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 (table.hasClass.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); } - - function changeTableArrow(arrowDirection) { - if (tableArrows) { - clearArrows(arrowUp, arrowDown); - th.insertAdjacentText("beforeend", arrowDirection); - } + } else if (timesClickedColumn === 2) { + timesClickedColumn = 0; + if (desc) { + changeTableArrow(arrow.up); + sortColumn(sortAscending); + } else { + changeTableArrow(arrow.down); + sortColumn(sortDescending); } + } + return timesClickedColumn; + } - function sortColumn(sortDirection) { - column.toBeSorted.sort(sortDirection, { - numeric: !isAlphaSort, - ignorePunctuation: !isPunctSort, - }); + function updateFilesize(i, table, tr, column, columnIndex) { + if (table.hasClass.cellsSort) { + tr.innerHTML = columnIndexAndTableRow[column.toBeSorted[i]]; + } else { + // 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 table.hasClass.cellsSort ? tr.innerHTML : tr.outerHTML; + } - if (timesClickedColumn === 1) { - if (desc) { - changeTableArrow(arrowDown); - sortColumn(sortDescending); + function updateTable(tableProperties) { + const { column, table, columnIndex, hasThClass } = tableProperties; + for (let [i, tr] of table.visibleRows.entries()) { + if (hasThClass.fileSize) { + if (table.hasClass.cellsSort) { + tr.innerHTML = updateFilesize(i, table, tr, column, columnIndex); } else { - changeTableArrow(arrowUp); - sortColumn(sortAscending); + tr.outerHTML = updateFilesize(i, table, tr, column, columnIndex); } - } else if (timesClickedColumn === 2) { - timesClickedColumn = 0; - if (desc) { - changeTableArrow(arrowUp); - sortColumn(sortAscending); + } else if (!hasThClass.fileSize) { + if (table.hasClass.cellsSort) { + tr.innerHTML = columnIndexAndTableRow[column.toBeSorted[i]]; } else { - changeTableArrow(arrowDown); - sortColumn(sortDescending); + tr.outerHTML = columnIndexAndTableRow[column.toBeSorted[i]]; } } } + } - 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 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; } } + return timesClickedColumn; + } + + function makeEachColumnSortable( + th, + headerIndex, + columnIndex, + table, + columnIndexesClicked + ) { + const desc = th.classList.contains("order-by-desc"); + const arrow = { up: " ▲", down: " ▼" }; + const fillValue = "!X!Y!Z!"; + + if (desc && table.hasClass.tableArrows) { + th.insertAdjacentText("beforeend", arrow.down); + } else if (table.hasClass.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[headerIndex], column); table.visibleRows = Array.prototype.filter.call( - table.body.querySelectorAll("tr"), + table.bodies.item(headerIndex).querySelectorAll("tr"), (tr) => { return tr.style.display !== "none"; } ); - getColSpanData(table.headers, column); - - const isRememberSort = sortableTable.classList.contains("remember-sort"); - if (!isRememberSort) { - timesClickedColumn = rememberSort(); + if (!table.hasClass.rememberSort) { + timesClickedColumn = rememberSort( + columnIndexesClicked, + timesClickedColumn, + columnIndex + ); } timesClickedColumn += 1; @@ -501,13 +568,13 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { }; if (hasThClass.dataSort) { - sortDataAttributes(table.visibleRows, column); + sortDataAttributes(table, column); } if (hasThClass.fileSize) { - sortFileSize(table.visibleRows, column); + sortFileSize(table, column, columnIndex, fillValue); } if (hasThClass.runtime) { - sortByRuntime(table.visibleRows, column); + sortByRuntime(table, column); } const isSortDates = { @@ -517,20 +584,27 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { }; // pick mdy first to override the inferred default class which is dmy. if (isSortDates.monthDayYear) { - sortDates("mdy", table.visibleRows, column); + sortDates("mdy", table, column); } else if (isSortDates.yearMonthDay) { - sortDates("ymd", table.visibleRows, column); + sortDates("ymd", table, column); } else if (isSortDates.dayMonthYear) { - sortDates("dmy", table.visibleRows, column); + sortDates("dmy", table, column); } const tableProperties = { + table, tableRows: table.visibleRows, + fillValue, column, + columnIndex, + th, hasThClass, isSortDates, + desc, + timesClickedColumn, + arrow, }; - getTableData(tableProperties); + timesClickedColumn = getTableData(tableProperties, timesClickedColumn); updateTable(tableProperties); }); diff --git a/npm/README.md b/npm/README.md index 6e0d26f..37ca0b9 100644 --- a/npm/README.md +++ b/npm/README.md @@ -3,6 +3,7 @@ [![jsDeliver downloads](https://data.jsdelivr.com/v1/package/npm/table-sort-js/badge)](https://www.jsdelivr.com/package/npm/table-sort-js) ![repo size](https://img.shields.io/github/repo-size/leewannacott/table-sort-js) ![MIT licence](https://img.shields.io/github/license/LeeWannacott/table-sort-js) +[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) ![build status](https://img.shields.io/github/actions/workflow/status/leewannacott/table-sort-js/jest.yml?branch=master) # TABLE-SORT-JS. @@ -17,39 +18,31 @@ ## Install instructions. -- Option 1: Install from npm: +- Option 1: Load as script from a Content Delivery Network (CDN): ```javascript -npm install table-sort-js + ``` +Or Minified (smaller size, but harder to debug!): + ```javascript -import tableSort from "table-sort-js/table-sort.js"; + ``` -Examples on using table-sort-js with frontend frameworks such as [React.js](https://leewannacott.github.io/table-sort-js/docs/react.html) and [Vue.js](https://leewannacott.github.io/table-sort-js/docs/vue.html) +Example on how to use table-sort-js with [HTML](https://leewannacott.github.io/table-sort-js/docs/html5.html) -- Option 2: Load as script from a Content Delivery Network (CDN): +- Option 2: Install from npm: ```javascript - +npm install table-sort-js ``` -Or Minified (smaller size, but harder to debug!): - ```javascript - +import tableSort from "table-sort-js/table-sort.js"; ``` -Refer to the documenation for examples on how to use table-sort-js with [HTML](https://leewannacott.github.io/table-sort-js/docs/html5.html) - -- Option 3: Download [table-sort.js](https://cdn.jsdelivr.net/npm/table-sort-js@latest/table-sort.js) (Select save as.), or download a [minified version](https://cdn.jsdelivr.net/npm/table-sort-js@latest/table-sort.min.js) (~5kB) - -Then rename and add the following script before your HTML table: - -```html - -``` +Examples on using table-sort-js with frontend frameworks such as [React.js](https://leewannacott.github.io/table-sort-js/docs/react.html) and [Vue.js](https://leewannacott.github.io/table-sort-js/docs/vue.html) ## To make tables sortable: @@ -64,6 +57,7 @@ Then rename and add the following script before your HTML table: | "no-class-infer" | Turns off inference for adding sort classes automatically i.e (file-size-sort, runtime-sort, dates-dmy-sort). | | "table-arrows" | Display ascending or descending triangles. | | "remember-sort" | If clicking on different columns remembers sort of the original column. | +| "cells-sort" | sort cells (td) rather than table rows (tr); useful for keeping table rows with classes/attributes in place. |
diff --git a/npm/package.json b/npm/package.json index 4c8819d..4c9ce6b 100644 --- a/npm/package.json +++ b/npm/package.json @@ -1,6 +1,6 @@ { "name": "table-sort-js", - "version": "1.16.0", + "version": "1.17.0", "description": "A JavaScript client-side HTML table sorting library with no dependencies required.", "license": "MIT", "repository": "LeeWannacott/table-sort-js", diff --git a/npm/table-sort.js b/npm/table-sort.js index 4b19ea0..a9a7e31 100644 --- a/npm/table-sort.js +++ b/npm/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); @@ -46,16 +45,18 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { sortableTable.insertBefore(createTableHead, sortableTable.firstChild); } - function getTableBody(sortableTable) { + function getTableBodies(sortableTable) { if (sortableTable.getElementsByTagName("thead").length === 0) { createMissingTableHead(sortableTable); if (sortableTable.querySelectorAll("tbody").length > 1) { + // Why index 1?; I don't remember return sortableTable.querySelectorAll("tbody")[1]; } else { - return sortableTable.querySelector("tbody"); + return sortableTable.querySelectorAll("tbody"); } } else { - return sortableTable.querySelector("tbody"); + // if or exists below the browser will make + return sortableTable.querySelectorAll("tbody"); } } @@ -108,389 +109,455 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { function makeTableSortable(sortableTable) { const table = { - body: getTableBody(sortableTable), - head: sortableTable.querySelector("thead"), + bodies: getTableBodies(sortableTable), + theads: sortableTable.querySelectorAll("thead"), + rows: [], + headers: [], }; - table.headers = table.head.querySelectorAll("th"); - table.rows = table.body.querySelectorAll("tr"); - - let columnIndexesClicked = []; + for (let index of table.bodies.keys()) { + if (table.bodies.item(index) == null) { + return; + } + table.headers.push(table.theads.item(index).querySelectorAll("th")); + table.rows.push(table.bodies.item(index).querySelectorAll("tr")); + } - const isNoSortClassInference = - sortableTable.classList.contains("no-class-infer"); + table.hasClass = { + noClassInfer: sortableTable.classList.contains("no-class-infer"), + cellsSort: sortableTable.classList.contains("cells-sort"), + tableArrows: sortableTable.classList.contains("table-arrows"), + rememberSort: sortableTable.classList.contains("remember-sort"), + }; - for (let [columnIndex, th] of table.headers.entries()) { - if (!th.classList.contains("disable-sort")) { - th.style.cursor = "pointer"; - if (!isNoSortClassInference) { - inferSortClasses(table.rows, columnIndex, th); + for ( + let headerIndex = 0; + headerIndex < table.theads.length; + headerIndex++ + ) { + let columnIndexesClicked = []; + for (let [columnIndex, th] of table.headers[headerIndex].entries()) { + if (!th.classList.contains("disable-sort")) { + th.style.cursor = "pointer"; + if (!table.hasClass.noClassInfer) { + inferSortClasses(table.rows[headerIndex], columnIndex, th); + } + makeEachColumnSortable( + th, + headerIndex, + columnIndex, + table, + columnIndexesClicked + ); } - makeEachColumnSortable( - th, - columnIndex, - table, - sortableTable, - columnIndexesClicked - ); } } } - 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 cellsOrRows(table, tr) { + if (table.hasClass.cellsSort) { + return tr.innerHTML; + } else { + return 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}`); - columnIndexAndTableRow[column.toBeSorted[i]] = tr.outerHTML; - } + function sortDataAttributes(table, column) { + for (let [i, tr] of table.visibleRows.entries()) { + let dataAttributeTd = column.getColumn(tr, column.spanSum, column.span) + .dataset.sort; + column.toBeSorted.push(`${dataAttributeTd}#${i}`); + columnIndexAndTableRow[column.toBeSorted[i]] = cellsOrRows(table, tr); } + } - 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; - 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}`); - } + function sortFileSize(table, 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 table.visibleRows.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]] = cellsOrRows(table, tr); } } + } - 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", "")); + function sortDates(datesFormat, table, column) { + try { + for (let [i, tr] of table.visibleRows.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?)/; + } + 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]] = cellsOrRows(table, tr); } + } 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(table, column) { + try { + for (let [i, tr] of table.visibleRows.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]] = cellsOrRows(table, tr); } + } 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 { + table, + tableRows, + fillValue, + column, + th, + hasThClass, + isSortDates, + desc, + arrow, + } = 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}`] = cellsOrRows( + table, + tr + ); } + } else { + // Fill in blank table cells dict key with filler value. + column.toBeSorted.push(`${fillValue}#${i}`); + columnIndexAndTableRow[`${fillValue}#${i}`] = cellsOrRows(table, tr); } - 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 (table.hasClass.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); } - - function changeTableArrow(arrowDirection) { - if (tableArrows) { - clearArrows(arrowUp, arrowDown); - th.insertAdjacentText("beforeend", arrowDirection); - } + } else if (timesClickedColumn === 2) { + timesClickedColumn = 0; + if (desc) { + changeTableArrow(arrow.up); + sortColumn(sortAscending); + } else { + changeTableArrow(arrow.down); + sortColumn(sortDescending); } + } + return timesClickedColumn; + } - function sortColumn(sortDirection) { - column.toBeSorted.sort(sortDirection, { - numeric: !isAlphaSort, - ignorePunctuation: !isPunctSort, - }); + function updateFilesize(i, table, tr, column, columnIndex) { + if (table.hasClass.cellsSort) { + tr.innerHTML = columnIndexAndTableRow[column.toBeSorted[i]]; + } else { + // 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 table.hasClass.cellsSort ? tr.innerHTML : tr.outerHTML; + } - if (timesClickedColumn === 1) { - if (desc) { - changeTableArrow(arrowDown); - sortColumn(sortDescending); + function updateTable(tableProperties) { + const { column, table, columnIndex, hasThClass } = tableProperties; + for (let [i, tr] of table.visibleRows.entries()) { + if (hasThClass.fileSize) { + if (table.hasClass.cellsSort) { + tr.innerHTML = updateFilesize(i, table, tr, column, columnIndex); } else { - changeTableArrow(arrowUp); - sortColumn(sortAscending); + tr.outerHTML = updateFilesize(i, table, tr, column, columnIndex); } - } else if (timesClickedColumn === 2) { - timesClickedColumn = 0; - if (desc) { - changeTableArrow(arrowUp); - sortColumn(sortAscending); + } else if (!hasThClass.fileSize) { + if (table.hasClass.cellsSort) { + tr.innerHTML = columnIndexAndTableRow[column.toBeSorted[i]]; } else { - changeTableArrow(arrowDown); - sortColumn(sortDescending); + tr.outerHTML = columnIndexAndTableRow[column.toBeSorted[i]]; } } } + } - 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 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; } } + return timesClickedColumn; + } + + function makeEachColumnSortable( + th, + headerIndex, + columnIndex, + table, + columnIndexesClicked + ) { + const desc = th.classList.contains("order-by-desc"); + const arrow = { up: " ▲", down: " ▼" }; + const fillValue = "!X!Y!Z!"; + + if (desc && table.hasClass.tableArrows) { + th.insertAdjacentText("beforeend", arrow.down); + } else if (table.hasClass.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[headerIndex], column); table.visibleRows = Array.prototype.filter.call( - table.body.querySelectorAll("tr"), + table.bodies.item(headerIndex).querySelectorAll("tr"), (tr) => { return tr.style.display !== "none"; } ); - getColSpanData(table.headers, column); - - const isRememberSort = sortableTable.classList.contains("remember-sort"); - if (!isRememberSort) { - timesClickedColumn = rememberSort(); + if (!table.hasClass.rememberSort) { + timesClickedColumn = rememberSort( + columnIndexesClicked, + timesClickedColumn, + columnIndex + ); } timesClickedColumn += 1; @@ -501,13 +568,13 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { }; if (hasThClass.dataSort) { - sortDataAttributes(table.visibleRows, column); + sortDataAttributes(table, column); } if (hasThClass.fileSize) { - sortFileSize(table.visibleRows, column); + sortFileSize(table, column, columnIndex, fillValue); } if (hasThClass.runtime) { - sortByRuntime(table.visibleRows, column); + sortByRuntime(table, column); } const isSortDates = { @@ -517,20 +584,27 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { }; // pick mdy first to override the inferred default class which is dmy. if (isSortDates.monthDayYear) { - sortDates("mdy", table.visibleRows, column); + sortDates("mdy", table, column); } else if (isSortDates.yearMonthDay) { - sortDates("ymd", table.visibleRows, column); + sortDates("ymd", table, column); } else if (isSortDates.dayMonthYear) { - sortDates("dmy", table.visibleRows, column); + sortDates("dmy", table, column); } const tableProperties = { + table, tableRows: table.visibleRows, + fillValue, column, + columnIndex, + th, hasThClass, isSortDates, + desc, + timesClickedColumn, + arrow, }; - getTableData(tableProperties); + timesClickedColumn = getTableData(tableProperties, timesClickedColumn); updateTable(tableProperties); }); diff --git a/public/docs/about.html b/public/docs/about.html index d2f10e5..e089683 100644 --- a/public/docs/about.html +++ b/public/docs/about.html @@ -72,7 +72,8 @@

Objectives of table-sort-js:

  • Having no dependencies keeps the library size down and avoids the - left-pad fiasco. (for example jquery which datatables relies on is around 80KB). + left-pad fiasco. (for example jquery which datatables relies on is + around 80KB).
  • Be versatile; sorts dates, numbers (in a natural order), diff --git a/public/docs/demo.html b/public/docs/demo.html index 1f4c92f..d3e3b24 100644 --- a/public/docs/demo.html +++ b/public/docs/demo.html @@ -61,167 +61,172 @@

    table-sort-js

    -
    Demo tables showing off what table-sort-js can sort - and features.
    +
    + Demo tables showing off what table-sort-js can sort and features. +
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Last NameFirst NameBirth DateEmployee IDDepartmentRuntimeFile Sizedata-sort daysdates in dd/mm/yyyy
    FranklinBenjamin1706-1-171k-level1h 1m 17s10bTuesday17/6/1978
    da VinciZarlo1452-4-15130001m 45s192038998987021bWednesday18/10/2027
    StathamJason1967-7-26HR11m 40s134809bFriday4/9/2008
    MichealAngelo1958-8-2154Marketing29s30980980bThursday2/3/1879
    Ben1994/9/23134Marketing41s902938402398bMonday8/6/1978
    -
    -

    Demo of colspan and data-sort class to sort fractions:

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    CategoryShowOverallOn Our DatesFirst Sold Out
    ComedyShow 118/2572%3/475%1999-07-30
    MusicShow 26/1060%3/475%1999-08-04
    TheatreShow 37/1547%3/475%1999-07-19
    ComedyShow 410/1567%2/367%1999-07-19
    ComedyShow 59/1275%1/250%1999-07-29
    ComedyShow 616/2467%2/450%1999-07-26
    ComedyShow 716/2662%2/450%2022-07-31
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Last NameFirst NameBirth DateEmployee IDDepartmentRuntimeFile Sizedata-sort daysdates in dd/mm/yyyy
    FranklinBenjamin1706-1-171k-level1h 1m 17s10bTuesday17/6/1978
    da VinciZarlo1452-4-15130001m 45s192038998987021bWednesday18/10/2027
    StathamJason1967-7-26HR11m 40s134809bFriday4/9/2008
    MichealAngelo1958-8-2154Marketing29s30980980bThursday2/3/1879
    Ben1994/9/23134Marketing41s902938402398bMonday8/6/1978
    +
    +

    + Demo of colspan and data-sort class to sort fractions: +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    CategoryShowOverallOn Our DatesFirst Sold Out
    ComedyShow 118/2572%3/475%1999-07-30
    MusicShow 26/1060%3/475%1999-08-04
    TheatreShow 37/1547%3/475%1999-07-19
    ComedyShow 410/1567%2/367%1999-07-19
    ComedyShow 59/1275%1/250%1999-07-29
    ComedyShow 616/2467%2/450%1999-07-26
    ComedyShow 716/2662%2/450%2022-07-31
    diff --git a/public/docs/development.html b/public/docs/development.html index 2abf4bb..12a7c9a 100644 --- a/public/docs/development.html +++ b/public/docs/development.html @@ -102,7 +102,8 @@

    How to contribute to table-sort-js:

  • note: table-sort-js currently uses node version 16; I would - recommend using node version manager (nvm) and running the command nvm 16 to select node version 16. + recommend using node version manager (nvm) and running the command + nvm 16 to select node version 16.
  • Testing table-sort-js: npm run test (currently uses Jest diff --git a/public/docs/html5.html b/public/docs/html5.html index aab4210..da7031e 100644 --- a/public/docs/html5.html +++ b/public/docs/html5.html @@ -65,7 +65,7 @@

    table-sort-js

    Example of how to use table-sort-js with basic HTML.
    -
    
    +        
    
     <script src="https://cdn.jsdelivr.net/npm/table-sort-js/table-sort.js"></script>
     <table class="table-sort">
       <thead>
    diff --git a/public/table-sort.js b/public/table-sort.js
    index dbf75bd..a9a7e31 100644
    --- a/public/table-sort.js
    +++ b/public/table-sort.js
    @@ -129,9 +129,12 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) {
           rememberSort: sortableTable.classList.contains("remember-sort"),
         };
     
    -
    -    for ( let headerIndex = 0; headerIndex < table.theads.length; headerIndex++) {
    -    let columnIndexesClicked = [];
    +    for (
    +      let headerIndex = 0;
    +      headerIndex < table.theads.length;
    +      headerIndex++
    +    ) {
    +      let columnIndexesClicked = [];
           for (let [columnIndex, th] of table.headers[headerIndex].entries()) {
             if (!th.classList.contains("disable-sort")) {
               th.style.cursor = "pointer";
    diff --git a/test/table.js b/test/table.js
    index bd40711..0ce6eb3 100644
    --- a/test/table.js
    +++ b/test/table.js
    @@ -105,7 +105,7 @@ function createTestTable(
       for (let [i, tr] of tableRows.entries()) {
         if (tr.style.display !== "none") {
           for (let i = 0; i < numberOfTableColumns; i++)
    -        if (props.tableTags === "cells-sort" || props.tableTags ==="tr-sort") {
    +        if (props.tableTags === "cells-sort" || props.tableTags === "tr-sort") {
               testIfSortedList[`col${i}`].push(tr.outerHTML);
             } else {
               testIfSortedList[`col${i}`].push(