Javascript. Сортировка таблицы

Доброго времени суток, Хабровчане.

Может для большинства здешних обитателей моя статья покажется слишком скучной и «и так понятно», но думаю новичкам будет в чем-то полезной.
Стояла передо мной как-то задача сделать сортировку таблицы по разным колонкам. Данный брались из базы, каждый раз их вытаскивать — накладно, решил задействовать JavaScript.
Были небольшие трудности из-за использования различной стилистики строк в таблице, а именно был разный цвет фона, в зависимости от данных самой таблицы.
Стилистику нарушать было нельзя. Кого заинтересовало, прошу под кат.


Подсветка фона выбрана так:
1) если время < 2012 год — подсветка красным
2) если расстояние > 9000 — подсветка желтым.
*) остальное чередование белого с серым.

Данные талицы берутся из БД, рисуется php, последняя колонка скрыта, это время в UNIX-формате, по ней сортировать колонку «Время» проще.

Подключаем такой класс:

var TableSort = function (idTbl, defSortCol, firstRow, classes) {
	// номер колонки, по которой выполнена текущая сортировка, считаем с 0
	var curSortCol = defSortCol;
	// номер колонки, у которой отрисовать картинку, показывающая направление сортировки, считаем с 0
	var curImgCol = defSortCol;
	// направление сортировки вверх
	var curSortUp = true;
	// id таблицы, в которой производим сортировку
	var curIdTbl = idTbl;

	// номер строки, с которой идут данные, считаем с 0
	var numColTr = (firstRow == null) ? 1 : firstRow;

	// нужно ли учитывать классы для строк
	if (classes == null) {	
		var style = false;
	} else {
		var style = true;
		// список классов, которые нужно сохранить
		var needClasses = classes[0];	
		// список классов, которые чередуются
		var listClasses = classes[1];
	}

    var tbl = document.getElementById(curIdTbl);
    var allImgs = new Array(); // all imgs-arrow
	allThs = tbl.getElementsByTagName('tr').item(0).getElementsByTagName('th');
	for (i=0; i<allThs.length; i++){
		if (allThs.item(i).getElementsByTagName('img') != null) {
			allImgs[i] = allThs.item(i).getElementsByTagName('img').item(0);
		} else {
			allImgs[i] = null;
		}
	}

	var tblData = new Array(); // current data in table

	// начинаем сортировку по колонке newCol, картинку рисуем у колонки imgCol
	this.initSort = function (newCol, imgCol) {
		if (newCol == curSortCol) {
			// кликнули на отсортированную колонку, меняем сортировку на обратную
			curSortUp = !curSortUp;
		} else {
			// сортируем по новой колонке
			curSortCol = newCol;
			curImgCol = (imgCol == null) ? newCol : imgCol;
			curSortUp = true;
		}
		showArrow();
		getDataTable();
		showSortTable();
		if (style) {
			doStyle();
		}
		
	};

	// show/change arrow
	function showArrow(){
		for (i=0; i<allImgs.length; i++){
			if (allImgs[i] != null) {
				if (i == curImgCol) {
					allImgs[i].style.visibility = "visible";
					if (curSortUp) {
						allImgs[i].src = "./img/up.png";
					} else {
						allImgs[i].src = "./img/down.png";
					}
				} else {
					allImgs[i].style.visibility = "hidden";
				}
			}
		}
	}

	// get new data from table
	function getDataTable() {
		allTrs = tbl.getElementsByTagName('tr');
		for (i=numColTr; i<allTrs.length; i++){
			tblData[i-numColTr] = new Array();
			for (j=0; j<allTrs[i].getElementsByTagName('td').length; j++) {
				tblData[i-numColTr][j] = allTrs[i].getElementsByTagName('td').item(j).innerHTML;
			}
			if (style) {
				tblData[i-numColTr][allTrs[i].getElementsByTagName('td').length]=allTrs[i].className;
			}
		}
		tblData.sort(_sort);
		if (!curSortUp) {
			tblData.reverse();
		}
	}

	// rules for sorting
	function _sort(a1, b1) {
		var a = a1[curSortCol];
		var b = b1[curSortCol];
		if (parseFloat(a) && parseFloat(b)) {
			return parseFloat(a) - parseFloat(b);
		} else {
			if (a.toLowerCase() < b.toLowerCase()) {
				return -1;
			} else if (a.toLowerCase() > b.toLowerCase()) {
				return 1;
			} else {
				return 0;
			}
		}
	}

	function showSortTable() {
		allTrs = tbl.getElementsByTagName('tr');
		for (i=numColTr; i<allTrs.length; i++){
			for (j=0; j<allTrs[i].getElementsByTagName('td').length; j++) {
				allTrs[i].getElementsByTagName('td').item(j).innerHTML = tblData[i-numColTr][j];
			}
			if (style) {
				allTrs[i].className=tblData[i-numColTr][allTrs[i].getElementsByTagName('td').length];
			}
		}
	}
	function doStyle(){
		allTrs = tbl.getElementsByTagName('tr');
		for (i=numColTr; i<allTrs.length; i++){
			if (allTrs[i] == null) {
				continue;
			}
			if(needClasses.indexOf(allTrs[i].className) != -1) {
				continue;
			}
			allTrs[i].className = listClasses[(i % listClasses.length)];
		}
	}

}

Создаем объект так:
var infoTblSort = new TableSort("idTbl", 0, 1, [['add', 'edd'],['odd', '']] );

Для работы в заголовке нужной колонки дописываем что-то похожее на:
onclick="infoTblSort.initSort(4,3);"

В этом примере передается 2 параметра, первый — по которому будет идти сортировка (в данном случае — это скрытая колонка UNIX-время), вторая — колонка, у которой будет стоять картинка, указывающая направление текста.

Пример тут.

На этом все, надеюсь мой опыт будет кому-то полезен. Прошу пощения за несколько сумбурный рассказ, впервые пишу статью.
Спасибо за внимание.

Similar posts

Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 17

    +1
    Если бы вы пользовались jQuery, то есть неплохой плагин tablesorter.com/docs/
    Но спасибо за реализацию на чистом JS
      0
      Спасибо за плагин. На тот момент с jQuery не приходилось работать.
      0
      так же для jQuery есть datatables.net/
        0
        Спасибо, посмотрю.
        0
        Когда-то давным-давно понадобилось, причем мне, который не сильно разбирается в js, css.
        Нашел какой-то скриптик, влепил — работает. Сейчас по комментариям в коде первая ссылка гугла ведет на в http://en.wikipedia.org/wiki/User:Alex_Smotrov/mw/sortable
          0
          На момент написания скрипта много гуглил. Что-то было реализовано, что сам доходил. Конкретно данную ссылку не использовал.
          +1
          Сортировка по времени не совсем корректно работает.
          gyazo.com/a304e33aaf01ba0eef7ffa97d02d4d43
            0
            Да, была ошибка. Спасибо, исправил. Копипаст — такой копипаст.
            +1
            Намного оптимальней будет хранить данные со ссылкой на строку в таблице отдельно от представления, тогда не придется доставать данные из DOM для сортировки.
              0
              А можно несколько подробнее?
                0
                Человек предлагает, один раз создать массив из 100500 данных таблицы, где одно из значений будет ссылкой на DOM элемент таблицы. После сортировки надо будет пробежаться по этим ссылкам и расставить DOM элементы (в данным случае это TR) в порядке сортировки.

                Как вариант, чтоб не захламлять память, можно в этом массиве данные заменить цифрами в порядке сортировки.
                Пример:

                было — ['Брест', 'Абакан', 'Воркута']
                стало — [2,1,3]

                А потом это уже сортировать легко и быстро.
                  0
                  Понятно. Вообще сначала так и хотел, 1 раз взять в данные из таблицы и больше их не вытаскивать, а просто показывать в нужном порядке. Проблему, с которой тогда столкнулся, не помню, и сделал в таком варианте.
                  Вариант с заменой на цифры тоже достоин внимания.
              0
              Несколько замечаний по коду:
              1. TableSort имеет слишком много параметров, смысл которых сразу из вызывающего кода и не разберёшь
              Лучше так
              new TableSort({
                  idTbl: 'idTbl',
                  defSortCol: 0,
                  firstRow: 1,
                  needClasses: ['add', 'edd'],
                  listClasses: ['odd', '']
              });
              

              2. Много переменных с глобальной областью видимости, хотя по замыслу их область видимости должна ограничиваться функцией TableSort. Как результат, код не будет работать в strict mode.
              3. Не создавайте массивы, используя конструктор. Скорей всего Ваши коллеги будут подражать Вам и рано или поздно потратят много эмоций, чтобы понять, что
              var arr = new Array(10);
              и
              var arr = [10];
              — совсем не одно и то же.
                0
                Спасибо, нет предела совершенству. Думаю в дальнейшем использовать jQuery и необходимый плагин.
                –1
                Спасибо что написали плагин.
                Таких как вы становится все меньше.

                plugindetector.com/category/tables

                Tablesorter конечно более надежен и многофункционален.
                И вообще поддержка и развитие это очень серьезная задача.
                Гораздо более серьезная чем первоначальное написание.
                  0
                  Приведенный способ среди всех, которые я видел, является средним (как по быстродействию, так и по сложности реализации). Действительно хорошим советом является отделение данных от DOM (KAdot). Это увеличит скорость.
                  Как-нибудь напишу статьи про:
                  1) построение HTML с помощью JS;
                  2) сортировку данных в таблицах.
                  Автору плюсик за старание.
                    0
                    Ваше решение, мягко говоря, не оптимально и годится для примера того «как делать не надо».
                    Обратим внимание на некоторые части вашего решения.
                    1. Для отделения мух от котлет в таблицах используются секции thead, tbody, tfoot. Если вы вынесете ваш заголовок (строку с th) в thead, вам не нужно будет задавать firstRow. В будущем вы можете добавить еще и tfoot для подвала. Секций tbody может быть несколько и видимо сортировать нужно в каждой секции отдельно (если конечно ваша логика вашего алгоритма не предусматривает иного).
                    2. Вы делаете очень накладные выборки и не кешируете их. Например:
                       for (j=0; j<allTrs[i].getElementsByTagName('td').length; j++) {
                           tblData[i-numColTr][j] = allTrs[i].getElementsByTagName('td').item(j).innerHTML;
                       }
                    

                    Этот код будет работать безумно долго, так как для строки вы N^2 раз выбираете все ячейки. То есть браузер перебирает N^2 DOM там где достаточно одного раза. Так же не забываем, что getElementsByTagName возвращает не массив, а коллекцию и обращение к length приводит к вычитыванию размерности этой коллекции. Об этом пишут практически в любом мануале по работе с DOM. Оптимальным (в плане работы с DOM) будет вариант:
                       var cells = allTrs[i].getElementsByTagName('td');
                       for (j = 0, cell; cell = cells[j]; j++) {
                          tblData[i-numColTr][j] = cell.innerHTML;
                       }
                    

                    3. Вот скажите, зачем вы таскаете html ячеек? Сначала вы нагружаете браузер когда перебираете ячейки, потом когда считываете innerHTML (он вычисляется), потом опять перебираете, и присваиваете html другой ячейке, заставляя браузер парсить этот html, а заканчиваете переносом классов. Почему нельзя перемещать строки?
                    Вся сортировка сводится к
                    var allTrs = tbl.tBodies[0].getElementsByTagName('tr');
                    
                    ...
                    
                    this.initSort = function (newCol, normalize) {
                      if (newCol == curSortCol)
                      {
                        curSortUp = !curSortUp;
                      }
                      else
                      {
                        curSortUp = true;
                        curSortCol = newCol;
                      }
                    
                      // вычисляем значение для сортировки
                      for (var i = 0, row; row = allTrs[i]; i++)
                        row.sortValue = normalize(row.childNodes[columnNum].innerHTML);
                    
                      // сортируем
                      allTrs.sort(function(a, b){
                        return (a.sortValue > b.sortValue) || -(a.sortValue < b.sortValue);
                      });
                    
                      if (!curSortUp)
                        allTrs.reverse();
                    
                      // выставляем ряды в нужном порядке
                      for (var i = allTrs.length - 1, row, last = null; row = allTrs[i]; i--)
                      {
                        row.parentNode.insertBefore(row, last);
                    
                        // тут можно сделать что-то с чередующимися классами
                    
                        last = row;
                      }
                    

                    Вот и все — функции getDataTable, _sort, showSortTable, doStyle можете удалить.
                    4. Кстати об initSort, она должна принимать номер колонки и функцию нормализации. По данным в html нельзя определить что за тип — это строка, число, дата или еще что. То есть нужно передавать:
                      initSort(1, Number); // для числовой сортировки
                      initSort(1, String);    // для строковой
                      initSort(1, function(value){ return value.toLowerCase() }) // для строковой без учета регистра
                      initSort(1, function(value){ return +new Date(value) }) // для сортировки дат
                          // но здесь value должно быть определенного формата, либо же пишется функция разбора даты..
                          // даты лучше сортировать как числа
                          // в любом случае вы вольны определить значение для сортировки так как нужно,
                          // автоматически это сделать не получится
                    
                      // Типовые сортировки можно сохранить в константы
                      TableSort.prototype.SORT_DATE = function(value){ return +new Date(value) };
                      ...
                      table.initSort(1, table.SORT_DATE);
                    

                    5. Этот код не правильно отсортирует если в таблице будут отрицательные числа
                        function _sort(a1, b1) {
                           ...
                            if (parseFloat(a) && parseFloat(b)) {
                                return parseFloat(a) - parseFloat(b);
                           ...
                    

                    6. Вместо добавления изображений в заголовки, лучше добавлять/удалять класс, а стили, в том числе и изображения, задавать через css.
                    7. Чередующиеся классы не является обязательным атрибутом таблицы, потому можно воспользоваться правилами :nth-child(2n) или :nth-child(2n + 1) чтобы раскрасить таблицу в зебру.
                      tr:nth-child(2n)
                      {
                        background: gray;
                      }
                    

                    В старых браузерах не будет зебры, но это не фатально, к тому же таких браузеров все меньше с каждым. Зато код будет меньше и чище и лучше стилизоваться через css.

                    В итоге ваша инициализация сортировки, если применить все рекомендации, будет выглядеть так:
                      var infoTblSort = new TableSort("idTbl", 0);
                    

                    И кода в разы меньше.

                    Удачи.

                    Only users with full accounts can post comments. Log in, please.