Недавно я пытался написать несколько условно кроссбраузерных букмарклетов с выборками и навигацией средней сложности. Решил ограничиться последними версиями 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 Array.prototype.slice.call(document.querySelectorAll(endSelector)) .filter(function(el){ return startNode.parentNode.isEqualNode(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 Array.prototype.slice.call(document.querySelectorAll(endSelector)) .filter(function(el){ return startNode.parentNode.isEqualNode(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 Array.prototype.slice.call(document.querySelectorAll(endSelector)) .filter(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 Array.prototype.slice.call(document.querySelectorAll(endSelector)) .filter(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 Array.prototype.slice.call(document.querySelectorAll(endSelector)) .filter(function(el){ return startNode.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_CONTAINS || startNode.isEqualNode(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 Array.prototype.slice.call(contextNode.querySelectorAll(subjectSelector)) .filter(function(el){ return el.querySelector(predicateSelector); }).shift(); }
var scope = document.querySelector('#point-of-view'); var target = findByDescendant(scope, 'a', 'code')
Если немного подредактировать этот метод (убрать конечное .shift()
), им можно будет получать массивы нужных элементов, а если в качестве contextNode
задавать document
, то выборка будет делаться из всего документа.
Вот и всё. Спасибо за потраченное время и простите за возможные ошибки.
ссылка на оригинал статьи http://habrahabr.ru/post/262853/
Добавить комментарий