Букмарклет: якщо XPath недоступний, а селекторів і методів навігації по DOM не вистачає

Нещодавно я намагався написати кілька умовно кроссбраузерных букмарклет з вибірками і навігацією середньої складності. Вирішив обмежитися останніми версіями 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
, то вибірка буде робитися з усього документа.

От і все. Спасибі за витрачений час і вибачте за можливі помилки.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.