CB's blog

Из select’а в select. Работа с двойными списками

Июль 10, 2006

В этой заметке я опишу свою реализацию двойных списков - довольно полезного элемента управления при построении сложных HTML-форм (как обычно не без участия JavaScript). Особое внимание уделяется работе с событиями. В конце - работающий пример.

Итак как обычно сначала рассмотрим HTML-часть: очевидным решением было использование таблицы для layout’а элементов формы, но я решил от этого отказаться: во-первых, потому что таблицы сегодня не в моде, во-вторых, подобные решения с таблицами уже точно существуют, и потом без таблиц (используя список) можно отлично и элегантно обойтись:

<form action="" id="form_sel2sel" method="post">
<ul class="sel2sel">
   <li>
      <select size="10" id="sel1" name="sel1">
         <option value="1">Item1</option>
         <option value="2">Item2</option>
         <option value="4">Item4</option>
      </select>
   </li>
   <li>
      <input type="button" id="btn_move12" value="&gt;" />
      <br />
      <input type="button" id="btn_move21" value="&lt;" />
   </li>
   <li>
      <select size="10" id="sel2" name="sel2">
         <option value="3">Item3</option>
         <option value="5">Item5</option>
      </select>
   </li>
</ul>
<div class="clr"></div>
<p>
<input type="hidden" value="" name="sel2_values" id="sel2_values" />
<input type="submit" class="clr" value="Go" />
</p>
</form>

Как видим, используется два select’a: sel1 и sel2 (параметр size=10 делает их раскрытыми), две кнопки: btn_move12 и btn_move21 (для перебрасывания элементов из списка в список), hidden - о нем речь пойдет немного позже и submit. Конструкций типа onclick мы не видим (их и не будет).

Прежде чем подключать JavaScript, опишем CSS-стили. Их немного, потому приведу полностью:

* {
   padding: 0;
   margin: 0 auto;
}
body {
   text-align: center;
}
.clr {
   clear: both;
}
ul {
   list-style: none;
   width: 410px;
}
li {
   float: left;
}
input{
   width: 50px;
   height: 30px;
   font-size: 20px;
   margin: 27px;
}
select {
   width: 150px;
}

Результат можно посмотреть на скриншоте:

Sel2sel screenshot

Вот теперь сделаем, чтобы картинка ожила. Основная функция - move, ее переметры - первый и второй select’ы. Заметим что IE как всегда отличился, и для него добавление элемента в select выглядит немого по-другому чем для остальных.

function move(select1, select2)
{
   var selInd = select1.selectedIndex;
   if(selInd != -1)
   {
      var opt = document.createElement('OPTION');
      opt.text = select1.options[selInd].text;
      opt.value = select1.options[selInd].value;

      try {
         select2.add(opt,null);
      }
      catch(ex) {
         select2.add(opt); // IE only
      }

      select1.remove(selInd);
   }
}

Все, что осталось - привязать выполнение функции move к определенным событиям. В моем случае - это нажатия на кнопки ">", "<" а также doubleclick’и по элементах select’ов.

Самым простым способом, на первый взгляд кажется добавление прямо в HTML типа:

<input type="button" id="btn_move21" value="&lt;" oncklick="move(…)"/>

Однако по моему опыту и мнению многих вебдевелоперов с именем так делать не стоит. Гораздо лучшим вариантом будет оставить HTML в покое и все события обрабатывать в JS-файле. Я использую для этих целей общеизвестную функцию addEvent By Scott Andrew, а также свою вспомогательную на ее основе (для нее нужно указывать не элемент, а id элемента):

function addEventById(Id, evType, fn, useCapture)
{
   addEvent(document.getElementById(Id), evType, fn, useCapture);
}

Однако не все так просто и гладко как хотелось. Если нужно к событию привязать функцию с параметрами, возникают сложности. Следующая функция позволяет обойти эту проблему (_params - массив параметров, а результат - тоже функция, но уже с параметрами):

function bindParameter(_func, _params)
{
   return function()
   {
      _func.apply(this, _params)
   };
}

Но IE5 не поддерживает apply и тогда приходится дописывать:

if (!Function.prototype.apply)
{
   Function.prototype.apply = function(oScope, args)
   {
      var sarg = [];
      var rtrn, call;

      if (!oScope)
         oScope = window;
      if (!args)
         args = [];

      for (var i = 0; i < args.length; i++)
         sarg[i] = "args["+i+"]";

      call = "oScope.__applyTemp__(" + sarg.join(",") + ");";

      oScope.__applyTemp__ = this;
      rtrn = eval(call);

      try {
         delete oScope.__applyTemp__;
      }
      catch(ex){}

      return rtrn;
   }
}

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

var d;

var sel1;
var sel2;
var sel2_values;

function init()
{
   d = document;

   sel1 = d.getElementById("sel1");
   sel2 = d.getElementById("sel2");
   sel2_values = d.getElementById("sel2_values");
   sel2_values.value = "";

   addEventById("btn_move12", "click", bindParameter(move,Array(sel1, sel2)), false);
   addEventById("btn_move21", "click", bindParameter(move,Array(sel2, sel1)), false);
   addEventById("form_sel2sel", "submit", go, false);

   addEventById("sel1", "dblclick", bindParameter(move,Array(sel1, sel2)), false);
   addEventById("sel2", "dblclick", bindParameter(move,Array(sel2, sel1)), false);
}

addEvent(window, "load", init, false);

После этого программа справляется со своими визуальными заданиями: элементы перебрасываются. Но скорее всего требовательному пользователю этого будет мало. Он захочет передавать информацию о состояниях selecto’в на сервер. Как жто сделать. Мне известно то что при передачи данных формы select’ы передают одно выбранное значение (или несколько в случае multiple). Нам же нужно передавать все значения. Для этого перед отправкой данных на сервер, запишем множество значений в hidden - элемент (значения разделяются символом |):

function go()
{
   var s = '';
   sel2_values.value = '';

   for(i=0; i<=sel2.options.length-1; i++)
       s += sel2.options[i].value + "|";

   sel2_values.value = s;
}

После отправки данных формы, сервер получает sel2_values, которые несложно обработать, например, PHP-функцией explode.

Под конец - небольшой "Список литературы" - те ресурсы без которых эта заметка не состоялась бы:

  1. http://www.mredkj.com/tutorials/tutorial005.html
  2. http://anchor.siter.com.au/dmitry/doublelisting.html
  3. http://youngpup.net/2002/oldblog123

Вот и все. Работающий пример ждет своего просмотра.

Комментарии (12) на “Из select’а в select. Работа с двойными списками”

  1. Евгений:

    Интересный пример, спасибо ;)

  2. Shimon:

    V Opera 9 poslednii’ element posle perenosa odnogo iz elementov vpravo nivkakuyu ne hochet perehodit’ tuda zhe. Naprimer esli pervym peredvinut’ Item4, to posle etogo Item2 ostanetsya poslednim i ne perei’det vpravo. Tak zhe v obratnuyu storonu.

  3. Dead Krolik:

    В опере однозначно есть баг.

  4. svoloshyn:

    Хм. В моей Опере 8.51 вроде все хорошо. Буду искать девятую и искать глюки.

  5. svoloshyn:

    Кажется, мне кое-как удалось решить проблему с Opera 9. Для этого нужно заменить

    try {
       select2.add(opt,null);
    }

    на

    try {
       select2.add(opt,null);
    
       if(select1.options.length>1)
       {
          if(selInd>0)
             select1.selectedIndex = selInd-1;
          else
             select1.selectedIndex = selInd+1;
       }
    }

    А вообще, работа с select’ами довольно грязная, так как выползает масса глюков со стороны браузеров. Кстати, ходят слухи, в ИЕ 7 этот елемент переписали заново, поскольку в прежних версиях там баги лезли отовсюду

  6. mor:

    Переделанная функция для поддержки select multiple

    function move_select(select1, select2) {
            var remove_list = new Array();
            for (i=0; i < select1.length; i++) {
                    if (select1.options[i].selected == true) {
                            var opt = document.createElement('option');
                            opt.text = select1.options[i].text;
                            opt.value = select1.options[i].value;
                            try {
                                    select2.add(opt,null);
                            } catch(ex) {
                                    select2.add(opt); // IE only
                            }
                            remove_list.push(i);
                    }
            }
            for (i=0; i < remove_list.length; i++) {
                    if (i == 0) {
                            select1.remove(remove_list[i]);
                    } else {
                            select1.remove(remove_list[i]-i);
                    }
            }
    }
    
  7. svoloshyn:

    2mor: С комментерия видно только что переделанная. НО:
    Зачем переделанная. Что там нового добавляется? Что было не так? Хочется комментариев + не хочется разбирать код.

  8. mor:

    2svoloshyn Добавлена возможность перемещения сразу нескольких выбранных позиций, в твоём примере перемещать можно было только по одному.

  9. svoloshyn:

    2mor: ясно. Извини, что не понял сам. Но и тебе стояло бы об этом написать перед своим кодом (select multiple тоже информативная фраза, но лично до меня почему-то сразу не дошло :( ).

    Я же написал для перемещения по одному для упрощения. Ведь главное - идея. К тому же если выбрать несколько позиций, теряется смысл перемещения по даблклику…

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

  10. Dub:

    Super

  11. ell:

    Сайт о регулярных выражениях Regexp

  12. vint:

    Спасибо, полезная статья!

Добавить комментарий