Россия, Звенигород |
Списки и Деревья
13.5 Практика: Вкладка keywords
Этот раздел "Практика" посвящен стандартному XUL, использующему обычные скрипты, на котором мы смастерим теги <listbox> и <tree>. Сначала напишем статическую страничку на чистом XUL, а затем усовершенствуем ее, включив скриптовые эффекты. И еще немножко поэкспериментируем.
Наконец, пришло время завершить верстку и содержание диалогового окна NoteTaker. До этих пор вкладка Keywords тега <tabbox> содержала только место для тега. Мы это поправим, добавив список, дерево и еще некоторые элементы формы.
Вкладка Keywords позволяет нам добавлять и удалять ключевые слова из текущей заметки. Она также перечисляет текущие ключевые слова. Наконец, она высвечивает ключевые слова, связанные с текущими, т.е. что-то вроде подсказки для пользователя.
Чтобы спроектировать эту вкладку, мы должны вернуться к "Верстка с XUL" , "Верстка XUL", начать с грубого наброска и затем разработать внешний вид, статический контент, элементы формы и так далее. Мы не будем повторять здесь весь процесс, но лишь суммируем основные результаты.
<textbox> позволяет нам ввести ключевое слово.
<listbox> высвечивает текущий набор слов
Кнопка Add копирует содержание тега <textbox> в список <listbox> как новый элемент.
Кнопка Delete убирает содержание <textbox> из списка, если оно там есть.
Клик на строке списка <listbox> копирует ее в <textbox>.
Тег <tree> будет высвечивать слова, связанные с текущим словом в списке <listbox>.
Оба тега, и <listbox> и <tree>, будут иметь динамически изменяемый контент. Здесь мы реализуем это с помощью JavaScript, DOM и снимка содержания дерева. Пока что слова, связанные с текущим, мы будем брать из небольшого, фиксированного множества слов. В следующей лекции мы заместим часть кода решением получше, основанным на шаблонах.
Суммируя все требования, мы получим блок диалога на рисунке 13.8.
13.5.1. Верстаем <listbox> и <tree>
Не откладывая дела в долгий ящик, покажем структуру панели Keywords в Листинге 13.5.
<tabpanel> <vbox> <hbox> <vbox> <description value="Enter Keyword:"/> <textbox id="dialog.keyword"/> <hbox> <button id="dialog.add" label="Add"/> <button id="dialog.delete" label="Delete"/> </hbox> </vbox> <vbox> <description value="Currently Assigned:"/> <listbox/> </vbox> </hbox> <description value="Related:"/> <tree/> </vbox> </tabpanel>Листинг 13.5. Содержание новой панели диалога Edit.
Вкладка сверстана из двух блоков, сложенных вертикально. Верхний блок имеет левую и правую половины. В этом листинге теги <listbox> и <tree> для краткости не заполнены. Содержание тега <listbox> приведено в листинге 13.6.
<listbox id="dialog.keywords" rows="3"> <listitem label="checkpointed"/> <listitem label="reviewed"/> <listitem label="fun"/> <listitem label="visual"/> </listbox>Листинг 13.6. Статический контент тега <listbox> для текущих ключевых слов NoteTaker.
В полной версии NoteTaker эти слова будут извлечены из RDF-файла, полученного из тех слов, которые нам предстоит ввести вручную. Здесь мы начнем с нескольких фиксированных слов. Подобным образом, для тега <tree> мы начнем с фиксированного набора связанных слов. Содержание тега <tree> показано в Листинге 13.7.
Рисунок 13.8. Приблизительная диаграмма, демонстрирующая <list> и <tree> технологии.
<tree id="dialog.related" hidecolumnpicker="true" seltype="single" flex="1"> <treecols> <treecol id="tree.all" hideheader="true" flex="1" primary="true"/> </treecols> <treechildren flex="1"> <treeitem container="true" open="true"> <treerow> <treecell label="checkpointed"/> </treerow> <treechildren> <treeitem> <treerow> <treecell label="breakdown"/> </treerow> </treeitem> <treeitem> <treerow> <treecell label="first draft"/> </treerow> </treeitem> <treeitem> <treerow> <treecell label="final"/> </treerow> </treeitem> </treechildren> </treeitem> <treeitem container="true" open="true"> <treerow> <treecell label="reviewed"/> </treerow> <treechildren> <treeitem> <treerow> <treecell label="guru"/> </treerow> </treeitem> <treeitem> <treerow> <treecell label="rubbish"/> </treerow> </treeitem> </treechildren> </treeitem> <treeitem container="true" open="true"> <treerow> <treecell label="fun"/> </treerow> <treechildren> <treeitem> <treerow> <treecell label="cool"/> </treerow> </treeitem> </treechildren> </treeitem> </treechildren> </tree>Листинг 13.7. Статический контент тега <tree> для слов, связанных с текущим.
Слова в этом дереве сгруппированы в иерархическую структуру, но мы не подразумеваем, что дочерние ключевые слова являются уточнениями родительских. Заманчиво так думать о них. Однако они просто связанные, родственные концепции. Web-страничка может получить пометку с ключевым словом "гуру", но не словом "просмотрено", если мы решим, что автор странички широко известен, а вовсе не то, что содержание странички нам хорошо известно.
Иерархическое представление данных как нельзя лучше подходит для иерархически организованных данных, но может использоваться и для данных другого типа.
Мы используем его, чтобы представить связи, формирующие простую сеть. Вместо того чтобы рассматривать сеть целиком, мы раскрываем ее части шаг за шагом, начиная из различных точек. Это соответствует RDF запросам, описываемым ниже ( "Шаблоны" , "Шаблоны"), но здесь реализуется знакомыми нам XUL и JavaScript технологиями.
13.5.2. Систематическое использование обработчиков событий
Вот как этот диалог будет работать. Левая верхняя часть - место, где мы вводим новое ключевое слово. Клик по кнопке Add добавит набранное слово в список вверху справа; клик по Delete удалит его из списка. Клик по слову в списке скопирует его в <textbox>. Если в списке выбрано слово, то соответствующее слово выбирается в дереве и дерево прокручивается так, чтобы высветить его. Клик по слову в дереве также копирует его в форму ввода.
Все эти действия можно реализовать как команды. Некоторые из них тривиальны. Это был бы перебор - делать каждый фрагмент кода командой. Никакие из описываемых действий не выглядят как формальные "транзакции", "инструкции", "операции". Это просто клики по кнопкам. Мы реализуем их с помощью обычных обработчиков событий.
Мы могли бы добавить обработчики событий в каждый тег <listbox> и <tree> вот так ( onclick= ...), но мы этого делать не будем. Снимок содержания дерева также поддерживает обработку определенных действий, что напоминает приспособленные к случаю команды (см. методы снимка дерева, начинающиеся с префикса performAction ). Для наших простых событий нам нужно:
onclick on the Add <button> onclick on the Delete <button> onselect on the <listbox> onselect on the <tree>
Вместо того чтобы использовать XUL синтаксис, мы можем сделать более интересную вещь. Будем использовать "EventTarget" интерфейс DOM 2 Events, в частности, addEventListener() метод. Этот метод доступен для каждого объекта DOM Element, чему соответствует большинство тегов XUL. Используя только скрипты, мы установим все обработчики событий, когда диалоговый блок Edit впервые загружается. В Листинге 13.8 показано, как это сделать.
var ids = {}; function init_handlers() { var handlers = [ // id event ["dialog.add", "click", add_click], ["dialog.delete", "click", delete_click], ["dialog.keywords", "select", keywords_select], ["dialog.related", "select", related_select] ]; for (var i = 0; i < handlers.length; i++) { var obj = document.getElementById(handlers[i][0]); obj.addEventListener(handlers[i][1], handlers[i][2], false); ids[handlers[i][0]] = obj; } // also spot this final tag ids["dialog.keyword"] = document.getElementById "dialog.keyword"); } window.addEventListener("load",init_handlers, true);Листинг 13.8. Установка обработчиков событий.
Функция init_handlers() - это обработчик событий, стартующий, когда документ впервые загружается. Стартуя, он запускает еще пять обработчиков, используя id тегов, названия событий и функции, перечисленные в массиве обработчиков. Эти функции зависят от единственного аргумента, а именно объекта Event. В случае нашей простой панели каждый обработчик используется только в одном месте, так что объект Event не так уж и полезен. Функция также сохраняет в объекте ids DOM-объекты для каждого обработчика. Это сделано для удобства получения в дальнейшем таких объектов.
Опишем эти обработчики по очереди. Списки и деревья - объекты более сложные, чем простая кнопка, поэтому закатаем рукава - там очень много кода. Другие программные среды предоставляют свои библиотеки, заголовочные файлы, модули; для Mozilla же существует документация на XBL, DOM - и эта книга. В листинге 13.9 приведен код функции обработчика add_click():
function add_click(ev) { var listbox = ids["dialog.keywords"]; var textbox = ids["dialog.keyword"]; // getRowCount() workaround var items = listbox.childNodes.length; if (textbox.value.replace(/^ *$/,"") == "" ) return; // don't add pure whitespace for (var i = 0; i < items; i++) { if (listbox.getItemAtIndex(i).label == textbox.value ) return; // already exists } listbox.appendItem(textbox.value, textbox.value); listbox.scrollToIndex(items > 1 ? items - 2 : items); }Листинг 13.9. Обработчик add_click() для ключевых слов.
Эта функция добавляет набранное слово в список существующих слов. Она просматривает элементы списка, чтобы проверить, нет ли там уже такого элемента, и если нет, то добавляет его и прокручивает список так, чтобы мы могли видеть появившийся элемент.
Большинство вызовов этой функции взяты из Таблицы 13.2, но нам пришлось заглянуть и в XBL-код тегов <listitem> и <textbox>, чтобы обнаружить свойства label и value.
Нет в мире совершенства, и в то время когда мы собирали материал для этого курса, в XBL методе getRowCount() тега <listbox> была ошибка, которая, вероятно, уже исправлена к настоящему времени. Эта функция возвращает полное число элементов списка (когда работает правильно). Чтобы обойти эту ошибку, мы обратились к уровню AOM, то есть использовали базовые XML методы из стандарта DOM 1. Третья строчка кода возвращает объект DOM 1 Core NodeList, чье свойство "длина" является числом дочерних элементов тега <listbox>. В данном случае это работает правильно, поскольку мы знаем, что в нашем списке нет тегов <listcols>.
Листинг 13.10 показывает код обработчика delete_click().
function delete_click(ev) { var listbox = ids["dialog.keywords"]; var textbox = ids["dialog.keyword"]; var items = listbox.childNodes.length; for (var i = 0; i < items; i++) if ( listbox.getItemAtIndex(i).label == textbox.value ) { listbox.removeItemAt(i); return; } }Листинг 13.10. Код обработчика delete_click().
Этот обработчик не слишком отличается от add_click(). Он удаляет элемент из списка.
Листинг 13.11 показывает код обработчика keywords_select()
function keywords_select(ev) { var listbox = ids["dialog.keywords"]; var textbox = ids["dialog.keyword"]; var tree = ids["dialog.related"]; var items = document.getElementsByTagName('treecell'); var item = null, selected = null; try { listbox.currentItem.label; } catch (e) { return; } textbox.value = listbox.currentItem.label; var items = document.getElementsByTagName('treecell'); for (var i = 0; i < items.length; i++) { if (items.item(i).getAttribute("label") == textbox.value) { item = items.item(i).parentNode.parentNode; break; } } if ( item ) { selected = item; if ( tree.view.getIndexOfItem(item) == -1 ) { while (item.tagName != "tree") { if (item.getAttribute("container") != "") item.setAttribute("open","true"); item = item.parentNode.parentNode; } } // tree.currentIndex = tree.view.getIndexOfItem(selected); // only supplies the focus,not the selection. i = tree.view.getIndexOfItem(selected); tree.treeBoxObject.selection.select(i); tree.treeBoxObject.ensureRowIsVisible(i); } }Листинг 13.11. Код обработчика keywords_select()
Данная функция копирует label текущего выбранного элемента списка в текстовый блок. Это занимает одну строчку кода. Остальные ищут и высвечивают это же слово в дереве, если оно существует. Интерфейс снимка дерева, описанный в Таблице 13.4 работает только на раскрытых элементах дерева (не включая те, что скрыты в результате скролирования). Если слово невидимо в настоящий момент, интерфейс осмотра не найдет его. Поэтому нам приходится использовать DOM для поиска по дереву. Когда мы находим подходящий элемент, мы можем работать со снимком дерева, чтобы управлять его картинкой.
При копировании выбранного слова мы получаем еще одну неприятную проблему со списком. XBL-свойство currentItem реализовано некорректно. А именно, оно неверно работает, если список не имеет текущего выбранного элемента. Хотя это и не очевидно, но обращение к данному методу даст сообщение об ошибке в консоли, если не принять специальных мер. Эта ошибка возникает, потому что обработчик onselect вызывается и в том случае, когда список скролируется. Блок try{} этот специальный случай обрабатывает и отключает обработчик за ненадобностью.
Когда мы ищем слово, нам нужно показать строку, его содержащую. Мы начинаем с <treeitem> и достигаем вершины дерева, раскрывая все встретившиеся контейнеры с помощью open="true". Опять же это делается средствами DOM 1 Core. После того, как элемент дерева с искомым словом становится видимым, он становится видимым и через интерфейс снимка.
В конце концов, мы выбираем строчку дерева с клавиатуры. Мы видим ее и соседние слова, которые тоже могут быть значащими, на один уровень выше и ниже нашего слова. Чтобы это сделать, мы используем интерфейс снимка дерева, который предоставлен нам свойством XBL tree.view. Мы могли бы достичь того же результата так же просто (чуть более многословно), используя метод AOM tree.treeBoxObject, но тогда пришлось бы обращаться и к методу XPCOM's QueryInterface(), для доступа к интерфейсу этого объекта. Так что мы выбираем короткий путь.
Мы не можем задействовать XBL-свойство tree.currentInde, чтобы выбрать строку дерева, потому что в этом свойстве хранится строка, имеющая фокус, но не высвеченная строка. Если мы поэкспериментируем, то увидим прерывистую линию, появившуюся вокруг правильной строчки, показывающую, что фокус на месте. Приходится вместо этого использовать объект AOM treeBoxObject.selection, который реализует интерфейс nsITreeSelection. Простой метод select() высвечивает нужную строку. Наконец, мы возвращаемся к AOM treeBoxObject и прокручиваем строку так, чтобы она не была обрезана краем окошка окружающего дерево. Листинг 13.12 показывает код обработчика related_select().
function related_select(ev) { var textbox = ids["dialog.keyword"]; var tree = ids["dialog.related"]; textbox.value = tree.view.getCellText(tree.currentIndex, "tree.all"); }Листинг 13.12. Код обработчика related_select().
После keywords_select() код обрабртчика related_select() выглядит очень просто. Он подбирает текущее выбранное слово из снимка дерева и копирует его значение в текстовый блок. Никаких DOM-операций.
На этом мы закончим описание логики работы обработчиков событий для панели Keywords. Некоторые команды, такие как notetaker-save и notetaker-load, нуждаются в обновлении, чтобы учесть эту новую вкладку. Это мы отложим до тех пор, пока не реализуем обработку данных на основе RDF в "Шаблоны" .