13. Объектная модель документа. DOM W3C
Контрольные задания
-
Обход дерева элементов. Написать скрипт, который:
- В каждый элемент внутри BODY добавляет атрибут title со значением, равным имени тега этого атрибута.
- Подсчитывает число элементов каждого вида внутри BODY.
-
Вставляет результат в последний абзац (заменяя его текстовое
содержимое) в виде, показанном на следующем примере:
Числа в скобках обозначают количество элементов с указанным именем тега.Количество элементов: 20. Элементы: BODY (1), HR (1), P(5).
РешениеФайл index.htm
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <HTML lang="ru"> <HEAD> <META http-equiv="Content-Type" content="text/html; charset=windows-1251"> <META http-equiv="Content-Script-Type" content="text/javascript"> <SCRIPT type="text/javascript" src="main.js"></SCRIPT> <TITLE>Задание 1</TITLE> </HEAD> <BODY> <H1>Задание 1</H1> <HR> <P> Скрипт этого приложения вставляет в каждый элемент атрибут <CODE>title</CODE> со значением, равным имени тега элемента. Кроме того, скрипт подсчитывает число элементов внутри <CODE>BODY</CODE>, считая сам <CODE>BODY</CODE> и выводит список самих элементов. </P> <P> <BUTTON id="button">Выполнить скрипт</BUTTON> </P> <P> После работы скрипта каждый элемент станет чувствительный к появлению над ним мышиного указателя (проверьте). </P> <P id="counter"> А в этом абзаце будет записано подсчитанное количество элементов и сами элементы. </P> </BODY> </HTML>
Файл main.js
// Обработчик события onload // Функциональный литерал будет вызван после полной загрузки документа // и построения DOM window.onload = function () { // Назначить обработчик события onclick элементу с id="button" document.getElementById("button").onclick = function () { // Подсчитаем количество элементов, получим их список // и навесим title на элементы var rez = skipping(document.body); // Создадим текстовое сообщение var str = "Количество элементов: "+rez.numElements + ". Элементы: "; for (var i in rez.list) str += i + "(" + rez.list[i] + "), "; str = str.replace(/, $/, "."); // Создадим текстовый узел var textEl = document.createTextNode(str); // Заменим им текст абзаца с id="counter" var counter = document.getElementById("counter"); // Этот метод сработает так, как нам хочется, если только // внутри абзаца нет других элементов (только текст) counter.replaceChild(textEl, counter.firstChild); }; // Функция получает узел node в качестве аргумента, // рекурсивно вычисляет количество узлов типа Element в нём // (считая сам корень), в каждый такой узел вставляет атрибут title // с именем тега элемента и составляет список используемых тегов с // указанием количества для каждого тега. // Функция возвращает объект rez с двумя свойствами: // rez.numElements -- число, общее число элементов; // rez.list -- объект, имена свойств соответствуют именам тегов, // а значения свойств равны их количествам. function skipping(node) { // Если это первый вход (с одним аргументом), // подготовим объект rez для результатов. // Иначе, если это рекурсивный вызов с двумя аргументами, // присвоим rez ссылку на второй аргумент, через который // передаётся объект с результатами var rez; if (arguments.length < 2) { rez = {}; // Создадим (первый раз) объект для результата rez.numElements = 0; // Обнулим счетчик элементов rez.list = {}; // Подготовим список элементов для // ассоциативного массива вида: // имя_элемента:количество } else rez = arguments[1]; // Если node есть узел Element, увеличиваем счетчик на 1 // и дополняем элемент атрибутом title с именем тега элемента. if (node.nodeType == 1 /* Node.ELEMENT_NODE */) { rez.numElements++; node.title = node.tagName; // Добавим элемент в список элементов if(rez.list[node.tagName]) rez.list[node.tagName]++; else rez.list[node.tagName] = 1; } var children = node.childNodes; // Получим список дочерних элементов for(var i=0; i<children.length; i++) // Смотрим все дочерние элементы skipping(children[i], rez); // Рекурсия по дочерним элементам return rez; // Возвращаем объект с результатами } };
Дополнительный комментарий
Извне функция skipping вызывается с одним аргументом и тогда в ней создается объект rez, а в нём обнуленный счетчик элементов (rez.numElements) и пустой ассоциативный массив rez.list для формирования списка тегов:
var rez; if (arguments.length < 2) { rez = {}; // Создадим (первый раз) объект для результата rez.numElements = 0; // Обнулим счетчик элементов rez.list = {}; // Подготовим список элементов для // ассоциативного массива вида: // имя_тега:количество } else rez = arguments[1];
Рекурсивный вызов функции в цикле по дочерним узлам выполняется с двумя аргументами:
for(var i=0; i<children.length; i++) // Смотрим все дочерние элементы skipping(children[i], rez); // Рекурсия по дочерним элементам
При таком входе в функцию сработает ветвь else, объект rez заново создаваться не будет, ему будет присвоена ссылка, полученная из второго аргумента:
else rez = arguments[1];
то есть ссылка на тот rez, который был создан при первом внешнем вызове функции skipping(node). Иными словами, все рекурсивные вызовы функции skipping будут работать с одним объектом rez, созданным при первом внешнем вызове функции skipping. Ссылка на этот же объект будет возвращена и в качестве результата работы функции.
Посмотрим внимательно на еще одно тонкое место кода:
// Добавим элемент в список элементов if(rez.list[node.tagName]) rez.list[node.tagName]++; else rez.list[node.tagName] = 1;
Переводится «на русский» это так: если в объекте rez.list уже есть свойство с именем node.tagName, увеличиваем значение этого свойства на 1, иначе заводим такое свойство и присваиваем ему 1.
Остальные части кода никакой сложности не содержат.
Решение c помощью jQuerypraxis/13/jobs/01(j)/index.htm
Файл index.htm
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <HTML lang="ru"> <HEAD> <META http-equiv="Content-Type" content="text/html; charset=windows-1251"> <META http-equiv="Content-Script-Type" content="text/javascript"> <SCRIPT type="text/javascript" src="jquery.min.js"></SCRIPT> <SCRIPT type="text/javascript" src="main.js"></SCRIPT> <TITLE>Задание 1</TITLE> </HEAD> <BODY> <H1>Задание 1</H1> <HR> <P> Скрипт этого приложения вставляет в каждый элемент атрибут <CODE>title</CODE> со значением, равным имени тега элемента. Кроме того, скрипт подсчитывает число элементов внутри <CODE>BODY</CODE>, считая сам <CODE>BODY</CODE> и выводит список самих элементов. </P> <P> <BUTTON id="button">Выполнить скрипт</BUTTON> </P> <P> После работы скрипта каждый элемент станет чувствительный к появлению над ним мышиного указателя (проверьте). </P> <P id="counter"> А в этом абзаце будет записано подсчитанное количество элементов и сами элементы. </P> </BODY> </HTML>
Файл main.js
// Обработчик готовности DOM $(function () { // Обработчик щелчка на кнопке $("#button").click(function () { var list = {"BODY":1}; // Здесь будем собирать теги и их количества // Пройдемся по всем элементам внутри BODY var numElements = $("BODY *").each(function () { this.title = this.tagName; // Навесим на элемент атрибут title // Добавим 1 к счетчикам элементов if(list[this.tagName]) list[this.tagName]++; else list[this.tagName] = 1; }).length+1; // вернем количество всех элементов внутри BODY // Создадим текстовое сообщение var str = "Количество элементов: "+numElements + ". Элементы: "; for (var i in list) str += i + "(" + list[i] + "), "; str = str.replace(/, $/, "."); // Заменим абзац с id="counter" $("#counter").replaceWith("<P>"+str+"</P>"); }); });
Дополнительный комментарий
Внутри обработчика щелчка на кнопке выполняется следующая работа:
-
Определяем объект list в котором названия свойств будут совпадать с названиями тегов, а значениями свойств будут количества элементов, относящихся к этим тегам.
var list = {"BODY":1}; // Здесь будем собирать теги и их количества
Объявление объекта сопровождается инициализацией его свойством "BODY" со значением 1. Элемент BODY на странице всегда один, вот мы его сразу и учли, а дальнейшую работу будем выполнять внутри BODY.
-
Отбираем все элементы внутри BODY:
$("BODY *")
Функция $ вернет обернутый набор.
-
Применяем метод each(функция) для обхода всех элементов полученного набора:
$("BODY *").each(function () { ... } )
-
Внутри итератора each для каждого элемента, ссылка на который задается ключевым словом this, делаем следующее:
-
Навешиваем на элемент атрибут title:
this.title = this.tagName; // Навесим на элемент атрибут title
-
Если элемент уже присутствует в объекте list, увеличим его
счетчик-значение на 1, если элемента еще нет, заводим
соответствующее свойство со значением 1.
// Добавим 1 к счетчикам элементов if(list[this.tagName]) list[this.tagName]++; else list[this.tagName] = 1;
-
-
Функция $ возвращает обертку. Но и каждый метод обертки снова возвращает обертку. Поэтому вычисляем число элементов как длину обернутого набора элементов:
var numElements = $("BODY *").each(function () { ... }).length+1; // вернем количество всех элементов внутри BODY
Длина увеличина на 1 чтобы учесть и тег BODY.
-
Создадим текстовое сообщение:
var str = "Количество элементов: "+numElements + ". Элементы: "; for (var i in list) str += i + "(" + list[i] + "), "; str = str.replace(/, $/, ".");
-
Заменим абзац с id="counter":
$("#counter").replaceWith("<P>"+str+"</P>");
Здесь применяется метод обертки replaceWith который заменяет своим аргументом каждый элемент обернутого набора (который у нас один — элемент с id="counter").
-
Текстовое содержимое документа. Написать скрипт, который формирует строку, содержащую все тексты из HTML-документа (в порядке их следования в документе). Полученную строку вывести на экран, добавив к документу новый абзац с полученным текстом.
РешениеФайл index.htm
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <HTML lang="ru"> <HEAD> <META http-equiv="Content-Type" content="text/html; charset=windows-1251"> <META http-equiv="Content-Script-Type" content="text/javascript"> <SCRIPT type="text/javascript" src="main.js"></SCRIPT> <TITLE>Задание 2. Текстовое содержимое документа</TITLE> </HEAD> <BODY> <H1>Задание 2. Текстовое содержимое документа</H1> <HR> <H2>Постановка задачи</H2> <P> Написать скрипт, который формирует строку, содержащую все тексты из HTML-документа (в порядке их следования в документе). Полученную строку вывести на экран, добавив к документу новый абзац с данным текстом. <H2>Решение</H2> <P> Решение базируется на функции <CODE>getText</CODE>, описанной в книге Девида Флэнагана «JavaScript. Подробное руководство» (пятое издание, Символ 2008 год). <P> <BUTTON id="button">Выполнить скрипт</BUTTON> </P> </BODY> </HTML>
Файл main.js
// Обработчик события onload // Функциональный литерал будет вызван после полной загрузки документа // и построения DOM window.onload = function () { // Назначить обработчик события onclick элементу с id="button" document.getElementById("button").onclick = function () { // Сформируем текстиовое содержимое var str = getText(document.body); // Добавим абзац к документу appendP(str); }; /** * getText(n): Отыскивает все узлы Text, вложенные в узел n, * Объединяет их содержимое т возвращает результат в виде строки. */ function getText(n) { // Операция объединения строк очень ресурсоёмкая, потому сначала // содержимое текстовых узлов помещается в массив, затем выполняется // операция конкатенации элементов массива в одну строку. var strings = []; // Массив, в который будем собирать строки getStrings(n, strings); // Заполнить массив при помощи функции getStrings return strings.join(" "); // Объединить и вернуть результирующую строку // Эта вложенная рекурсивная функция отыскивает все текстовые узлы // и добавляет их содержимое в конец массива. function getStrings(n, strings) { if (n.nodeType == 3 /* Node.TEXT_NODE */) strings.push(n.data); else if (n.nodeType == 1 /* Node.ELEMENT_NODE */) { // Обратите внимание, обход выполняется с использованием // методов firstChild и nextSibling for(var m = n.firstChild; m != null; m = m.nextSibling) getStrings(m, strings); } } } // Добавить абзац к документу function appendP(text) { // Создадим элемент "абзац" var el = document.createElement("p"); // Добавим к созданному элементу потомка -- текстовый элемент el.appendChild(document.createTextNode(text)); // Добавим построенный элемент к BODY -- и он сразу отобразится на экране document.body.appendChild(el); } };
Решение c помощью jQuerypraxis/13/jobs/02(j)/index.htm
Файл index.htm
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <HTML lang="ru"> <HEAD> <META http-equiv="Content-Type" content="text/html; charset=windows-1251"> <META http-equiv="Content-Script-Type" content="text/javascript"> <SCRIPT type="text/javascript" src="main.js"></SCRIPT> <TITLE>Задание 2. Текстовое содержимое документа</TITLE> </HEAD> <BODY> <H1>Задание 2. Текстовое содержимое документа</H1> <HR> <H2>Постановка задачи</H2> <P> Написать скрипт, который формирует строку, содержащую все тексты из HTML-документа (в порядке их следования в документе). Полученную строку вывести на экран, добавив к документу новый абзац с данным текстом. <H2>Решение</H2> <P> Решение базируется на функции <CODE>getText</CODE>, описанной в книге Девида Флэнагана «JavaScript. Подробное руководство» (пятое издание, Символ 2008 год). <P> <BUTTON id="button">Выполнить скрипт</BUTTON> </P> </BODY> </HTML>
Файл main.js
// Обработчик готовности DOM $(function () { // Обработчик щелчка на кнопке $("#button").click(function () { // Вставить текст документа после последнего его дочернего элемента $("BODY > *:last").after("<P&пt;"+ $(document.body).text()+"</P>"); }); });
Комментарий
$("BODY > *:last") — получает обертку, состоящую из одного элемента — последнего прямого потомка элемента BODY.
$("BODY > *:last").after(HTML-код) — метод обертки after вставляет свой аргумент после каждого элемента (у нас всего один) обертки.
$(document.body).text() — метод обертки text возвращает текстовое содержимое элементов обернутого набора. В нашем случае в качестве селектора указан объект document.body, соответствующий элементу BODY Можно было бы написать и так: $("BODY").text(). Результат был бы тем же самым.
-
Перестановка табличных столбцов. Написать скрипт, который по нажатию на кнопку выполняет цикличную перестановку табличных столбцов (первый столбец на второе место, второй — на третье, последний — на первое). Ниже приводится содержимое HTML-файла, изменять который не разрешается.
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <HTML lang="ru"> <HEAD> <META http-equiv="Content-Type" content="text/html; charset=windows-1251"> <SCRIPT type="text/javascript" src="main.js"></SCRIPT> <TITLE>Задание 3. Перестановка табличных столбцов</TITLE> </HEAD> <BODY> <H1>Задание 3. Перестановка табличных столбцов</H1> <TABLE id="table" border=1 cellspacing=0 cellpadding=10> <TR> <TH>Модель</TH> <TH>Цвет</TH> <TH>Цена</TH> </TR> <TR> <TD>Важная птица</TD> <TD>Триумф</TD> <TD>165</TD> </TR> <TR> <TD>Выеденное яйцо</TD> <TD>Антилопа Люкс</TD> <TD>15</TD> </TR> <TR> <TD>Жареный петух</TD> <TD>Торнадо</TD> <TD>875</TD> </TR> <TR> <TD>Заблудшая овца</TD> <TD>Миндаль</TD> <TD>21</TD> </TR> <TR> <TD>Маковая росинка</TD> <TD>Корсика</TD> <TD>46</TD> </TR> <TR> <TD>Фига с маслом</TD> <TD>Опатия</TD> <TD>383</TD> </TR> </TABLE> <P> <BUTTON id="button">Переставить</BUTTON> </P> </BODY> </HTML>
РешениеДавайте сначала разберёмся, какой DOM браузер строит для таблицы.
Если посмотреть на HTML-страницу в инспекторе DOM (Firefox/Инструменты/Инспектор DOM), увидим следующую картину:
BODY #text +H1 #text +BUTTON #text -TABLE В таблице только два дочерних узла: #text и TBODY #text -TBODY Строки погружены в TBODY (вместе с узлами #text) -TR Это первая строка таблицы (первый дочерний узел TBODY) #text В ней 7 дочерних элементов: 4 узла #text и 3 узла TH +TH #text +TH #text +TH #text #text Это второй дочерний узел TBODY -TR Это вторая строка таблицы (третий дочерний узел TBODY) #text В ней 7 дочерних элементов: 4 узла #text и 3 узла TD +TD #text +TD #text +TD #text #text Это четвертый дочерний узел TBODY +TR Это третья строка таблицы #text +TR #text +TR #text +TR #text +TR #text
Видим, что пробелы и концы строк (которые равнозначны пробелам) Firefox преобразует в текстовые узлы #text и вставляет их в дерево. Отметим, что так делает только Firefox, браузеры IE и Opera узлы #text не формируют и в дерево не вставляют. Но наш скрипт должен работать во всех браузерах. Запомним эту особенность Firefox.
Все браузеры погружают табличные строки внутрь элемента TBODY, даже если он не задан в HTML-коде страницы явно.
Таким образом, без учета текстовых узлов дерево таблицы в DOM имеет вид:
TABLE TBODY TR TR TR TR TR TR
Вот теперь мы готовы к разработке скрипта.
Первый вариант
// Массив ссылок на строки таблицы var trs = document.getElementById("table").getElementsByTagName("tr"); // ОБРАБОТЧИК ЩЕЛЧКА НА КНОПКЕ document.getElementById("button").onclick = function () { // Выполним перестановку в каждой строке таблицы for(var i=0; i<trs.length; i++) { // Массив ссылок на ячейки текущей строки // Указываем в качестве имени тега "*" -- это обеспечит // поиск в строке как td, так и th, а текстовые узлы будут // пропущены -- они не являются элементами. var tds = trs[i].getElementsByTagName("*"); // Последний элемент в строке переставим на место первого trs[i].insertBefore(tds[tds.length-1], tds[0]); } }
praxis/13/jobs/03(1)/index.htm
Это очень короткое решение. Ему не важно наличие (или отсутствие) текстовых узлов вперемежку с элементами. Ему не важно погруженность табличных строк в TBODY. Но у этого решения есть недостатки.
Посмотрите, отбор табличных ячеек в каждой табличной строке идет по инструкции:
var tds = trs[i].getElementsByTagName("*");
То есть отбираются все теги. Будут отобраны и TH, и TD — это хорошо! А если внутри табличной ячейки есть HTML-разметка? Например, некоторые слова выделены при помощи EM или STRONG? Скрипт работать перестанет, ибо указание "*" выберет все теги, в том числе и те, которые находятся внутри ячеек.
Второй вариант
Подправим скрипт так:
// Массив ссылок на строки таблицы var trs = document.getElementById("table").getElementsByTagName("tr"); // ОБРАБОТЧИК ЩЕЛЧКА НА КНОПКЕ document.getElementById("button").onclick = function () { // Выполним перестановку в каждой строке таблицы for(var i=0; i<trs.length; i++) { // Массив ссылок на ячейки текущей строки var tds = trs[i].getElementsByTagName("td"); if(!tds.length) tds = trs[i].getElementsByTagName("th"); // Последний элемент в строке переставим на место первого trs[i].insertBefore(tds[tds.length-1], tds[0]); } }
praxis/13/jobs/03(2)/index.htm
В HTML-коде таблицы в строку с жареным петухом добавлено выделение STRONG чтобы проверить работу кода для этого случая. Как и ожидалось, все прекрасно работает во всех браузерах (IE, Firefox, Opera).
Но… Представим, что какая-то ячейка таблицы содержит таблицу! Случаи вложенных таблиц не редки. Тогда код перестанет работать. Он перестанет работать уже на инструкции
var trs = document.getElementById("table").getElementsByTagName("tr");
ибо будут отобраны не только TR основной таблицы, но и TR вложенной таблицы.
praxis/13/jobs/03(2_)/index.htm
Папка 03(2_) содержит этот вариант. Скрипт остался прежнем, а в таблицу вложена еще одна таблица. Пощелкайте по кнопке несколько раз, убедитесь, что скрипт работает неправильно.
Придется разрабатывать новый скрипт, который будет работать только со строками основной таблицы, игнорируя таблицы вложенные.
Третий вариант
Скрипт в этом варианте работает так:
-
Получим массив дочерних узлов TABLE:
var trs = document.getElementById("table").childNodes;
Массив trs может включать единственный узел TBODY, может содержать дополнительно текстовые узлы, возникшие из-за пробелов и концов строк. В нем может оказаться и узел-элемент CAPTION — заголовок таблицы.
-
Найдем среди дочерних узлов узел TBODY. Потомками этого узла и будут элементы TR — строки таблицы. Так как дочерних узлов у TABLE может быть много (CAPTION, TBODY, #text), поиск выполним циклом. Значение i при выходе из цикла и даст номер узла TBODY в массиве:
for(i=0; i<trs.length; i++) if(trs[i].nodeType == 1 /* Node.ELEMENT_NODE */ && trs[i].tagName.toLowerCase() == "tbody") break;
-
Удалим текстовые прямые потомки (если они есть) в узле TBODY и получим массив табличных строк (уже без текстовых узлов)
trs = delTextNodes(trs[i]).childNodes;
Функция delTextNodes была немного изменена — в нее добавлен возврат узла, который был получен в качестве аргумента — это позволило записать непрерывную цепочку кода:
trs[i] — ссылка на TBODY (i нашел цикл)
delTextNodes(trs[i]) — ссылка на TBODY, в котором удалены прямые текстовые потомки
delTextNodes(trs[i]).childNodes — массив прямых потомков (уже без #text)
-
Удалим текстовые прямые потомки в каждой табличной строке
for(i=0; i<trs.length; i++) delTextNodes(trs[i]);
-
Запишем обработчик щелчка на кнопке
document.getElementById("button").onclick = function () { // Выполним перестановку в каждой строке таблицы for(var i=0; i<trs.length; i++) { // Массив ссылок на ячейки текущей строки var tds = trs[i].childNodes; // Последний элемент в строке переставим на место первого trs[i].insertBefore(tds[tds.length-1], tds[0]); } }
И теперь все вместе:
var i; // Получим массив дочерних узлов TABLE var trs = document.getElementById("table").childNodes; // Найдем среди дочерних узлов узел TBODY for(i=0; i<trs.length; i++) if(trs[i].nodeType == 1 /* Node.ELEMENT_NODE */ && trs[i].tagName.toLowerCase() == "tbody") break; // Удалим текстовые прямые потомки (если они есть) в узле TBODY // Получим массив табличных строк (уже без текстовых узлов) trs = delTextNodes(trs[i]).childNodes; // Удалим текстовые прямые потомки в каждой строке for(i=0; i<trs.length; i++) delTextNodes(trs[i]); // ОБРАБОТЧИК ЩЕЛЧКА НА КНОПКЕ document.getElementById("button").onclick = function () { // Выполним перестановку в каждой строке таблицы for(var i=0; i<trs.length; i++) { // Массив ссылок на ячейки текущей строки var tds = trs[i].childNodes; // Последний элемент в строке переставим на место первого trs[i].insertBefore(tds[tds.length-1], tds[0]); } } // Удалить из узла node текстовые узлы, которые являются // прямыми потомками node. Обратите внимание: если узел // удаляется, счетчик цикла не наращивается. Так сделано потому, // что список узлов -- "живой", на место удалённого узла встает // следующий (childNodes.length тоже меняется автоматически), и он // не будет проверен, если счетчик цикла увеличится в его заголовке. // Функция возвращает ссылку на преобразованный узел function delTextNodes(node) { var childNodes = node.childNodes; // Ссылка на "живой" массив детей for(var i=0; i<childNodes.length;) if(childNodes[i].nodeType == 3 /* Node.TEXT_NODE */) node.removeChild(childNodes[i]); else i++; return node; }
praxis/13/jobs/03(3)/index.htm
Правда о таблицах
Web-разработчики редко используют всю мощь таблиц, однако откройте «Спецификацию HTML 4.0» (или «4.1»), и прочитайте раздел «11. Таблицы». Вы узнаете о таблицах много интересного.
С каждой таблицей может быть связан заголовок (элемент CAPTION), предоставляющий краткое описание таблицы. Этот элемент размещается сразу за тегом TABLE и в объектном дереве таблицы является сестринским узлом узла TBODY.
Как мы уже видели, строки таблицы вкладываются в TBODY (тело таблицы), даже если этот элемент явно не записан в HTML-коде таблицы. Но кроме TBODY строки могут в таблице группироваться в элементах THEAD (раздел верхнего заголовка) и TFOOT (раздел нижнего заголовка).
Более того, можно объявлять свойства столбцов в начале определения таблицы с помощью элементов COLGROUP и COL, что позволяет браузерам генерировать таблицу последовательно, а не ждать считывания всех данных таблицы перед тем, как начать генерацию.
Для таблиц с THEAD и TFOOT, а также для таблиц с несколькими TBODY (такое тоже возможно!) наш скрипт работать не будет. Правда, задача ставилась для конкретной таблицы без всяких, приведенных выше, осложнений. Поэтому, будем считать, что она выполнена.
Решение c помощью jQuerypraxis/13/jobs/03(j)/index.htm
jQuery посвящена заметка 15. Но все же приведу здесь решение задачи про перестановку табличных столбцов при помощи этой библиотеки. Во-первых, вы сразу оцените простоту и малость кода, во-вторых, предварительно познакомитесь с этой библиотекой на конкретном примере, с которым основательно поработали в рамках «чистого» JavaScript.
Итак, приступаем.
Прежде всего добавим в HTML-код подключение этой библиотеки:
... <HEAD> <META http-equiv="Content-Type" content="text/html; charset=windows-1251"> <SCRIPT type="text/javascript" src="jquery.min.js"></SCRIPT> <SCRIPT type="text/javascript" src="main.js"></SCRIPT> <TITLE>Задание 3. Перестановка табличных столбцов</TITLE> </HEAD> ...
Библиотека поставляется в виде файла jquery.min.js, который мы подключаем к странице перед подключением нашего файла main.js.
А теперь само решение (содержимое файла main.js):
// Обработчик готовности DOM $(function () { // Получить набор табличных строк var tsr = $("#table > tbody > tr, #table > thead > tr, #table > tfoot > tr"); // Обработчик щелчка на кнопке $("#button").click(function () { // Выполнить функцию для каждого элемента набора (для каждой строки). tsr.each( function() { // Получить набор ячеек текущей строки var tds = $(this).children("td,th"); // Вставить последнюю ячейку перед первой (со старого места удаляется) $(tds[0]).before(tds[tds.length-1]); }); }); });
Папка 03(j) содержит файлы этого приложения.
Ну, краткость кода, вы, конечно, оценили визуально сразу! А его простота станет ясной после моих пояснений, приведенных ниже.
В библиотеке jQuery есть всего одна функция! Она имеет имя $.
Весь приведенный выше код является развернутой формулой:
$(функция);
Если аргументом функции $ является функция (в нашем случае это функциональный литерал), то $ выполняет аргумент-функцию после того, как браузер построит DOM.
Иными словами, запись $(функция); равнозначна нашей прежней записи:
// Обработчик события onload // Функциональный литерал будет вызван после полной загрузки документа // и построения DOM window.onload = function () { ... };
Правда, запись $(функция); не совсем равнозначна обработчику события onload, она лучше! Дело в том, что $ выполнит функцию-аргумент сразу, как только будет построен DOM, а событие onload наступает гораздо позже, только после того, как будут загружены все картинки страницы, или истечет время ожидания такой загрузки.
Теперь давайте разбираться с тем, что записано внутри функционального литерала:
// Получить набор табличных строк var tsr = $("#table > tbody > tr, #table > thead > tr, #table > tfoot > tr");
Мы снова видим вызов функции $. Аргументом на сей раз является строка, в которой записан селектор CSS.
#table — элемент с идентификатором id="table"
#table > tbody — все элементы TBODY, которые являются прямыми потомками элемента #table
#table > tbody > tr — все элементы TR, которые являются прямыми потомками элементов TBODY, которые являются прямыми потомками элемента #table
Теперь вы без труда расшифруете записи:
#table > thead > tr #table > tfoot > tr
Запятая между этими селекторами означает «или» (добавление к набору элементов, удовлетворяющих первому или второму, или третьему селектору). Таким образом, инструкция:
var tsr = $("#table > tbody > tr, #table > thead > tr, #table > tfoot > tr");
выполнит отбор всех строк таблицы (а табличные строки могут располагаться только в элементах TBODY, THEAD и TFOOT внутри TABLE). И этот набор (объект подобный массиву) будет присвоен переменной tsr.
Заметим, что будут отбираться только прямые потомки, значит, если таблица содержит вложенные таблицы, их строки не попадут в набор. Кроме того, нас совершенно не волнуют текстовые узлы #text, которые любит вставлять Firefox, преобразуя в них пробелы и концы строк в HTML-коде.
Мощно, правда? Одна инструкция, а делает столько много.
Итак, получен набор всех табличных строк. Теперь надо в каждой строке переставить последнюю ячейку на первое место (вставить перед первой ячейкой).
Можно это сделать обычным циклом for, но библиотека jQuery предлагает лучший способ.
Дело в том, что функция $ возвращает не просто набор отобранных по селектору элементов в виде объекта, подобного массиву, она дополняет этот объект выводком полезных методов, которыми можно этот набор обрабатывать. Это в терминологии jQuery называется обернутым набором или оберткой.
Итак, tsr — это обертка. Среди прочих, она имеет метод each(функция). Этот метод применяет функцию ко всем элементам обернутого набора (в нашем случае ко всем табличным строкам):
tsr.each( function() { // Получить набор ячеек текущей строки var tds = $(this).children("td,th"); // Вставить последнюю ячейку перед первой (со старого места удаляется) $(tds[0]).before(tds[tds.length-1]); });
В качестве аргумента each задан функциональный литерал, внутри которого расположены всего две инструкции.
Первая инструкция:
var tds = $(this).children("td,th");
получает обернутый набор, составленный из дочерних узлов TD и TH текущего узла, на который внутри each ссылается ключевое слово this.
Подробнее:
$(this) — здесь аргументом функции $ является объект (ссылка на объект, построенный для TR). Когда аргументом $ является объект, он возвращается в составе обертки (которая содержит все методы jQuery).
$(this).children("td,th") — к полученной обертки применяется метод children, который возвращает обернутый набор, состоящий из прямых потомков TD и TH текущего элемента TR.
Заметьте, мы опять игнорируем текстовые узлы #text, нам до них нет дела!
Вторая инструкция переставляет последнюю ячейку на первое место:
$(tds[0]).before(tds[tds.length-1]);
Подробнее:
$(tds[0]) — обернули первую ячейку методами jQuery
$(tds[0]).before(tds[tds.length-1]) — применяем метод before, который вставляет элемент, заданный аргументом (это последняя ячейка) перед текущим элементом (первой ячейкой).
Вот и все!
Осталось добавить, что цикл each вставлен в обработчик щелчка на кнопке с id="button":
$("#button").click(function () { // Выполнить функцию для каждого элемента набора (для каждой строки). tsr.each( function() { // Получить набор ячеек текущей строки var tds = $(this).children("td,th"); // Вставить последнюю ячейку перед первой (со старого места удаляется) $(tds[0]).before(tds[tds.length-1]); }); });
Подробнее:
$("#button") — получает обернутый набор, состоящий из одного элемента с идентификатором id="button".
$("#button").click(функция) — в каждый элемент набора (у нас всего один элемент) вставляет обработчик функция события click.
-