Недавно я пытался написать несколько условно кроссбраузерных букмарклетов с выборками и навигацией средней сложности. Решил ограничиться последними версиями Google Chrome, Firefox и Internet Explorer. Приступив к проверке в последнем браузере, с грустью обнаружил, что даже в IE 11 всё ещё нет поддержки XPath.

Вроде бы полная поддержка обещана в Edge: «Microsoft Edge supports the XML Path Language Version 1.0 with no variations or extensions». И уже даже, кажется, реализация добавлена в Internet Explorer Developer Channel (никто не проверял?). Но это пока недостаточное утешение.

Следующим шагом стало обнаружение библиотеки от Google. Я даже для очистки совести проверил способ с вживлением библиотеки на странички в IE 11 (по описанному здесь методу) — всё замечательно работает даже на параноидальных сайтах вроде Твиттера (к слову, если вы вдруг не знали, в Firefox всё ещё нельзя запустить букмарклет в Твиттере или, например, в Гитхабе, из-за до сих пор не исправленного бага). Но метод этот очень громоздкий. Он хорошо подходит для разработки сайтов, но маленькие пользовательские букмарклеты он отягощает лишней асинхронностью, усложнением логики и дополнительным временем на загрузку файла.

Пришлось искать более простые замены для некоторых не хватавших мне инструментов XPath.

При этом я старался воздерживаться от некоторых полезных новых методов, которые всё ещё не кроссбраузерны (вроде Element.closest(), для которого, впрочем, есть полифил).

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

Пока их всего шесть, для тех конкретных возможностей XPath, которых мне не хватило. Функции не очень удобны в использовании, для них хорошо было бы реализовать чейнинг (возможность цепочек вызовов), но, насколько я слышал, расширять DOM небезопасно, поэтому добавлять их в Element.prototype я не решился.

1. Замена для /following-sibling::subject[predicate]

Скажем, у нас есть дерево элементов:

<div>
    ...
    <p class='foo' id='point-of-view'></p>
    <p class='bar'></p>
    ...
    <p class='target'></p>
    ...
</div>


И нам нужно добраться от первого p до неизвестно какого по счёту соседнего p с нужным классом. Можно организовать цикл с проверками всех соседей. А можно всё сделать в условное одно касание. Создаём функцию:

function findNextSibling(startNode, endSelector) {
	return [].filter.call(document.querySelectorAll(endSelector), function(el) {
				return startNode.parentNode === el.parentNode &&
					   startNode.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING;
			}).shift();
}


И потом можем вызывать её вот так:

var from = document.querySelector('#point-of-view');
var to   = findNextSibling(from, 'p.target')


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

2. Замена для /preceding-sibling::subject[predicate]

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

<div>
    ...
    <p class='target'></p>
    <p class='bar'></p>
    ...
    <p class='foo' id='point-of-view'></p>
    ...
</div>


function findPrevSibling(startNode, endSelector) {
	return [].filter.call(document.querySelectorAll(endSelector), function(el) {
				return startNode.parentNode === el.parentNode &&
					   startNode.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_PRECEDING;
			}).pop();
}


var from = document.querySelector('#point-of-view');
var to   = findPrevSibling(from, 'p.target')


3. Замена для /following::subject[predicate]

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

<div>
    ...
    <p class='foo' id='point-of-view'></p>
    <p class='bar'></p>
    ...
</div>
<div>
    ...
     <div>
        <p class='target'></p>
    </div>
    ...
</div>


function findNext(startNode, endSelector) {
	return [].filter.call(document.querySelectorAll(endSelector), function(el) {
				return startNode.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING;
			}).shift();
}


var from = document.querySelector('#point-of-view');
var to   = findNext(from, 'p.target')


4. Замена для /preceding::subject[predicate]

В обратном направлении, возвращая последний элемент массива предшествующих элементов:

<div>
    ...
     <div>
        <p class='target'></p>
    </div>
    ...
</div>
<div>
    ...
    <p class='bar'></p>
    <p class='foo' id='point-of-view'></p>
    ...
</div>


function findPrev(startNode, endSelector) {
	return [].filter.call(document.querySelectorAll(endSelector), function(el) {
				return startNode.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_PRECEDING;
			}).pop();
}


var from = document.querySelector('#point-of-view');
var to   = findPrev(from, 'p.target')


5. Замена для /ancestor-or-self::subject[predicate]

Эта ось часто используется для нахождения нужного инициатора события, поднимающегося снизу вверх, а также для других корректировок (например, нужно добраться до определённого элемента от значения getSelection().focusNode, поскольку это свойство часто соответствует текстовому узлу). Можно было бы воспользоваться упомянутым полифилом для Element.closest(), но ради единообразия я добавил функцию в стиле предыдущих.

Для обоих случаев функция вернёт один и тот же элемент:

<a href='#target'><code><b id='point-of-view'>ссылка</b></code></a>


<a href='#target' id='point-of-view'>ссылка</a>


function findClosestAncestorOrSelf(startNode, endSelector) {
	return [].filter.call(document.querySelectorAll(endSelector), function(el) {
				return startNode.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_CONTAINS ||
					   startNode === el;
			}).pop();
}


var from = document.querySelector('#point-of-view');
var to   = findClosestAncestorOrSelf(from, 'a')


6. Замена для /descendant::subject[node-predicate]

Это временная упрощённая замена грядущему селектору CSS4 :has(), который всё ещё не поддерживается ни одним из браузеров, угу.

Например, нужно выбрать ссылку, которая содержит элемент code, вот такую:

<div id='point-of-view'>
    ...
    <a href='#target'>просто ссылка</a>
    ...
    <a href='#target'><code>ссылка на объяснение свойства или метода</code></a>
    ...
<div>


Аргументов прибавится, но всё равно ничего сложного:

function findByDescendant(contextNode, subjectSelector, predicateSelector) {
	return [].filter.call(contextNode.querySelectorAll(subjectSelector), function(el) {
				return el.querySelector(predicateSelector);
			}).shift();
}


var scope  = document.querySelector('#point-of-view');
var target = findByDescendant(scope, 'a', 'code')


Если немного подредактировать этот метод (убрать конечное .shift()), им можно будет получать массивы нужных элементов, а если в качестве contextNode задавать document, то выборка будет делаться из всего документа.

Вот и всё. Спасибо за потраченное время и простите за возможные ошибки.

Комментарии (5)


  1. vintage
    17.07.2015 11:45
    +1

    Может лучше взять jpath и добавить надостающего?


    1. vmb Автор
      17.07.2015 12:04

      А разве jpath не для JSON? При её помощи можно и по DOM ходить?


      1. vintage
        17.07.2015 13:48
        +1

        Это другой jpath :-)


        1. vmb Автор
          17.07.2015 17:51

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


  1. vmb Автор
    17.07.2015 20:08

    Столкнулся с жуткими тормозами в работе функций в Твиттере. Посмотрел профайлер, обнаружил прожорливую функцию, присмотрелся к ней и понял, что допустил чудовищную ошибку. Спутал функции isEqualNode и устаревшую isSameNode, вместо которой сейчас употребляется точное сравнение объектов ( === ). Первая сравнивает два нетождественных объекта и совершает при этом кучу проверок. Отредактировал код, теперь тормозов нет. Прошу прощения.