body {
  min-height: 100vh;
  margin: 0;
  padding: 16px;
  box-sizing: border-box;
  display: flex;
  justify-content: center;
  align-items: start;
  font-size: 16px;
  font-family: "Segoe UI", Arial, Helvetica, sans-serif;
}

table {
  border-spacing: 0px;
}

th,
td {
  padding: 6px 18px;
}

thead th {
  border-bottom: 1px solid #333333;
  cursor: pointer;
  user-select: none;
}

thead th:hover {
  background-color: #eeeeee;
}

tbody td:not(:last-child) {
  border-right: 1px solid #333333;
}

tbody td:first-child {
  text-align: right;
}

tfoot td {
  border-top: 1px solid #333333;
  text-align: right;
  color: #444444;
}

th[data-sorting] {
  position: relative;
}

th[data-sorting]::after {
  position: absolute;
  top: 50%;
  right: 5px;
  width: 0;
  height: 0;
  transform: translate(-50%, -50%);
  content: "";
}

th[data-sorting="asc"]::after {
  border-bottom: 8px solid #4169e1;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
}

th[data-sorting="desc"]::after {
  border-top: 8px solid #4169e1;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
}
// Импорт библиотек
import dayjs from "https://esm.sh/dayjs@1.11.10";
import customParseFormat from "https://esm.sh/dayjs@1.11.10/plugin/customParseFormat";
import "https://esm.sh/dayjs@1.11.10/locale/ru";

// Конфигурация библиотек
dayjs.extend(customParseFormat);
dayjs.locale("ru");

// Объявление данных
const rawData = [
  { birthDate: "12.11.1967", firstName: "Льоня" },
  { birthDate: "04.03.1967", firstName: "Настя" },
  { birthDate: "28.04.2019", firstName: "Макс" },
  { birthDate: "11.02.1926", firstName: "Витя" },
  { birthDate: "02.04.1960", firstName: "Даша" }
];

// Парсим дату и добавляем номер записи
const mappedData = rawData.map((entry, index) => ({
  ...entry,
  birthDate: dayjs(entry.birthDate, "DD.MM.YYYY"),
  index
}));

// Объявление конфигурации таблицы
const columns = [
  {
    key: "index",
    title: "№",
    width: 30,
    render: (data) => data.index + 1
  },
  {
    key: "birthDate",
    title: "Дата рождения",
    width: 200,
    render: (data) => data.birthDate.format("DD MMMM YYYY")
  },
  {
    key: "firstName",
    title: "Имя",
    width: 120,
    render: (data) => data.firstName
  }
];

// Объявляем параметры сортировки для каждой колонки
const sorting = [
  { key: "index", compare: (a, b) => a - b },
  { key: "birthDate", compare: (a, b) => a.valueOf() - b.valueOf() },
  { key: "firstName", compare: (a, b) => a.localeCompare(b) }
];

// Функции для работы с таблицей

// Создём конфигурацию ширин колонок
const createTableColgroup = (table, columns) => {
  const colgroup = document.createElement("colgroup");

  for (const column of columns) {
    const col = document.createElement("col");
    col.width = column.width;
    colgroup.append(col);
  }

  table.append(colgroup);
};

// Создаём таблицу из данных и колонок
const createTable = (data, columns) => {
  const table = document.createElement("table");

  createTableColgroup(table, columns);

  const head = table.createTHead();
  const body = table.createTBody();
  const foot = table.createTFoot();

  const headRow = head.insertRow();
  for (const column of columns) {
    const cell = document.createElement("th"); // Создать th при помощи insertCell нельзя
    cell.dataset.key = column.key;
    cell.textContent = column.title;
    headRow.append(cell);
  }

  data.forEach((row, rowIndex) => {
    const bodyRow = body.insertRow();
    bodyRow.dataset.index = rowIndex;

    for (const column of columns) {
      const cell = bodyRow.insertCell();
      cell.innerHTML = column.render(row);
    }
  });

  const footRow = foot.insertRow();
  const footSummaryCell = footRow.insertCell();
  footSummaryCell.colSpan = columns.length;
  footSummaryCell.textContent = `Всего записей: ${data.length}`;

  return table;
};

// Сортируем строки уже созданной таблицы в определенном направлении по заданной колонке
const sortTable = (table, data, column, direction) => {
  const tableBody = table.tBodies[0];
  const bodyRows = [...tableBody.rows];
  let compareFunc = sorting.find((item) => item.key === column)?.compare;
  if (compareFunc === undefined) {
    compareFunc = () => 0;
  }

  bodyRows.sort((a, b) => {
    const aIndex = parseInt(a.dataset.index, 10);
    const bIndex = parseInt(b.dataset.index, 10);

    if (direction === "asc") {
      return compareFunc(data[aIndex][column], data[bIndex][column]);
    } else if (direction === "desc") {
      return compareFunc(data[aIndex][column], data[bIndex][column]) * -1;
    }

    return aIndex - bIndex;
  });

  for (const bodyRow of bodyRows) {
    tableBody.append(bodyRow);
  }
};

// Применяем визуальную индикацию сортировки
const applySorting = (headCells, sortingColumn, sortingDirection) => {
  headCells.forEach((headCell) => {
    headCell.removeAttribute("data-sorting");
    if (headCell.dataset.key === sortingColumn) {
      headCell.dataset.sorting = sortingDirection;
    }
  });
};

// Работа с таблицей

const table = createTable(mappedData, columns);
const headCells = [...table.tHead.rows[0].cells];

let sortingColumn = "birthDate";
let sortingDirection = "asc";

applySorting(headCells, sortingColumn, sortingDirection);
sortTable(table, mappedData, sortingColumn, sortingDirection);

// Объявляем слушатель клика на колонках шапки таблицы
headCells.forEach((headCell) => {
  headCell.addEventListener("click", (event) => {
    event.preventDefault();

    const columnKey = headCell.dataset.key;

    if (sortingColumn === columnKey) {
      if (sortingDirection === "asc") {
        sortingDirection = "desc";
      } else if (sortingDirection === "desc") {
        sortingColumn = undefined;
        sortingDirection = undefined;
      }
    } else {
      sortingColumn = columnKey;
      sortingDirection = "asc";
    }

    applySorting(headCells, sortingColumn, sortingDirection);
    sortTable(table, mappedData, sortingColumn, sortingDirection);
  });
});

// Добавляем собранную таблицу на страницу
document.body.append(table);

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.