Только для читателей Lifeexample возможно открыть интернет-магазин на Moguta.CMS со скидкой в 15%

<<< Пространство имен PHP (Внедрение в проект) || Мнемоники HTML — полный список >>>

jQuery UI sortable – как узнать новую позицию элементов

21.02.2014
jQuery UI sortable – как узнать новую позицию элементов

Здравствуйте, уважаемые читатели блога LifeExample в этой статье будет показан процесс создания сортируемых элементов при помощи jQuery UI sortable, как сортировать элементы, отправлять обновленные данные на сервер через AJAX и обрабатывать их с помощью алгоритма определяющего удаленные элементы и место их удаления.

Если вы попали на эту страницу, значит вы скорее всего понимаете что-такое jQuery UI sortable, но на всякий случай поясню, jQuery UI sortable – это плагин позволяющий менять местами выводимые на HTML странице элементы, простым перетаскиванием их с помощью указателя мыши (обычно метод такого перемещения называют Drag and Drop – перетаскивай и отпускай ).

Сортировка элементов jQuery-UI-sortable

Использование jQuery UI sortable, в веб-проекте хорошо тем, что позволяет сэкономить большое количество времени, собственно как и множество других возможностей доступных в jQuery UI.

Написав всего одну строку кода, мы сделаем все пункты в маркированных списках перетаскиваемыми.

1
$("ul").sortable();

Несмотря на удобство использования, jQuery UI sortable порождает следующую проблему, о которой программист начинает задумываться лишь при необходимости зафиксировать новые позиции элементов в базе данных. Как узнать новую позицию элементов?

Проблема заключается вот в чем: после перетаскивания элемента, последовательность поменяла свой порядок, но как узнать с какой позиции был удален элемент и на какую он переместился.

Приведу пример для наглядности

Например, если первоначальной последовательностью были цифры

1, 2, 3, 4, 5

и число "3" было перемещено между "1" и "2", получаем следующий результат:

1, 3, 2, 4, 5

В тоже время если "2" был перемещен между "3" и "4", получаем следующий массив

1, 3, 2, 4, 5

Хммм… один и тот же массив…

Как понять какие элементы поменялись местами

Скачать исходный код примера ( Скачали: 751 чел. ) 

В скачанном архиве вы найдете классы Profile.php и SortableElement.php модуль инициализации index.php, который просто создает данные для демонстрации примера и занимается отбором. Файл sortExample.html является основной страницей и оформлен в style.css.

Файлы sortable.js и SortController.php являются важными. Они обеспечивают взаимодействие между браузером и сервером. В классе контроллера представлена логика для обработки получаемых данных от метода jQuery UI sortable при сортировке элементов.

Следующая функция JavaScript инициализирует контейнер (в данном случае div), который содержит сортируемые элементы и вызывается после построения страницы.

1
2
3
4
5
6
7
8
function initSortableContainer(){
  $(function(){
      $("#SortableElements").sortable({
         cursor: "move",
         stop: handleReorderElements
         });
      });
   }

С помощью функции handleReorderElements() отправляется AJAX запрос на сервер. Функция initSortableContainer() задает событие для контейнера div, который содержит сортируемые элементы и указывает, что функция handleReorderElements() вызывается после завершения операций по сортировке.

Функцией jQuery sortable(«serialize») мы можем получить строку GET параметров из всех отсортированных id элементов в новом порядке

1
2
3
4
5
6
7
8
9
10
function handleReorderElements(){
   var url = 'controllers/SortController.php';
   var fieldData = $( "#SortableElements" ).sortable( "serialize" );
   fieldData += "&action=reorderElements";

   var posting = $.post( url, fieldData);
   posting.done( function( data){
      reorderElementsResponse( data );
      });
   }

Используемая в приведенном выше коде функция reorderElementsResponse(data) является обработчиком обратного вызова AJAX метода, который был отправлен через jQuery UI sortable.

1
2
3
4
5
6
7
8
function reorderElementsResponse( data ){
   if (data.FAIL === undefined){
      $("#Message").html( data.resultString + data.itemIndexString );
      }
   else {
      alert( "Не удалось сохранить порядок!" );
      }
   }

Она выведет соответствующее сообщение об успешном или не успешном сохранении сортировки.

Для функционирования данного примера, id перемещаемого элемента должен содержать в себе нижнее подчеркивание и номер. Если вы обратите внимание на index.php, то увидите, что id перемещаемых элементов генерируются вот так

1
id='SortableElement_".$element->getId()

В результате id получаются такого рода: SortableElement_1, SortableElement_2, SortableElement_3 и т.д. Переменные, отправляемые на сервер, выглядят следующим образом: SortableElement[]=1&SortableElement[]=2&SortableElement[]=3, и т.д. и фактически рассматриваются в PHP как массив.

В SortController.php, мы можем получить этот массив с помощью

1
$sortIdArray = $_POST["SortableElement"];

Достаточно легко!

Обработка данных на сервере

SortController.php обрабатывает отправленный AJAX запрос через jQuery UI sortable со страницы. Он получает значения и сохраняет их в переменной $sortIdArray. Затем он вытягивает первоначальный массив из сессии и сохраняет ключи, которые являются id-шниками элементов в отдельный массив:

1
2
$sortList = $profile->getSortableElementList();
$originalIdList = array_keys( $sortList );

Следующая функция сравнивает массивы для определения того какой элемент был перемещен. Функция getArrayDiff($originalKeys, $newKeys) получает массив с исходной последовательностью Id элементов и массив с измененной последовательностью Id.

С помощью этой функции сравнивается каждый первоначальный и измененный порядковый номер элементов.

Сравнение происходит до момента, пока не найдутся не совпадающие друг с другом порядковые номера.

Один из id будет удален. Для того чтобы определить какой именно id будет удален эта функция удаляет первоначальный id из обоих массивов и повторно сравнивает два массива.

Структура таблицы в базе данных

А теперь пора немного вернуться назад и обсудить структуру таблицы, с помощью которой база данных сохраняет порядок измененных id.

Секрет состоит в типе данных DOUBLE для поля с порядком сортировки ItemIndex.

Вместо того чтобы перезаписывать все записи в таблице и обновлять их индексы для отображения нового порядка, мы просто можем изменить ItemIndex на дробную величину между предыдущим и следующим значением ItemIndex.

Например, если мы перемещаем ItemIndex 8 между ItemIndexes 3 и 4 мы просто обновляем индекс перемещенного объекта на 3.5.

Таким образом при сортировке по id , восьмой элемент будет отражаться ровно между третьим и четвертым. Это позволит сохранить порядок элементов всего одним запросом к базе.

При такой конфигурации таблицы всего лишь остается посчитать разницу между следующим и предыдущим ItemIndexes при обработке операций произведенной jQuery UI sortable с перемещением элементов в начало списка или в конец списка:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if ($previousKey === 0)
   {
   $itemIndex = (double)((double)(reset( $sortList )->getItemIndex() / 2));
   }
else if ($nextKey === INF)
   {
   $itemIndex = (double)((double)(end( $sortList )->getItemIndex() + 10));
   }
else
   {
   $lowIndex = (double)$sortList[(int)$previousKey]->getItemIndex();
   $highIndex = (double)$sortList[(int)$nextKey]->getItemIndex();
   $itemIndex = (double)($lowIndex + ($highIndex - $lowIndex) / 2);
   }

У данного метода есть один серьезный минус — мы можем потерять возможность сохранения индексов при многочисленном делении из-за нехватки точности вещественного числа.

На самом деле мы можем повторно разделить величину типа DOUBLE более тысячи раз, пока у не закончатся уточнения. На практике, возможно, этого не случится. Чтобы обезопасить себя от потери точности, необходимо время от времени обновлять все индексы в таблице, например с помощью cron задач или вручную

Данный метод был встречен мной на одном из буржуйских блогов. В статью я его включил для расширения кругозора, и потому что к нему имеются исходники. Лично мне больше по душе другой вариант сохранения сортировки для jQuery UI sortable, который я разработал самостоятельно и использую его в своем движке moguta.cms.

Второй пример сохранения jQuery UI sortable

Чтобы поменять порядок элементов, нам необходимо иметь в таблице поле sort с целочисленным типом INT, в отличии, от предыдущего примера где для аналогичного поля мы использовали тип DOUBLE. Как вы понимаете нам придется менять местами значения сортировки записей между друг другом.

Например, если в элементах 1,2,3,4,5 мы меняем местами два соседних элемента 3 и 2, это значит, что в одной строке таблицы произойдет замена индекса с 2 на 3, а в другой соответственно с 3 на 2. В этом случае в следующий раз при сортировке по полю sort нам будет выведена уже последовательность 1,3,2,4,5

Сложность заключается в вычислении всей цепочки обменов при перемещении, например, 4 элемента на место перед 2–ым. Вот так 1,4,2,3,5.

Для выявления цепочки перемещения мною был разработан следующий алгоритм. Используя штатные методы jQuery UI sortable такие как start() и update() мы можем обработать два события – начало сортировки и ее завершение, когда элемент отпущен и установлен на новую позицию.

Алгоритм сохранения сортировки jQuery UI sortable:

  1. На старте перед началом перемещения запоминаем последовательность id-шников элементов;
  2. Удаляем индекс перемещаемого элемента из последовательности;
  3. На место удаленного id записываем маркер ‘start’;
  4. После перемещения запоминаем новую последовательность id-шников;
  5. Удаляем индекс перемещаемого элемента из последовательности;
  6. На место удаленного id записываем маркер ‘end’;
  7. В итоге получаем два массива [«1», «2», «3», «start», «5»] и [«1», «end», «2», «3», «5»]
  8. Проходя в направлении от метки ‘start’ к метке ‘end’, отсекаем незатронутые элементы последовательностей.
  9. Получаем цепочку для последовательного обмена порядковых номеров [«3″,»2″,»4»]
  10. Передаем на сервер данный массив, где методом обработки производим последовательную перезапись порядковых номеров для строк в таблице базы данных, в соответствии с присланной AJAX запросом цепочкой перемещения элемента.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
/**
     * Разрешает менять местами строки таблицы, для сортировки элементов
     * tableSelector - селектор объекта , таблица в которой будет доступна сортировка строк
     * tablename - название таблицы в базе данных (обязательно должна иметь поля id и sort)
     * у строк таблицы обязательно должен быть атрибут data-id
     * @returns {undefined}
     */

    sortable: function(tableSelector,tablename) {      
     
      /**
       * создает массив позиций с заданной меткой на месте перемещаемого элемента
       * используется для сохранения стартовой последовательности и финальной
       */

      function createArray(ui, marker) {
        var strItems = [];
        // пробегаем по таблице сохраняя последовательность id строк
        // если встречаем id  =  id перемещаемого элемента то на это место добавляем маркер
        $(tableSelector).children().each(function (i) {
          var tr = $(this);
          if(tr.data("id")==ui.item.data("id")){
            strItems.push(marker);
          }else{
            if(tr.data("id")!=undefined){              
               strItems.push(tr.data("id"));
            }
          }
   
        });
       return strItems;
      }
   
      var listIdStart = [];
      var listIdEnd = [];
 

      if($(tableSelector).hasClass('ui-sortable')){
        $(tableSelector).sortable('destroy');      
        $(tableSelector).unbind();
      }
     

      $(tableSelector).sortable({  
        // на старте перед началом перемещения запоминаем последовательность id
        start: function (event, ui) {          
          listIdStart = createArray(ui,'start');        
        },
        // после перемещения запоминаемновую последовательность id
        update: function (event, ui) {
          listIdEnd = createArray(ui,'end');      
     
          var $thisId = ui.item.data("id");
          //Вычисляет последовательность замены порядковых индексов
          var sequence = getSequenceSort(listIdStart,listIdEnd,$thisId);
       
          if(sequence.length>0){
            sequence = sequence.join();            
            admin.ajaxRequest({
              mguniqueurl:"action/changeSortRow", //метод обработки на сервере
              switchId: $thisId,
              sequence: sequence,
              tablename: tablename,
            },
            function(response) {
              admin.indication(response.status, response.msg)            
              }
            );
          }
        }
      });
     
   
             
       /**
      * Вычисляет последовательность замены порядковых индексов
      * Получает  два массива
      * ["1", "start", "9", "2", "10"]
      * ["1", "9", "2", "end", "10"]
      * и ID перемещенной категории
      */

      function getSequenceSort(arr1, arr2, id) {
        var startPos = '';
        var endPos = '';

        // вычисляем стартовую позицию элемента
        arr1.forEach(function(element, index, array) {        
          if(element=="start"){
            startPos=index;
            arr1[index]=id;
            return false;
          }
        });

        // вычисляем конечную позицию элемента      
        arr2.forEach(function(element, index, array) {        
          if(element=="end"){
            endPos=index;
            arr2[index]=id;
            return false;
          }
        });  

        // вычисляем индексы которым и надо поменяться местами    
        var result = [];    

        // направление переноса, сверху вниз
        if(endPos>startPos){
          arr1.forEach(function(element, index, array) {        
            if(index>startPos && index<=endPos){
              result.push(element);
            }
          });
        }

        // направление переноса, снизу вверх
        if(endPos<startPos){
          arr2.forEach(function(element, index, array) {        
            if(index>endPos && index<=startPos){
              result.unshift(element);
            }
          });
        }

        return result;      
      };
    },

Такой подход как я упоминал выше применен в движке для интернет-магазинов Moguta.CMS при сортировки товаров. Вот вырезка javascript функции реализующей данный алгоритм:

Плюсы очевидны база всегда в актуальном состоянии, а все расчеты происходят на компьютере клиента.

На первый взгляд увеличение числа запросов пропорционально количеству перестановок.

На практике если сравнивать данный способ с первым, обновление по крону будет производить постоянно количество запросов столько же, но каждый день.

Надеюсь, статья о jQuery UI sortable будет полезной вам, и вы сможете выбрать для себя подходящий способ сохранения порядка перемещаемых элементов и вопрос как узнать новую позицию элементов вас больше не потревожит.

Было бы интересно послушать ваши комментарии относительно одного и другого приведенных методов сохранения сортировки.

Чтобы не пропустить публикацию следующей статьи подписывайтесь на рассылку по E-mail или RSS ленту блога.

Нравится

Комментарии

  • Виктор

    Ваш пример (второй) не рабочий, ошибки,ошибки. Куда вставлять этот код? Что за объект admin здесь admin.ajaxRequest( ….
    Что это вообще ajaxRequest ? за аякс…. Не знаю может я чего-то не понимаю.

  • Bobiz

    Вместо скриншота можно было и живой пример поместить, а так, извините, лажа получается 🙂

  • Роман

    Псевдокритикам поясню: данная статья рассчитана строго на тех, кто хоть как то разбирается в JavaScript языке и знаком с Sortable из фрэйма Jquery-UI. А так, могу лишь посоветовать поискать замечательные плагины, модули, дополнения для Ваших CMS

  • Кирилл

    А нельзя сделать как-то так:
    1. Из базы получаем порядковый номер каждого элемента (по нему же упорядочиваем);
    2. Сохраняем номер в отдельном аттрибуте элемента;
    3. При перемещении получаем id и порядковый номер элемента, который подняли и элемента на который «уронили» поднятый;
    4. Отправляем запрос на сервер: заменить порядковый номер элемента с id №1 на переданный, заменить порядковый номер элемента с id №2 на переданный.

  • Оставить комментарий

    Подписаться на комментарии к этой статье по RSS

    Яндекс.Метрика