Статьи

Построитель списков HTML: исследование по применению jQuery


Недавно клиент запросил конкретную реализацию для элемента управления в веб-приложении.
То, что они хотели, выглядело как стандартный виджет Windows, называемый Microsoft
создателем списков . Вы, несомненно, видели конструктор списков: два списка рядом друг с другом, левый изначально заполнен опциями, правый изначально пуст, с кнопками для перемещения выбранных элементов из одного списка в другой. Я узнал идиому и приступил к работе, думая, что найду решение без особых проблем. Конечно, я не собирался изобретать велосипед, поэтому первым делом я проверил наборы инструментов, которые я уже использовал.

 

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

 

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

 
Требования

 

Естественно, первое, что нужно собрать в любом решении, это набор требований.
Очевидными были следующие:

  1. Решение должно было содержать два списка
  2. Элементы должны быть в состоянии перемещаться с помощью кнопок из каждого списка в другой
  3. Пользователь должен иметь возможность использовать решение только с клавиатуры
  4. Решение должно было быть полезным в форме HTML

Требование 4 означало, что решение должно возвращать представление состояния, которое можно интерпретировать на сервере.
После некоторого рассмотрения я добавил следующее:

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

(Они были включены, чтобы визуально предположить, что все части принадлежат одному виджету.)

  1. Должны быть кнопки для перемещения всего содержимого каждого списка в другой
  2. Должны быть кнопки для перемещения любой комбинации элементов в каждом списке вверх или вниз (другими словами, для изменения порядка списков)
  3.  

Элементы

 

Первым шагом было, очевидно, поставить на место основные элементы, два выбора (очевидные варианты для списков) и кнопки (включая кнопку отправки для формы, к которой будет принадлежать построитель списка):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>List Builder</title>
<link rel="stylesheet" type="text/css" href="css/list-builder.css"/>
<script type="text/javascript" src="js/jquery-1.2.6.min.js"></script>
<script type="text/javascript" src="js/list-builder.js"></script>
</head>
<body>
<form method="post" action="list-builder.html">
<fieldset>
<div>
<div id="leftButtons">
<button type="button" id="leftUpButton" class="button">Up</button>
<button type="button" id="leftDownButton" class="button">Down</button>
</div>
<div id="leftList">
<select id="leftSelect" size="10" multiple="multiple">
<option value="first">First</option>
<option value="second">Second</option>
<option value="third">Third</option>
<option value="fourth">Fourth</option>
<option value="fifth">Fifth</option>
<option value="sixth">Sixth</option>
<option value="seventh">Seventh</option>
<option value="eighth">Eighth</option>
<option value="ninth">Ninth</option>
<option value="tenth">Tenth</option>
<option value="eleventh">Eleventh</option>
<option value="twelfth">Twelfth</option>
<option value="thirteenth">Thirteenth</option>
<option value="fourteenth">Fourteenth</option>
<option value="fifteenth">Fifteenth</option>
</select>
</div>
<div id="middleButtons">
<div id="addRemoveButtons">
<button type="button" id="addButton" class="button">Add</button>
<button type="button" id="removeButton" class="button">Remove</button>
</div>
<div id="addAllRemoveAllButtons">
<button type="button" id="addAllButton" class="button">Add All</button>
<button type="button" id="removeAllButton" class="button">Remove All</button>
</div>
</div>
<div id="rightList">
<select id="rightSelect" size="10" multiple="multiple">
</select>
</div>
<div id="rightButtons">
<button type="button" id="rightUpButton" class="button">Up</button>
<button type="button" id="rightDownButton" class="button">Down</button>
</div>
</div>
<div style="clear: both; padding-top: 15px;">
<input type="submit" value="Submit"/>
</div>
</fieldset>
</form>
</body>
</html>

На данный момент у меня были основные части, но это не выглядело так:


[img_assist | nid = 6915 | title = Построитель списка только в HTML | desc = | link = none | align = center | width = 375 | height = 490]

Следующим шагом была попытка стилизовать эти части в минималистском стиле:
@charset "utf-8";

fieldset {
border: 0;
margin: 0;
padding: 0;
}

#leftButtons, #leftList, #rightButtons, #middleButtons, #rightList {
float: left;
}

/* spacing between the add-remove group and the addAll-removeAll group */
#addAllRemoveAllButtons {
margin-top: 15px;
}

.button {
display: block;
margin-bottom: 3px;
}

Это выглядело немного лучше:


[img_assist | nid = 6916 | title = Построитель списков HTML и CSS | desc = | link = none | align = center | width = 330 | height = 220]
 

 Далеко не идеально, но, по крайней мере, требование 1 выполнено. Тем не менее, встал вопрос о вертикальном центрировании (не говоря уже о реальной функциональности, конечно!). Мне пришло в голову, что, хотя я мог бы, вероятно, найти CSS-решение, которое бы выполнило некоторые или даже большую часть того, что я имел в виду, браузер уже выполнил большую часть вычислений, которые мне были нужны: все, что мне нужно было сделать, это построить на том, что это уже сделано.
 

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


Размеры

Поскольку я предоставил большинству элементов в HTML
атрибуты
id , изменение размеров списков было довольно легко осуществить с помощью jQuery, который имеет отличную поддержку селекторов CSS:

  var leftSelect = $('#leftSelect');
var rightSelect = $('#rightSelect');
var selectWidth = leftSelect.width();
var selectHeight = leftSelect.height();
leftSelect.width(selectWidth).height(selectHeight);
rightSelect.width(selectWidth).height(selectHeight);

Краткое объяснение для тех, кто не знаком с jQuery: селектор «$ (‘# leftSelect’)» в jQuery возвращает элемент с атрибутом id «leftSelect», а именно левое выделение (вы можете увидеть воображение, которое я привел в мои идентификаторы), обернутый в объект jQuery, который действует почти как массив. Конечно, поскольку я использовал атрибут id, лучше бы это был массив из одного элемента! Однако было удобнее иметь обертку, так как она имеет несколько методов, которые упрощают и упорядочивают доступ к свойствам DOM. Это можно увидеть в оставшейся части кода и в значительной степени говорит само за себя,при условии, что методы, которые не принимают параметров, обычно возвращают значение свойства, а методы, которые принимают параметры, обычно сбрасывают значение свойства.
 

Элемент
div, который содержит право выбора, также должен быть достаточно широким, чтобы нажимать кнопки «вверх» и «вниз» справа от него в нужном месте:
  $('#rightList').width(selectWidth);

Требование 5 теперь выполнено:


[img_assist | nid = 6917 | title = Построитель списка выбирает размер | desc = | link = none | align = center | width = 400 | height = 210]
Центрирование

Браузер уже знает размеры всего и его нужно только попросить раскрыть его результаты. Однако сначала нам нужно иметь возможность интерпретировать строковые значения настроек стиля как число:

  var numericValue = function(value)
{
var digits = value.match(/[0-9]+/);
if (digits && digits.length > 0)
{
return Number(digits[0]);
}
return Number(0);
};

Нам также нужна высота
div, представляющего строку:

  var divHeight = $('#leftList').height();

После того, как у нас есть эти предпосылки, вертикальное центрирование div кнопок вверх и вниз
становится простым:

  var leftButtons = $('#leftButtons');
var leftButtonsHeight = leftButtons.height() + numericValue(leftButtons.css('margin-top')) +
numericValue(leftButtons.css('margin-bottom')) + numericValue(leftButtons.css('padding-top')) +
numericValue(leftButtons.css('padding-bottom'));
var leftButtonsMargin = ((divHeight - leftButtonsHeight) / 2) + 'px';
leftButtons.css('margin-top', leftButtonsMargin);

Горизонтальное центрирование немного сложнее, но не так страшно. Требуется знание ожидаемой высоты
div :

  var leftUpButton = $('#leftUpButton');
var leftDownButton = $('#leftDownButton');
var divWidth = leftButtons.width();
var leftUpMargin = ((divWidth - leftUpButton.width()) / 2) + 'px';
leftUpButton.css({ 'margin-left': leftUpMargin, 'margin-right': leftUpMargin });
var leftDownMargin = ((divWidth - leftDownButton.width()) / 2) + 'px';
leftDownButton.css({ 'margin-left': leftDownMargin, 'margin-right': leftDownMargin });
The same applies, of course, to the right up and down buttons.
The principle is the same with the middle buttons; there are simply more of them:
// center the middle buttons vertically
var middleButtons = $('#middleButtons');
var middleButtonsHeight = middleButtons.height() + numericValue(middleButtons.css('margin-top')) +
numericValue(middleButtons.css('margin-bottom')) + numericValue(middleButtons.css('padding-top')) +
numericValue(middleButtons.css('padding-bottom'));
var middleButtonsMargin = ((divHeight - middleButtonsHeight) / 2) + 'px';
middleButtons.css('margin-top', middleButtonsMargin);
// center the middle buttons horizontally
var addButton = $('#addButton');
var removeButton = $('#removeButton');
var addAllButton = $('#addAllButton');
var removeAllButton = $('#removeAllButton');
divWidth = middleButtons.width();
var addMargin = ((divWidth - addButton.width()) / 2) + 'px';
addButton.css({ 'margin-left': addMargin, 'margin-right': addMargin });
var removeMargin = ((divWidth - removeButton.width()) / 2) + 'px';
removeButton.css({ 'margin-left': removeMargin, 'margin-right': removeMargin } );
var addAllMargin = ((divWidth - addAllButton.width()) / 2) + 'px';
addAllButton.css({ 'margin-left': addAllMargin, 'margin-right': addAllMargin });
var removeAllMargin = ((divWidth - removeAllButton.width()) / 2) + 'px';
removeAllButton.css({ 'margin-left': removeAllMargin, 'margin-right': removeAllMargin });

Теперь кнопки и выделения отображаются правильно:


[img_assist | nid = 6918 | title = Окончательный вид Построителя Списка | desc = | link = none | align = center | width = 435 | height = 205]

и требование 6 выполнено.


Возвращение государства

 

Поскольку селекторы возвращают только значения выбранных опций, полное представление о состоянии построителя списка невозможно гарантировать без некоторого вмешательства JavaScript.
Скрытый ввод в HTML:
  <input type="hidden" name="listBuilderState" id="listBuilderState"/>

и метод, который можно вызывать в обработчиках событий:

  var updateListBuilderState = function() {
var state = '<?xml version="1.0" encoding="UTF-8"?>\n';
state += '<state>\n';
state += ' <select instance="left">\n';
var selected = $('#leftSelect option');
for (var i = 0; i < selected.length; i++)
{
var option = selected[i];
state += ' <value';
if (option.selected)
{
state += ' selected="true"';
}
state += '>';
state += option.value;
state += '</value>\n';
}
state += ' </select>\n';
state += ' <select instance="right">\n';
selected = $('#rightSelect option');
for (var i = 0; i < selected.length; i++)
{
var option = selected[i];
state += ' <value';
if (option.selected)
{
state += ' selected="true"';
}
state += '>';
state += option.value;
state += '</value>\n';
}
state += ' </select>\n';
state += '</state>\n';
$('#listBuilderState').val(state);
}

позволяет построителю списка возвращать XML-документ как часть отправки формы.
Это делает POST предпочтительным методом отправки форм с использованием построителя списков, поскольку длина URL-адресов GET может быть ограничена.
 

Этот метод будет вызываться во время нашей функции готовности к документу, чтобы инициализировать значение скрытого ввода формы и обновляться каждый раз, когда используются левый или правый выбор, с помощью мыши или клавиатуры:
  updateListBuilderState();
var updateState = function(event)
{
updateListBuilderState();
};
leftSelect.click(updateState).keyup(updateState);
rightSelect.click(updateState).keyup(updateState);

Это удовлетворяет требованию 4.

 
Функциональность Add-Remove

 

Благодаря продвинутым селекторам jQuery, функция переноса записей списка из одного выбора в другой несложна:
  handler = function(event)
{
$('#leftSelect option:selected').appendTo('#rightSelect').attr({ selected: false });
updateListBuilderState();
this.blur();
};
addButton.click(handler).keyup(handler);
handler = function(event)
{
$('#rightSelect option:selected').appendTo('#leftSelect').attr({ selected: false });
updateListBuilderState();
this.blur();
};
removeButton.click(handler).keyup(handler);
handler = function(event)
{
$('#leftSelect option').appendTo('#rightSelect').attr({ selected: false });
updateListBuilderState();
this.blur();
};
addAllButton.click(handler).keyup(handler);
handler = function(event)
{
$('#rightSelect option').appendTo('#leftSelect').attr({ selected: false });
updateListBuilderState();
this.blur();
};
removeAllButton.click(handler).keyup(handler);

Это соответствует требованиям 2 и 7.



Movin ‘on Up

 

Алгоритм перемещения записи вверх в пределах выбора не сложен; В объекте HTML DOM Select есть два соответствующих метода:
удалить и
добавить . Они работают так, что метод remove берет индексный номер (начиная с нуля) и удаляет соответствующий объект HTML DOM Option из select и нумерует порядковые номера для оставшихся объектов Option. Метод add принимает объект Option, который нужно добавить, и объект Option, уже находящийся в списке,
перед которым необходимо вставить предоставленный параметр. Если второй параметр имеет значение null, первый параметр добавляется в конец списка (то есть перед первой пустой записью).
 

Для отдельной записи, когда вы удаляете запись из списка, наверняка либо эта опция является первой (с индексом 0), либо у нее есть сосед с индексом ниже 1. Логика проста: установите опекуна, чтобы убедиться, что что если индекс удаляемой опции равен 0, ничего не делать;
если он больше 0, найдите этого соседа и используйте его в качестве второго параметра в методе add.
 

Снова на помощь приходит jQuery, упрощая процесс.
Сначала мы найдем выбранные варианты для перемещения (в нескольких параграфах мы вернемся к тому, как это сделать):
      var selected = ...;

Если они есть, мы проверяем первый индекс 0; если первый имеет индекс 0, мы просто выходим:

      if (selected.length > 0)
{
if (selected[0].index == 0)
{
return;
}
// main logic here
}

Приведенный выше комментарий показывает, куда идет основная логика. Это перебирает выбранные опции, вызывает функцию, которая получает индекс опции и получает объект Select, содержащий весь список (опять же, мы вернемся в нескольких параграфах о том, как это делается), находит предыдущую опцию в список и вызывает методы добавления и удаления, как описано выше:

        selected.each(function(index) {
var i = this.index;
var list = ...;
var previous = list.options[i - 1];
list.remove(i);
list.add(this, previous);
});

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

Однако мы не хотим вырезать и вставить логику дважды, один раз для левой и один раз для правой кнопки.
Вместо этого мы хотим параметризировать функцию, чтобы иметь возможность сказать, какой выбор мы хотим во время выполнения. Что делать?
 

Ответ зависит от природы JavaScript, который рассматривает функции не так, как многие известные языки, а
не обрабатывает их иначе, чем другие объекты. Другими словами, функции — это просто объекты, к которым вы можете обращаться с определенным синтаксисом. На самом деле функции JavaScript на самом деле являются
замыканиями . Закрытие — это функция, которая поставляется со средой «магически» назначенных внешних значений, на которые она может воздействовать. В случае функции JavaScript эти магические внешние значения состоят из любой переменной, которая находилась в области действия в момент создания функции.
 

Таким образом, мы можем использовать функцию, которая возвращает желаемую функцию-обработчик.
Логика одинакова для обоих списков, за исключением идентификатора списка; мы передадим идентификатор списка во внешнюю функцию в качестве параметра:
  var moveUpHandler = function(selectId)
{
return function(event) {
var selected = $('#' + selectId + ' option:selected');
// logic as before

Теперь мы можем видеть, как был
получен список выбранных опций: хотя
функция moveUpHandler уже давно завершена и выходит из области действия к тому времени, когда фактический обработчик (функция, которую он возвращает) запускает значение параметра
selectId , поскольку оно находилось в область действия, когда обработчик был создан, все еще доступна для использования в селекторе jQuery.
 

Нечто подобное делается внутри функции, применяемой к каждой из выбранных опций, поскольку selectId все еще находится в области действия даже там:
          var list = $('#' + selectId)[0];

Теперь всю функцию, возвращающую обработчик события, можно увидеть вместе:

  var moveUpHandler = function(selectId)
{
return function(event) {
var selected = $('#' + selectId + ' option:selected');
if (selected.length > 0)
{
if (selected[0].index == 0)
{
return;
}
selected.each(function(idx) {
var i = this.index;
var list = $('#' + selectId)[0];
var previous = list.options[i - 1];
list.remove(i);
list.add(this, previous);
});
}
updateListBuilderState();
this.blur();
};
}

Обратите внимание, что в конце логики для перемещения значений мы обновляем скрытый ввод с текущим состоянием построителя списка и размываем (то есть расфокусируем) кнопку.
Это последнее удобство, но код напоминает нам о многих значениях
этого в JavaScript: в функции, применяемой к каждой выбранной опции,
это опция; в функции обработчика
эта кнопка нажата.
 

Теперь назначить обработчик событиям, запускаемым левой кнопкой, просто:
  var handler = moveUpHandler('leftSelect');
leftUpButton.click(handler).keyup(handler);

Назначение обработчика правой кнопке аналогично.



Двигаться вниз

 

Более сложным является функционал для перемещения записей в списке вниз.
В частности, перемещение нескольких записей вниз является сложным. Что на самом деле это значит?
 

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

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

Предположим, вы выбрали 1, 2 и 4 для перемещения вниз.
Согласно второму определению, результат движения — 1, 3, 2, 5, 4. Первый выбор не перемещался вообще! Если вы попытаетесь переместить блок вверх, используя приведенную выше логику, ничего не произойдет, поскольку первый элемент блока имеет индекс 0. Это кажется нелогичным. Использование первого определения приводит к конфигурации 3, 1, 2, 5, 4, которая в результате выглядит более интуитивно понятной и легко переворачивается при перемещении блока вверх в соответствии с логикой, которую мы уже видели. Обратимость — это интуитивно полезная черта, поэтому мы будем использовать первое определение здесь.
 

Для его реализации мы будем использовать ту же методологию, которая описана выше, для создания замыкания.
Во-первых, мы должны убедиться, что есть что перемещать:
      var selected = $('#' + selectId + ' option:selected');
if (selected.length > 0)
{
// logic here
}

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

        var options = $('#' + selectId + ' option');
if (selected[selected.length - 1].index == options.length - 1)
{
return;
}

Затем мы должны определить, какую опцию каждый выделенный фрагмент в конечном итоге будет отображать
раньше (помните, что именно так
работает функция
добавления ). Мы упростим задачу, чтобы рассмотреть возможность перемещения только одного выбора за раз, и вызовем параметр, перед которым этот выбор должен быть перемещен,
х . Наша проблема, как это часто бывает в школьной алгебре, состоит в том, чтобы решить для
х .
 

Для этого нам понадобится вспомогательная функция, которая позволит нам различать выбранные параметры для перемещения и те, которые не были выбраны.
Это не сложно:
  var contains = function(list, value)
{
if (! value)
{
return false;
}
for (var i = 0; i < list.length; i++)
{
if (list[i] == value)
{
return true;
}
}
return false;
}

Имея это в виду, мы можем написать реальную логику.
Нам нужно держаться за две переменные. Первый вариант-кандидат
x (помните, это вариант-кандидат для второго параметра
метода
add ). Чтобы инициализировать его, мы просто посчитаем вперед два параметра из индекса выбора, который нужно переместить, пропустив первый.
 

Второй — это список опций, имеющих более высокий индекс, чем выбор, который нужно переместить. Мы перебираем этот список по одной опции за раз: если встречающаяся опция является членом выбранных опций, которые нужно переместить, пропускаем кандидата
х еще на один шаг вперед (это сохраняет отношения между выбранными опциями) и переходим к рассмотрению следующей вариант в списке. Если мы окажемся за концом списка, остановитесь;
x равен
нулю, и мы добавим в конец списка. В противном случае остановите, когда индекс кандидата
x больше, чем индекс текущей опции в списке.
        var befores = new Array();
for (var i = 0; i < selected.length; i++)
{
var currentIndex = selected[i].index;
var beforeIndex = currentIndex + 2;
var before = options[beforeIndex];
for (var j = 1; before; j++)
{
if (contains(selected, options[currentIndex + j]))
{
beforeIndex++;
before = options[beforeIndex];
}
else if (before.index > options[currentIndex + j].index)
{
break;
}
}
befores.push(before ? before : null);
}

Тогда, конечно, проблема проста:

        selected.each(function(index) {
list = $('#' + selectId)[0];
list.remove(this.index);
list.add(this, befores[index]);
});

Наконец, мы можем увидеть все это вместе:

  var moveDownHandler = function(selectId)
{
return function(event) {
var selected = $('#' + selectId + ' option:selected');
if (selected.length > 0)
{
var options = $('#' + selectId + ' option');
if (selected[selected.length - 1].index == options.length - 1)
{
return;
}
var befores = new Array();
for (var i = 0; i < selected.length; i++)
{
var currentIndex = selected[i].index;
var beforeIndex = currentIndex + 2;
var before = options[beforeIndex];
for (var j = 1; before; j++)
{
if (contains(selected, options[currentIndex + j]))
{
beforeIndex++;
before = options[beforeIndex];
}
else if (before.index > options[currentIndex + j].index)
{
break;
}
}
befores.push(before);
}
selected.each(function(index) {
list = $('#' + selectId)[0];
list.remove(this.index);
list.add(this, befores[index] ? befores[index] : null);
});
}
updateListBuilderState();
this.blur();
};
};

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

  handler = moveDownHandler('leftSelect');
leftDownButton.click(handler).keyup(handler);

Опять же, назначение обработчика правой кнопке аналогично.

Это соответствует требованиям 8.



доступность

 

Еще одна деталь реализации необходима для выполнения требования 3. Мы использовали несколько
обработчиков
onkeyup , которые полезны, когда проблема с доступом через клавиатуру является проблемой. Однако при переходе от одного элемента управления к другому на HTML-странице используется клавиша табуляции (или shift-tab для отмены движения), и мы не хотим, чтобы использование этих клавиш запускало обработчик onkeyup, когда все, что мы пытаемся сделать это перейти от одного элемента управления к другому!
 

Решение — простая функция, которая оборачивает другую функцию, проверяя код клавиши события, чтобы убедиться, что это не клавиша табуляции (символ ASCII 9) или клавиша Shift (16).
Если код ключа проходит тест, будет вызвана внутренняя функция:
  var doNotFireOnTab = function(handler) {
return function(event)
{
var keyCode = event.keyCode;
// ignore tab and shift-tab
if (keyCode == 9 || keyCode == 16)
{
return;
}
handler(event);
}
}

Теперь мы можем немного изменить присвоение, данное выше:

  handler = moveDownHandler('leftSelect');
leftDownButton.click(handler).keyup(doNotFireOnTab(handler));

Аналогичные изменения будут применены ко всем назначениям обработчиков onkeyup.

 
Вывод

 

Это был немалый набор требований.
Простота этого решения почти полностью обязана JQuery. Без базовой библиотеки код решения был бы гораздо более сложным. Спасибо, JQuery!

 
Скачать

 

Я настроил решение в файле WAR для всех, кто хочет попробовать.
Ваш местный Tomcat должен легко его развернуть; аналогичные решения на других языках должны быть относительно простыми. Решение чередуется между формой XHTML, содержащей построитель списка (примечание. Это может сбить с толку более старые версии IE), и страницей, показывающей XML, отправленный обратно на сервер, со ссылкой, возвращающейся на страницу формы.

Повеселись!