Селекторы наизнанку

9 июля 2009 года, 20:04

В то время, как работа над Каскадными Таблицами Трансформации (CTS) идёт полным ходом, в голову приходят самые разнообразные идеи, с помощью которых довольно просто облегчить жизнь программисту. Одна из таких идей умудрилась прийти в голову во время разработки механизма в CTS, который отвечает непосредственно за создание новых элементов.

Идея напрямую связана с CSS-селекторами. Все привыкли, что селекторы помогают нам выбирать объекты. Это логично, потому они так и называются. Селекторы — это один из способов упрощения навигации по дереву элементов в документе. Однако они могут быть полезны не только в качестве гидов, но и в качестве направляющих при создании новых элементов в документе.

Селекторы добавления элементов

Когда мы с помощью JavaScript создаём элементы в документе, мы используем функцию document.createElement. Данная функция в качестве аргумента принимает имя элемента, который следует создать. После этого мы можем вручную установить необходимые параметры элемента: CSS-класс, уникальный идентификатор и другие. Позже, если понадобится, мы создаём новые элементы и вкладываем в уже созданный, выстраивая себе таким образом локальное дерево элементов.

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

Конечно, все CSS-селекторы нам ни к чему, поэтому мы оставим только контрольные: по CSS-классу, по уникальному идентификатору элемента и по его имени, конечно. В итоге наш селектор будет иметь такой общий вид (части в квадратных скобках не являются обязательными):

имя[#идентификатор][.класс]

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

Как определять селекторы

Перед тем, как реализовывать нашу основную функцию, потренируемся на поиске селекторов в строке. Создадим простую функцию, с помощью которой строка в вышеприведённой нотации будет преобразована в массив селекторов.

//Мы продолжаем в комментариях к этому коду. //Выясним, как работает эта функция. function parse_selector(raw_selector) { //Для начала создадим массив всех селекторов. //Их количество может быть разным и всех их мы будем хранить здесь. var selector = []; //Получаем все встречающиеся в строке важные части. //Они будут последовательно расположены в массиве raw_selector. raw_selector = raw_selector.match(/(?:#|\.)?\w+/g); //Проходимся по вновь собранным данным. for (var i = 0, j = -1, n = raw_selector.length; i < n; i++) { //Извлекаем из строки первый символ (он для нас контрольный), //а также остальную часть строки (контрольный символ не входит в значение конструкции). var type = raw_selector[i].substring(0, 1), content = raw_selector[i].substring(1); //Для каждого типа заполняем соответствующее поле в массиве. switch(type) { //Идентификатор элемента case "#": selector[j]["id"] = content; break; //CSS-класс элемента case ".": selector[j]["class"] = content; break; //Имя элемента default: //Начинаем новый элемент прямо здесь selector[++j] = {}; selector[j]["element"] = raw_selector[i]; break; } } //Пользователь получает такой массив на выходе: // [{"element" : имя, "class" : класс, "id" : идентификатор}, // {"element" : имя, "class" : класс, "id" : идентификатор}...] //Последовательность элементов совпадает с их вложенностью в друг друга. return selector; }

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

Попробуем теперь рассмотреть, как должна работать функция создания локального дерева элементов по селектору.

Функция локального дерева

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

Рассмотрим на примере. Пусть нам дан следующий селектор:

div#element div.inner em#deep.class

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

<div id="element"> <div class="inner"> <em id="deep" class="class"></em> </div> </div>

В соответствии с выясненными нами требованиями, функция должна возвратить такой объект, который бы дал нам доступ к каждому из элементов. Как такое сделать? Довольно просто: функция будет возвращать ассоциативный массив, ключами в котором будут частичные селекторы от общего, переданного в функцию. Для нашего примера частичный селектор самого первого элемента div будет равен div#element, а вложенного в него — div#element div.inner. Таким образом можно обеспечить доступ ко всему построенному дереву.

Более того, мы создадим в возвращаемом функцией массиве ещё два ключа для быстрого доступа к корневому и наиболее вложенному созданным элементам — root и last.

Мы знаем всё что нужно для реализации функции.

//Функция принимает два аргумента на входе. //Первый — это селектор в виде обычной строки. //Второй — это корневой элемент по умолчанию. //Второй аргумент не обязателен, но если он задан, //то всё дерево будет являться вложенным в заданный элемент. function create_element_by_selector(selector, current) { //Создаём массив для хранения элементов и строку частичного селектора. var elements = {}, active = ""; //Если текущий элемент задан (аргумент current), то используем его как корневой. if (current != null) { elements["root"] = current; } //Как и в первом блоке кода, выделяем ключевые части. selector = selector.match(/(?:#|\.)?\w+/g); //Проходимся по собранным элементам for (var i = 0, j = -1, n = selector.length; i < n; i++) { //Извлекаем из строки первый символ (он для нас контрольный), //а также остальную часть строки (контрольный символ не входит в значение конструкции). var type = selector[i].substring(0, 1), content = selector[i].substring(1); //Создаём своеобразный автомат. switch(type) { //Добавляем текущему элементу идентификатор case "#": current.setAttribute("id", content); break; //Добавляем текущему элементу CSS-класс case ".": current.className = content; break; default: if (current == null) { //Если текущий элемент не задан, создаём его и определяем как корень. current = document.createElement(selector[i]); elements["root"] = current; } else { //Если задан, создаём вложенность. active += " " //Пробел необходим для обособления элемента в селекторе. elements[active] = current; current = current.appendChild(document.createElement(selector[i])); } break; } //Добавляем к частичному селектору содержимое обработанной строки. active += selector[i]; } //Так как последний элемент остался необработанным, //добавляем указатель на него после цикла. elements[active] = current; //Не забываем добавить указатель на самый вложенный элемент. elements["last"] = current; //Возвращяем всё на радость пользователю. return elements; }

Функция готова. Давайте попробуем её в действии на примере. Положим, имеем следующий элемент:

<div id="element"></div>

Используем нашу функцию и создадим в нём необходимую вложенность:

var local_tree = create_element_by_selector("div span.inner a#new", document.getElementById("element"));

В итоге получим следующее локальное дерево:

<div id="element"> <div> <span class="inner"> <a id="new"></a> </span> </div> </div>

Мы создали переменную local_tree, с помощью которой можем обращаться к элементам дерева. Установим вложенному элементу span какой-нибудь CSS-класс, а ссылке зададим направление и содержимое:

//Добавляем CSS-класс нужному элементу //Обратите внимание, что частичный селектор должен содержать все ключевые конструкции. local_tree["div span.inner"].className += " deep"; //Управляем ссылкой (она самая-самая вложенная в нашем дереве) local_tree["last"].href = "http://enumerate.ru/"; local_tree["last"].innerHTML = "Я — ссылка, а ты нет";

Заключение

Данное исследование являлось прикладным по отношению к разрабатываемому сейчас CTS. Автор считает, что подобные исследования, хоть порой и не являются новаторскими, однако позволяют по-новому взглянуть на привычные вещи и даже переосмыслить их. Даже если и существуют альтернативные способы решения проблемы, это не значит, что данный способ не имеет право на существование: любое исследование — это и есть основа существования многогранных решений повседневных проблем. Постарайтесь подойти к этому с философской точки зрения.

Мнения (9)

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

  • Kalan

    09 июля 2009 г.21:45

    Можно, наверное, и остальные атрибуты прикрутить, типа a[href=…].

  • Дин автор

    10 июля 2009 г.17:26

    @Kalan, да, неплохое развитие событий.

    Вместе с этим, хотелось бы видеть параллельное создание элементов, то есть когда они не только вложены друг в друга, но и находятся на одном уровне. Такое можно предусмотреть с помощью следующего синтаксиса (рассмотрим на примере):

    div#example (div.corner em a|div.another strong|div (div|span))

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

    Вышеприведённый код создаст такое локальное дерево элементов:

    <div id="example"> <div class="corner"> <em> <a /> </em> </div> <div class="another"> <strong /> </div> <div> <div /> <span /> </div> </div>

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

  • Анонимус

    03 августа 2009 г.18:52

    Что-то давно не было нового псто.

  • whitequark

    13 сентября 2009 г.19:32

    Может, имеет смысл при поиске промежуточных элементов воспользоваться XPath (если он, конечно, есть) и разбивать только по пробелам? Все же это будет быстрее.

  • Дин

    15 сентября 2009 г.08:11

    @Whitequark, не факт, что это будет быстрее. К тому же, если мы говорим о создании группы параллельных элементов, то такая тактика работать вообще не будет.

  • whitequark

    15 сентября 2009 г.17:46

    @Дин, может, и не быстрее (хотя код на Си обычно быстрее аналогичного на JS), но во всяком случае будет проще (то есть не нужно) реализовывать всякие вкусные фичи вроде атрибутов и пр. А в чем проблема с параллельными элементами? Я же предлагаю xpath'ить каждый шаг отдельно, фактически оставляя браузеру работу по разбору подселектора.

  • skyDog

    18 октября 2009 г.19:11

    Это все интересно конечно, и если двигаться дальше по пути усовершенствования таких 'utility' функций можно создать очень неплохие библиотеки. Только стоит ли огород городить, может просто подключить jQuery и такой вот код:

    $('#example').append($('<div><span class="inner"><a id="new"></a></span></div>'))
  • Дин автор

    19 октября 2009 г.12:22

    @SkyDog, иногда подключение сторонних библиотек стоит очень дорого для небольших динамических клиент-ориентированных страниц. К тому же, писать это всё в чистом HTML мне, к примеру, не нравится, а в виде селекторов это будет вполне красиво выглядеть.

  • Йура

    03 сентября 2010 г.05:18

    @skyDog, таки стоит ли огород городить, может просто:

      document.getElementById('container').innerHTML = '<div>some div <span class="inner">some span <a href="#" id="new">Some Link</a></span></div>'

    Гениально, правда?

Я тоже знаю!

Для обращения к человеку используйте символ @, после которого следует имя того, к кому обращаетесь (пробелы заменяются на знак подчёркивания). Если вам интересно, можете подписаться на комментарии по RSS или по эл. почте. Ведите себя достойно, вы же не роботы, правда?

Вы можете использовать следующие XHTML-элементы в разметке комментария: strong, em, span[class=crossline], a[href=uri], code[type=язык], blockquote, ul и ol. В качестве языка кода может быть указан, например, javascript или css.