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
+});