diff --git a/README.md b/README.md index 30f772b..6e0d26f 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,8 @@ ## Install instructions. -- Option 1: Install from npm: +- Option 1: Install from npm: + ```javascript npm install table-sort-js ``` @@ -25,16 +26,21 @@ npm install table-sort-js ```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) - Option 2: Load as script from a Content Delivery Network (CDN): + ```javascript ``` + Or Minified (smaller size, but harder to debug!): + ```javascript ``` + 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) @@ -72,7 +78,7 @@ Then rename and add the following script before your HTML table: | <th> Inferred Classes. | Description | | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | -| "numeric-sort" | Sorts numbers including decimals - Positive, Negative (in both minus and parenthesis representations) | +| "numeric-sort" | Sorts numbers including decimals - Positive, Negative (in both minus and parenthesis representations) | | "dates-dmy-sort" | Sorts dates in dd/mm/yyyy format. e.g (18/10/1995). Can use "/" or "-" as separator. | | "dates-ymd-sort" | Sorts dates in [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) yyyy/mm/dd format. e.g (2021/10/28). Use "/" or "-" as separator. | | "file-size-sort" | Sorts file sizes(B->TiB) uses the binary prefix. (e.g 10 B, 100 KiB, 1 MiB); optional space between number and prefix. | diff --git a/browser-extensions/chrome/table-sort-js.zip b/browser-extensions/chrome/table-sort-js.zip index adf5967..ed4be8a 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 385b443..4b19ea0 100644 --- a/browser-extensions/chrome/table-sort.js +++ b/browser-extensions/chrome/table-sort.js @@ -65,11 +65,13 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { // Doesn't infer dates with delimiter "."; as could capture semantic version numbers. const dmyRegex = /^(\d\d?)[/-](\d\d?)[/-]((\d\d)?\d\d)/; const ymdRegex = /^(\d\d\d\d)[/-](\d\d?)[/-](\d\d?)/; + const numericRegex = /^(?:\(\d+(?:\.\d+)?\)|-?\d+(?:\.\d+)?)$/; const inferableClasses = { runtime: { regexp: runtimeRegex, class: "runtime-sort", count: 0 }, filesize: { regexp: fileSizeRegex, class: "file-size-sort", count: 0 }, dmyDates: { regexp: dmyRegex, class: "dates-dmy-sort", count: 0 }, ymdDates: { regexp: ymdRegex, class: "dates-ymd-sort", count: 0 }, + numericRegex: { regexp: numericRegex, class: "numeric-sort", count: 0 }, }; let classNameAdded = false; let regexNotFoundCount = 0; @@ -105,28 +107,44 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { } function makeTableSortable(sortableTable) { - const tableBody = getTableBody(sortableTable); - const tableHead = sortableTable.querySelector("thead"); - const tableHeadHeaders = tableHead.querySelectorAll("th"); - const tableRows = tableBody.querySelectorAll("tr"); + const table = { + body: getTableBody(sortableTable), + head: sortableTable.querySelector("thead"), + }; + table.headers = table.head.querySelectorAll("th"); + table.rows = table.body.querySelectorAll("tr"); + + let columnIndexesClicked = []; const isNoSortClassInference = sortableTable.classList.contains("no-class-infer"); - for (let [columnIndex, th] of tableHeadHeaders.entries()) { + for (let [columnIndex, th] of table.headers.entries()) { if (!th.classList.contains("disable-sort")) { th.style.cursor = "pointer"; if (!isNoSortClassInference) { - inferSortClasses(tableRows, columnIndex, th); + inferSortClasses(table.rows, columnIndex, th); } - makeEachColumnSortable(th, columnIndex, tableBody, sortableTable); + makeEachColumnSortable( + th, + columnIndex, + table, + sortableTable, + columnIndexesClicked + ); } } } - function makeEachColumnSortable(th, columnIndex, tableBody, sortableTable) { + function makeEachColumnSortable( + th, + columnIndex, + table, + sortableTable, + columnIndexesClicked + ) { const desc = th.classList.contains("order-by-desc"); - let tableArrows = sortableTable.classList.contains("table-arrows"); + const tableArrows = sortableTable.classList.contains("table-arrows"); const [arrowUp, arrowDown] = [" ▲", " ▼"]; const fillValue = "!X!Y!Z!"; @@ -249,9 +267,7 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { } } - let [timesClickedColumn, columnIndexesClicked] = [0, []]; - - function rememberSort(timesClickedColumn, columnIndexesClicked) { + function rememberSort() { // if user clicked different column from first column reset times clicked. columnIndexesClicked.push(columnIndex); if (timesClickedColumn === 1 && columnIndexesClicked.length > 1) { @@ -260,14 +276,15 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { const secondLastColumnClicked = columnIndexesClicked[columnIndexesClicked.length - 2]; if (lastColumnClicked !== secondLastColumnClicked) { - timesClickedColumn = 0; columnIndexesClicked.shift(); + timesClickedColumn = 0; } } + return timesClickedColumn; } - function getColSpanData(sortableTable, column) { - sortableTable.querySelectorAll("th").forEach((th, index) => { + 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; @@ -285,16 +302,7 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { } function getTableData(tableProperties) { - const { - tableRows, - column, - isFileSize, - isTimeSort, - isSortDateDayMonthYear, - isSortDateMonthDayYear, - isSortDateYearMonthDay, - isDataAttribute, - } = tableProperties; + const { tableRows, column, hasThClass, isSortDates } = tableProperties; for (let [i, tr] of tableRows.entries()) { let tdTextContent = getColumn( tr, @@ -305,17 +313,17 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { tdTextContent = ""; } if (tdTextContent.trim() !== "") { - if (isFileSize) { + if (hasThClass.fileSize) { fileSizeColumnTextAndRow[column.toBeSorted[i]] = tr.outerHTML; } // These classes already handle pushing to column and setting the tr html. if ( - !isFileSize && - !isDataAttribute && - !isTimeSort && - !isSortDateDayMonthYear && - !isSortDateYearMonthDay && - !isSortDateMonthDayYear + !hasThClass.fileSize && + !hasThClass.dataSort && + !hasThClass.runtime && + !isSortDates.dayMonthYear && + !isSortDates.yearMonthDay && + !isSortDates.monthDayYear ) { column.toBeSorted.push(`${tdTextContent}#${i}`); columnIndexAndTableRow[`${tdTextContent}#${i}`] = tr.outerHTML; @@ -329,17 +337,48 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { const isPunctSort = th.classList.contains("punct-sort"); const isAlphaSort = th.classList.contains("alpha-sort"); + const isNumericSort = th.classList.contains("numeric-sort"); + + 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 strLocaleCompare(str1, str2) { + return str1.localeCompare( + str2, + navigator.languages[0] || navigator.language, + { numeric: !isAlphaSort, ignorePunctuation: !isPunctSort } + ); + } + + function handleNumbers(str1, str2) { + let num1, num2; + num1 = parseNumberFromString(str1); + num2 = parseNumberFromString(str2); + + if (!isNaN(num1) && !isNaN(num2)) { + return num1 - num2; + } else { + return strLocaleCompare(str1, str2); + } + } + 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 a.localeCompare( - b, - navigator.languages[0] || navigator.language, - { numeric: !isAlphaSort, ignorePunctuation: !isPunctSort } - ); + return strLocaleCompare(a, b); } } @@ -391,9 +430,9 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { } function updateTable(tableProperties) { - const { tableRows, column, isFileSize } = tableProperties; + const { tableRows, column, hasThClass } = tableProperties; for (let [i, tr] of tableRows.entries()) { - if (isFileSize) { + if (hasThClass.fileSize) { tr.innerHTML = fileSizeColumnTextAndRow[column.toBeSorted[i]]; let fileSizeInBytesHTML = tr .querySelectorAll("td") @@ -426,71 +465,70 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { } tr.querySelectorAll("td").item(columnIndex).innerHTML = fileSizeInBytesHTML; - } else if (!isFileSize) { + } else if (!hasThClass.fileSize) { tr.outerHTML = columnIndexAndTableRow[column.toBeSorted[i]]; } } } + let timesClickedColumn = 0; th.addEventListener("click", function () { - timesClickedColumn += 1; const column = { - // column used for sorting; better name? toBeSorted: [], span: {}, spanSum: {}, }; - const visibleTableRows = Array.prototype.filter.call( - tableBody.querySelectorAll("tr"), + table.visibleRows = Array.prototype.filter.call( + table.body.querySelectorAll("tr"), (tr) => { return tr.style.display !== "none"; } ); - getColSpanData(sortableTable, column); + getColSpanData(table.headers, column); - const isDataAttribute = th.classList.contains("data-sort"); - if (isDataAttribute) { - sortDataAttributes(visibleTableRows, column); + const isRememberSort = sortableTable.classList.contains("remember-sort"); + if (!isRememberSort) { + timesClickedColumn = rememberSort(); } + timesClickedColumn += 1; - const isFileSize = th.classList.contains("file-size-sort"); - if (isFileSize) { - sortFileSize(visibleTableRows, column); - } + const hasThClass = { + dataSort: th.classList.contains("data-sort"), + fileSize: th.classList.contains("file-size-sort"), + runtime: th.classList.contains("runtime-sort"), + }; - const isTimeSort = th.classList.contains("runtime-sort"); - if (isTimeSort) { - sortByRuntime(visibleTableRows, column); + if (hasThClass.dataSort) { + sortDataAttributes(table.visibleRows, column); } - - const isSortDateDayMonthYear = th.classList.contains("dates-dmy-sort"); - const isSortDateMonthDayYear = th.classList.contains("dates-mdy-sort"); - const isSortDateYearMonthDay = th.classList.contains("dates-ymd-sort"); - // pick mdy first to override the inferred default class which is dmy. - if (isSortDateMonthDayYear) { - sortDates("mdy", visibleTableRows, column); - } else if (isSortDateYearMonthDay) { - sortDates("ymd", visibleTableRows, column); - } else if (isSortDateDayMonthYear) { - sortDates("dmy", visibleTableRows, column); + if (hasThClass.fileSize) { + sortFileSize(table.visibleRows, column); + } + if (hasThClass.runtime) { + sortByRuntime(table.visibleRows, column); } - const isRememberSort = sortableTable.classList.contains("remember-sort"); - if (!isRememberSort) { - rememberSort(timesClickedColumn, columnIndexesClicked); + const isSortDates = { + dayMonthYear: th.classList.contains("dates-dmy-sort"), + monthDayYear: th.classList.contains("dates-mdy-sort"), + yearMonthDay: th.classList.contains("dates-ymd-sort"), + }; + // pick mdy first to override the inferred default class which is dmy. + if (isSortDates.monthDayYear) { + sortDates("mdy", table.visibleRows, column); + } else if (isSortDates.yearMonthDay) { + sortDates("ymd", table.visibleRows, column); + } else if (isSortDates.dayMonthYear) { + sortDates("dmy", table.visibleRows, column); } const tableProperties = { - tableRows: visibleTableRows, + tableRows: table.visibleRows, column, - isFileSize, - isSortDateDayMonthYear, - isSortDateMonthDayYear, - isSortDateYearMonthDay, - isDataAttribute, - isTimeSort, + hasThClass, + isSortDates, }; getTableData(tableProperties); updateTable(tableProperties); diff --git a/browser-extensions/firefox/table-sort-js.zip b/browser-extensions/firefox/table-sort-js.zip index 971b051..fc637c6 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 385b443..4b19ea0 100644 --- a/browser-extensions/firefox/table-sort.js +++ b/browser-extensions/firefox/table-sort.js @@ -65,11 +65,13 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { // Doesn't infer dates with delimiter "."; as could capture semantic version numbers. const dmyRegex = /^(\d\d?)[/-](\d\d?)[/-]((\d\d)?\d\d)/; const ymdRegex = /^(\d\d\d\d)[/-](\d\d?)[/-](\d\d?)/; + const numericRegex = /^(?:\(\d+(?:\.\d+)?\)|-?\d+(?:\.\d+)?)$/; const inferableClasses = { runtime: { regexp: runtimeRegex, class: "runtime-sort", count: 0 }, filesize: { regexp: fileSizeRegex, class: "file-size-sort", count: 0 }, dmyDates: { regexp: dmyRegex, class: "dates-dmy-sort", count: 0 }, ymdDates: { regexp: ymdRegex, class: "dates-ymd-sort", count: 0 }, + numericRegex: { regexp: numericRegex, class: "numeric-sort", count: 0 }, }; let classNameAdded = false; let regexNotFoundCount = 0; @@ -105,28 +107,44 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { } function makeTableSortable(sortableTable) { - const tableBody = getTableBody(sortableTable); - const tableHead = sortableTable.querySelector("thead"); - const tableHeadHeaders = tableHead.querySelectorAll("th"); - const tableRows = tableBody.querySelectorAll("tr"); + const table = { + body: getTableBody(sortableTable), + head: sortableTable.querySelector("thead"), + }; + table.headers = table.head.querySelectorAll("th"); + table.rows = table.body.querySelectorAll("tr"); + + let columnIndexesClicked = []; const isNoSortClassInference = sortableTable.classList.contains("no-class-infer"); - for (let [columnIndex, th] of tableHeadHeaders.entries()) { + for (let [columnIndex, th] of table.headers.entries()) { if (!th.classList.contains("disable-sort")) { th.style.cursor = "pointer"; if (!isNoSortClassInference) { - inferSortClasses(tableRows, columnIndex, th); + inferSortClasses(table.rows, columnIndex, th); } - makeEachColumnSortable(th, columnIndex, tableBody, sortableTable); + makeEachColumnSortable( + th, + columnIndex, + table, + sortableTable, + columnIndexesClicked + ); } } } - function makeEachColumnSortable(th, columnIndex, tableBody, sortableTable) { + function makeEachColumnSortable( + th, + columnIndex, + table, + sortableTable, + columnIndexesClicked + ) { const desc = th.classList.contains("order-by-desc"); - let tableArrows = sortableTable.classList.contains("table-arrows"); + const tableArrows = sortableTable.classList.contains("table-arrows"); const [arrowUp, arrowDown] = [" ▲", " ▼"]; const fillValue = "!X!Y!Z!"; @@ -249,9 +267,7 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { } } - let [timesClickedColumn, columnIndexesClicked] = [0, []]; - - function rememberSort(timesClickedColumn, columnIndexesClicked) { + function rememberSort() { // if user clicked different column from first column reset times clicked. columnIndexesClicked.push(columnIndex); if (timesClickedColumn === 1 && columnIndexesClicked.length > 1) { @@ -260,14 +276,15 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { const secondLastColumnClicked = columnIndexesClicked[columnIndexesClicked.length - 2]; if (lastColumnClicked !== secondLastColumnClicked) { - timesClickedColumn = 0; columnIndexesClicked.shift(); + timesClickedColumn = 0; } } + return timesClickedColumn; } - function getColSpanData(sortableTable, column) { - sortableTable.querySelectorAll("th").forEach((th, index) => { + 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; @@ -285,16 +302,7 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { } function getTableData(tableProperties) { - const { - tableRows, - column, - isFileSize, - isTimeSort, - isSortDateDayMonthYear, - isSortDateMonthDayYear, - isSortDateYearMonthDay, - isDataAttribute, - } = tableProperties; + const { tableRows, column, hasThClass, isSortDates } = tableProperties; for (let [i, tr] of tableRows.entries()) { let tdTextContent = getColumn( tr, @@ -305,17 +313,17 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { tdTextContent = ""; } if (tdTextContent.trim() !== "") { - if (isFileSize) { + if (hasThClass.fileSize) { fileSizeColumnTextAndRow[column.toBeSorted[i]] = tr.outerHTML; } // These classes already handle pushing to column and setting the tr html. if ( - !isFileSize && - !isDataAttribute && - !isTimeSort && - !isSortDateDayMonthYear && - !isSortDateYearMonthDay && - !isSortDateMonthDayYear + !hasThClass.fileSize && + !hasThClass.dataSort && + !hasThClass.runtime && + !isSortDates.dayMonthYear && + !isSortDates.yearMonthDay && + !isSortDates.monthDayYear ) { column.toBeSorted.push(`${tdTextContent}#${i}`); columnIndexAndTableRow[`${tdTextContent}#${i}`] = tr.outerHTML; @@ -329,17 +337,48 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { const isPunctSort = th.classList.contains("punct-sort"); const isAlphaSort = th.classList.contains("alpha-sort"); + const isNumericSort = th.classList.contains("numeric-sort"); + + 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 strLocaleCompare(str1, str2) { + return str1.localeCompare( + str2, + navigator.languages[0] || navigator.language, + { numeric: !isAlphaSort, ignorePunctuation: !isPunctSort } + ); + } + + function handleNumbers(str1, str2) { + let num1, num2; + num1 = parseNumberFromString(str1); + num2 = parseNumberFromString(str2); + + if (!isNaN(num1) && !isNaN(num2)) { + return num1 - num2; + } else { + return strLocaleCompare(str1, str2); + } + } + 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 a.localeCompare( - b, - navigator.languages[0] || navigator.language, - { numeric: !isAlphaSort, ignorePunctuation: !isPunctSort } - ); + return strLocaleCompare(a, b); } } @@ -391,9 +430,9 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { } function updateTable(tableProperties) { - const { tableRows, column, isFileSize } = tableProperties; + const { tableRows, column, hasThClass } = tableProperties; for (let [i, tr] of tableRows.entries()) { - if (isFileSize) { + if (hasThClass.fileSize) { tr.innerHTML = fileSizeColumnTextAndRow[column.toBeSorted[i]]; let fileSizeInBytesHTML = tr .querySelectorAll("td") @@ -426,71 +465,70 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { } tr.querySelectorAll("td").item(columnIndex).innerHTML = fileSizeInBytesHTML; - } else if (!isFileSize) { + } else if (!hasThClass.fileSize) { tr.outerHTML = columnIndexAndTableRow[column.toBeSorted[i]]; } } } + let timesClickedColumn = 0; th.addEventListener("click", function () { - timesClickedColumn += 1; const column = { - // column used for sorting; better name? toBeSorted: [], span: {}, spanSum: {}, }; - const visibleTableRows = Array.prototype.filter.call( - tableBody.querySelectorAll("tr"), + table.visibleRows = Array.prototype.filter.call( + table.body.querySelectorAll("tr"), (tr) => { return tr.style.display !== "none"; } ); - getColSpanData(sortableTable, column); + getColSpanData(table.headers, column); - const isDataAttribute = th.classList.contains("data-sort"); - if (isDataAttribute) { - sortDataAttributes(visibleTableRows, column); + const isRememberSort = sortableTable.classList.contains("remember-sort"); + if (!isRememberSort) { + timesClickedColumn = rememberSort(); } + timesClickedColumn += 1; - const isFileSize = th.classList.contains("file-size-sort"); - if (isFileSize) { - sortFileSize(visibleTableRows, column); - } + const hasThClass = { + dataSort: th.classList.contains("data-sort"), + fileSize: th.classList.contains("file-size-sort"), + runtime: th.classList.contains("runtime-sort"), + }; - const isTimeSort = th.classList.contains("runtime-sort"); - if (isTimeSort) { - sortByRuntime(visibleTableRows, column); + if (hasThClass.dataSort) { + sortDataAttributes(table.visibleRows, column); } - - const isSortDateDayMonthYear = th.classList.contains("dates-dmy-sort"); - const isSortDateMonthDayYear = th.classList.contains("dates-mdy-sort"); - const isSortDateYearMonthDay = th.classList.contains("dates-ymd-sort"); - // pick mdy first to override the inferred default class which is dmy. - if (isSortDateMonthDayYear) { - sortDates("mdy", visibleTableRows, column); - } else if (isSortDateYearMonthDay) { - sortDates("ymd", visibleTableRows, column); - } else if (isSortDateDayMonthYear) { - sortDates("dmy", visibleTableRows, column); + if (hasThClass.fileSize) { + sortFileSize(table.visibleRows, column); + } + if (hasThClass.runtime) { + sortByRuntime(table.visibleRows, column); } - const isRememberSort = sortableTable.classList.contains("remember-sort"); - if (!isRememberSort) { - rememberSort(timesClickedColumn, columnIndexesClicked); + const isSortDates = { + dayMonthYear: th.classList.contains("dates-dmy-sort"), + monthDayYear: th.classList.contains("dates-mdy-sort"), + yearMonthDay: th.classList.contains("dates-ymd-sort"), + }; + // pick mdy first to override the inferred default class which is dmy. + if (isSortDates.monthDayYear) { + sortDates("mdy", table.visibleRows, column); + } else if (isSortDates.yearMonthDay) { + sortDates("ymd", table.visibleRows, column); + } else if (isSortDates.dayMonthYear) { + sortDates("dmy", table.visibleRows, column); } const tableProperties = { - tableRows: visibleTableRows, + tableRows: table.visibleRows, column, - isFileSize, - isSortDateDayMonthYear, - isSortDateMonthDayYear, - isSortDateYearMonthDay, - isDataAttribute, - isTimeSort, + hasThClass, + isSortDates, }; getTableData(tableProperties); updateTable(tableProperties); diff --git a/npm/README.md b/npm/README.md index 445219c..6e0d26f 100644 --- a/npm/README.md +++ b/npm/README.md @@ -1,41 +1,57 @@ -![table-sort-js](https://img.shields.io/npm/v/table-sort-js) -![table-sort-js](https://img.shields.io/npm/dm/table-sort-js) -![table-sort-js](https://img.shields.io/github/repo-size/leewannacott/table-sort-js) -![table-sort-js](https://img.shields.io/github/license/LeeWannacott/table-sort-js) -![table-sort-js](https://img.shields.io/github/actions/workflow/status/leewannacott/table-sort-js/jest.yml?branch=master) +![npm version](https://img.shields.io/npm/v/table-sort-js) +![npm downloads](https://img.shields.io/npm/dm/table-sort-js) +[![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) +![build status](https://img.shields.io/github/actions/workflow/status/leewannacott/table-sort-js/jest.yml?branch=master) -## TABLE-SORT-JS. +# TABLE-SORT-JS. - Description: HTML table sorting library with sort type inference builtin and browser extension available. [#VanillaJS](http://vanilla-js.com/) - [Demo](https://leewannacott.github.io/Portfolio/#/GitHub) - [Documentation.](https://leewannacott.github.io/table-sort-js/docs/about.html) (work in progress) -- [npm package.](https://www.npmjs.com/package/table-sort-js) +- [npm package.](https://www.npmjs.com/package/table-sort-js) and [jsDelivr](https://www.jsdelivr.com/package/npm/table-sort-js) - [Firefox](https://addons.mozilla.org/en-US/firefox/addon/table-sort-js/) and [Chrome](https://chrome.google.com/webstore/detail/table-sort-js/dioemkojkjhlhmfiocgniipejgkbfibb) browser extensions: Tables of any website you visit become sortable! ## Install instructions. -Option 1. Install from npm: ` npm install table-sort-js` +- Option 1: Install from npm: + +```javascript +npm install table-sort-js +``` ```javascript import tableSort from "table-sort-js/table-sort.js"; ``` -Refer to the documentation for 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) +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) -Option 2. Download [table-sort.js](https://leewannacott.github.io/table-sort-js/table-sort.js) (Select save as.), or download a [minified version](https://cdn.jsdelivr.net/npm/table-sort-js) (~5kB) +- Option 2: Load as script from a Content Delivery Network (CDN): -Then add the following script before your HTML table: +```javascript + +``` -```html - +Or Minified (smaller size, but harder to debug!): + +```javascript + ``` 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) -#### To make tables sortable: +- 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 + +``` + +## To make tables sortable: - Add `class="table-sort"` to HTML <table> tags. - Click on table headers to sort columns. @@ -49,6 +65,8 @@ Refer to the documenation for examples on how to use table-sort-js with [HTML](h | "table-arrows" | Display ascending or descending triangles. | | "remember-sort" | If clicking on different columns remembers sort of the original column. | +
+ | <th> classes | Description | | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | | "data-sort" | Sort by [data attributes](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes), e.g <td data-sort="42"> | @@ -56,19 +74,24 @@ Refer to the documenation for examples on how to use table-sort-js with [HTML](h | "onload-sort" | Sort column on loading of the page. Simulates a click from the user. (can only sort onload for one column) | | "disable-sort" | Disallow sorting the table by this specific column. | +
+ | <th> Inferred Classes. | Description | | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| "numeric-sort" | Sorts numbers including decimals - Positive, Negative (in both minus and parenthesis representations) | | "dates-dmy-sort" | Sorts dates in dd/mm/yyyy format. e.g (18/10/1995). Can use "/" or "-" as separator. | | "dates-ymd-sort" | Sorts dates in [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) yyyy/mm/dd format. e.g (2021/10/28). Use "/" or "-" as separator. | | "file-size-sort" | Sorts file sizes(B->TiB) uses the binary prefix. (e.g 10 B, 100 KiB, 1 MiB); optional space between number and prefix. | | "runtime-sort" | Sorts runtime in hours minutes and seconds e.g (10h 1m 20s). Useful for sorting the GitHub actions Run time column... | +
+ | <th> Classes that change defaults. | Description | | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | "order-by-desc" | Order by descending on first click. (default is aescending) | | "alpha-sort" | Sort alphabetically (z11,z2); default is [natural sort](https://en.wikipedia.org/wiki/Natural_sort_order) (z2,z11). | | "punct-sort" | Sort punctuation; default ignores punctuation. | -#### Development: +## Development: If you wish to contribute, install instructions can be found [here.](https://leewannacott.github.io/table-sort-js/docs/development.html) diff --git a/npm/table-sort.js b/npm/table-sort.js index 385b443..4b19ea0 100644 --- a/npm/table-sort.js +++ b/npm/table-sort.js @@ -65,11 +65,13 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { // Doesn't infer dates with delimiter "."; as could capture semantic version numbers. const dmyRegex = /^(\d\d?)[/-](\d\d?)[/-]((\d\d)?\d\d)/; const ymdRegex = /^(\d\d\d\d)[/-](\d\d?)[/-](\d\d?)/; + const numericRegex = /^(?:\(\d+(?:\.\d+)?\)|-?\d+(?:\.\d+)?)$/; const inferableClasses = { runtime: { regexp: runtimeRegex, class: "runtime-sort", count: 0 }, filesize: { regexp: fileSizeRegex, class: "file-size-sort", count: 0 }, dmyDates: { regexp: dmyRegex, class: "dates-dmy-sort", count: 0 }, ymdDates: { regexp: ymdRegex, class: "dates-ymd-sort", count: 0 }, + numericRegex: { regexp: numericRegex, class: "numeric-sort", count: 0 }, }; let classNameAdded = false; let regexNotFoundCount = 0; @@ -105,28 +107,44 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { } function makeTableSortable(sortableTable) { - const tableBody = getTableBody(sortableTable); - const tableHead = sortableTable.querySelector("thead"); - const tableHeadHeaders = tableHead.querySelectorAll("th"); - const tableRows = tableBody.querySelectorAll("tr"); + const table = { + body: getTableBody(sortableTable), + head: sortableTable.querySelector("thead"), + }; + table.headers = table.head.querySelectorAll("th"); + table.rows = table.body.querySelectorAll("tr"); + + let columnIndexesClicked = []; const isNoSortClassInference = sortableTable.classList.contains("no-class-infer"); - for (let [columnIndex, th] of tableHeadHeaders.entries()) { + for (let [columnIndex, th] of table.headers.entries()) { if (!th.classList.contains("disable-sort")) { th.style.cursor = "pointer"; if (!isNoSortClassInference) { - inferSortClasses(tableRows, columnIndex, th); + inferSortClasses(table.rows, columnIndex, th); } - makeEachColumnSortable(th, columnIndex, tableBody, sortableTable); + makeEachColumnSortable( + th, + columnIndex, + table, + sortableTable, + columnIndexesClicked + ); } } } - function makeEachColumnSortable(th, columnIndex, tableBody, sortableTable) { + function makeEachColumnSortable( + th, + columnIndex, + table, + sortableTable, + columnIndexesClicked + ) { const desc = th.classList.contains("order-by-desc"); - let tableArrows = sortableTable.classList.contains("table-arrows"); + const tableArrows = sortableTable.classList.contains("table-arrows"); const [arrowUp, arrowDown] = [" ▲", " ▼"]; const fillValue = "!X!Y!Z!"; @@ -249,9 +267,7 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { } } - let [timesClickedColumn, columnIndexesClicked] = [0, []]; - - function rememberSort(timesClickedColumn, columnIndexesClicked) { + function rememberSort() { // if user clicked different column from first column reset times clicked. columnIndexesClicked.push(columnIndex); if (timesClickedColumn === 1 && columnIndexesClicked.length > 1) { @@ -260,14 +276,15 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { const secondLastColumnClicked = columnIndexesClicked[columnIndexesClicked.length - 2]; if (lastColumnClicked !== secondLastColumnClicked) { - timesClickedColumn = 0; columnIndexesClicked.shift(); + timesClickedColumn = 0; } } + return timesClickedColumn; } - function getColSpanData(sortableTable, column) { - sortableTable.querySelectorAll("th").forEach((th, index) => { + 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; @@ -285,16 +302,7 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { } function getTableData(tableProperties) { - const { - tableRows, - column, - isFileSize, - isTimeSort, - isSortDateDayMonthYear, - isSortDateMonthDayYear, - isSortDateYearMonthDay, - isDataAttribute, - } = tableProperties; + const { tableRows, column, hasThClass, isSortDates } = tableProperties; for (let [i, tr] of tableRows.entries()) { let tdTextContent = getColumn( tr, @@ -305,17 +313,17 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { tdTextContent = ""; } if (tdTextContent.trim() !== "") { - if (isFileSize) { + if (hasThClass.fileSize) { fileSizeColumnTextAndRow[column.toBeSorted[i]] = tr.outerHTML; } // These classes already handle pushing to column and setting the tr html. if ( - !isFileSize && - !isDataAttribute && - !isTimeSort && - !isSortDateDayMonthYear && - !isSortDateYearMonthDay && - !isSortDateMonthDayYear + !hasThClass.fileSize && + !hasThClass.dataSort && + !hasThClass.runtime && + !isSortDates.dayMonthYear && + !isSortDates.yearMonthDay && + !isSortDates.monthDayYear ) { column.toBeSorted.push(`${tdTextContent}#${i}`); columnIndexAndTableRow[`${tdTextContent}#${i}`] = tr.outerHTML; @@ -329,17 +337,48 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { const isPunctSort = th.classList.contains("punct-sort"); const isAlphaSort = th.classList.contains("alpha-sort"); + const isNumericSort = th.classList.contains("numeric-sort"); + + 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 strLocaleCompare(str1, str2) { + return str1.localeCompare( + str2, + navigator.languages[0] || navigator.language, + { numeric: !isAlphaSort, ignorePunctuation: !isPunctSort } + ); + } + + function handleNumbers(str1, str2) { + let num1, num2; + num1 = parseNumberFromString(str1); + num2 = parseNumberFromString(str2); + + if (!isNaN(num1) && !isNaN(num2)) { + return num1 - num2; + } else { + return strLocaleCompare(str1, str2); + } + } + 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 a.localeCompare( - b, - navigator.languages[0] || navigator.language, - { numeric: !isAlphaSort, ignorePunctuation: !isPunctSort } - ); + return strLocaleCompare(a, b); } } @@ -391,9 +430,9 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { } function updateTable(tableProperties) { - const { tableRows, column, isFileSize } = tableProperties; + const { tableRows, column, hasThClass } = tableProperties; for (let [i, tr] of tableRows.entries()) { - if (isFileSize) { + if (hasThClass.fileSize) { tr.innerHTML = fileSizeColumnTextAndRow[column.toBeSorted[i]]; let fileSizeInBytesHTML = tr .querySelectorAll("td") @@ -426,71 +465,70 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { } tr.querySelectorAll("td").item(columnIndex).innerHTML = fileSizeInBytesHTML; - } else if (!isFileSize) { + } else if (!hasThClass.fileSize) { tr.outerHTML = columnIndexAndTableRow[column.toBeSorted[i]]; } } } + let timesClickedColumn = 0; th.addEventListener("click", function () { - timesClickedColumn += 1; const column = { - // column used for sorting; better name? toBeSorted: [], span: {}, spanSum: {}, }; - const visibleTableRows = Array.prototype.filter.call( - tableBody.querySelectorAll("tr"), + table.visibleRows = Array.prototype.filter.call( + table.body.querySelectorAll("tr"), (tr) => { return tr.style.display !== "none"; } ); - getColSpanData(sortableTable, column); + getColSpanData(table.headers, column); - const isDataAttribute = th.classList.contains("data-sort"); - if (isDataAttribute) { - sortDataAttributes(visibleTableRows, column); + const isRememberSort = sortableTable.classList.contains("remember-sort"); + if (!isRememberSort) { + timesClickedColumn = rememberSort(); } + timesClickedColumn += 1; - const isFileSize = th.classList.contains("file-size-sort"); - if (isFileSize) { - sortFileSize(visibleTableRows, column); - } + const hasThClass = { + dataSort: th.classList.contains("data-sort"), + fileSize: th.classList.contains("file-size-sort"), + runtime: th.classList.contains("runtime-sort"), + }; - const isTimeSort = th.classList.contains("runtime-sort"); - if (isTimeSort) { - sortByRuntime(visibleTableRows, column); + if (hasThClass.dataSort) { + sortDataAttributes(table.visibleRows, column); } - - const isSortDateDayMonthYear = th.classList.contains("dates-dmy-sort"); - const isSortDateMonthDayYear = th.classList.contains("dates-mdy-sort"); - const isSortDateYearMonthDay = th.classList.contains("dates-ymd-sort"); - // pick mdy first to override the inferred default class which is dmy. - if (isSortDateMonthDayYear) { - sortDates("mdy", visibleTableRows, column); - } else if (isSortDateYearMonthDay) { - sortDates("ymd", visibleTableRows, column); - } else if (isSortDateDayMonthYear) { - sortDates("dmy", visibleTableRows, column); + if (hasThClass.fileSize) { + sortFileSize(table.visibleRows, column); + } + if (hasThClass.runtime) { + sortByRuntime(table.visibleRows, column); } - const isRememberSort = sortableTable.classList.contains("remember-sort"); - if (!isRememberSort) { - rememberSort(timesClickedColumn, columnIndexesClicked); + const isSortDates = { + dayMonthYear: th.classList.contains("dates-dmy-sort"), + monthDayYear: th.classList.contains("dates-mdy-sort"), + yearMonthDay: th.classList.contains("dates-ymd-sort"), + }; + // pick mdy first to override the inferred default class which is dmy. + if (isSortDates.monthDayYear) { + sortDates("mdy", table.visibleRows, column); + } else if (isSortDates.yearMonthDay) { + sortDates("ymd", table.visibleRows, column); + } else if (isSortDates.dayMonthYear) { + sortDates("dmy", table.visibleRows, column); } const tableProperties = { - tableRows: visibleTableRows, + tableRows: table.visibleRows, column, - isFileSize, - isSortDateDayMonthYear, - isSortDateMonthDayYear, - isSortDateYearMonthDay, - isDataAttribute, - isTimeSort, + hasThClass, + isSortDates, }; getTableData(tableProperties); updateTable(tableProperties); diff --git a/public/table-sort.js b/public/table-sort.js index eaf9bcd..4b19ea0 100644 --- a/public/table-sort.js +++ b/public/table-sort.js @@ -71,7 +71,7 @@ function tableSortJs(testingTableSortJS = false, domDocumentWindow = document) { filesize: { regexp: fileSizeRegex, class: "file-size-sort", count: 0 }, dmyDates: { regexp: dmyRegex, class: "dates-dmy-sort", count: 0 }, ymdDates: { regexp: ymdRegex, class: "dates-ymd-sort", count: 0 }, - numericRegex: {regexp: numericRegex, class: "numeric-sort",count:0} + numericRegex: { regexp: numericRegex, class: "numeric-sort", count: 0 }, }; let classNameAdded = false; let regexNotFoundCount = 0; diff --git a/test/table.test.js b/test/table.test.js index 32b7c6e..6d1f99e 100644 --- a/test/table.test.js +++ b/test/table.test.js @@ -512,7 +512,7 @@ test("Sort all combination positive, negative numbers with parenthesis as well", { classTags: "numeric-sort" } ) ).toStrictEqual({ - col0: ["-6","-3","-2.3","(1.4)","1","1.05","14"], + col0: ["-6", "-3", "-2.3", "(1.4)", "1", "1.05", "14"], }); }); @@ -520,11 +520,35 @@ test("Sort all combination of negative and positive integers and decimal numbers expect( createTestTable( { - col0: ["1.05", "-2.3", "-3", "1", "-6", "","(0.5)","1a","b","(c)","{1}"], + col0: [ + "1.05", + "-2.3", + "-3", + "1", + "-6", + "", + "(0.5)", + "1a", + "b", + "(c)", + "{1}", + ], }, { classTags: "numeric-sort" } ) ).toStrictEqual({ - col0: ["-6","-3","-2.3","(0.5)","1","1.05","{1}","1a","b","(c)",""], + col0: [ + "-6", + "-3", + "-2.3", + "(0.5)", + "1", + "1.05", + "{1}", + "1a", + "b", + "(c)", + "", + ], }); -}); \ No newline at end of file +});