Недавно я пытался написать несколько условно кроссбраузерных букмарклетов с выборками и навигацией средней сложности. Решил ограничиться последними версиями Google Chrome, Firefox и Internet Explorer. Приступив к проверке в последнем браузере, с грустью обнаружил, что даже в IE 11 всё ещё нет поддержки
Вроде бы полная поддержка обещана в Edge: «Microsoft Edge supports the XML Path Language Version 1.0 with no variations or extensions». И уже даже, кажется, реализация добавлена в Internet Explorer Developer Channel (никто не проверял?). Но это пока недостаточное утешение.
Следующим шагом стало обнаружение библиотеки от Google. Я даже для очистки совести проверил способ с вживлением библиотеки на странички в IE 11 (по описанному здесь методу) — всё замечательно работает даже на параноидальных сайтах вроде Твиттера (к слову, если вы вдруг не знали, в Firefox всё ещё нельзя запустить букмарклет в Твиттере или, например, в Гитхабе, из-за до сих пор не исправленного бага). Но метод этот очень громоздкий. Он хорошо подходит для разработки сайтов, но маленькие пользовательские букмарклеты он отягощает лишней асинхронностью, усложнением логики и дополнительным временем на загрузку файла.
Пришлось искать более простые замены для некоторых не хватавших мне инструментов
При этом я старался воздерживаться от некоторых полезных новых методов, которые всё ещё не кроссбраузерны (вроде
При поиске готовых решений некоторых проблем я натыкался на довольно большие куски кода с циклами, которые трудно было считать компактной заменой. Поэтому я создал на первое время маленький набор небольших функций, который хотелось бы предложить для обсуждения. Дело в том, что я не профессиональный программист, скорее любопытный пользователь, и очень бы не хотелось изобретать уродливый велосипед. Поэтому, если вам известны какие-то более компактные и изящные полифилы, которые можно использовать в небольших букмарклетах, пожалуйста, поделитесь. Если будут идеи, как усовершенствовать эти функции, тоже буду благодарен за советы.
Пока их всего шесть, для тех конкретных возможностей
1. Замена для
Скажем, у нас есть дерево элементов:
И нам нужно добраться от первого
И потом можем вызывать её вот так:
Возможно, это не лучшее решение с точки зрения быстродействия и потребляемых ресурсов (создание и обход больших временных коллекций и массивов), но с точки зрения компактности и удобства, мне кажется, терпимо. Тем более что букмарклеты часто применяются для небольших однократных действий, для которых экономия времени и ресурсов не так критична.
2. Замена для
То же самое в обратном порядке (и возвращать будем уже последний элемент массива, он же ближайший из предшествующих):
3. Замена для
Это задание вроде бы сложнее предыдущего (не так легко обычными способами получить коллекцию элементов, последующих за данным элементом в порядке прямого обхода
4. Замена для
В обратном направлении, возвращая последний элемент массива предшествующих элементов:
5. Замена для
Эта ось часто используется для нахождения нужного инициатора события, поднимающегося снизу вверх, а также для других корректировок (например, нужно добраться до определённого элемента от значения
Для обоих случаев функция вернёт один и тот же элемент:
6. Замена для
Это временная упрощённая замена грядущему селектору CSS4
Например, нужно выбрать ссылку, которая содержит элемент
Аргументов прибавится, но всё равно ничего сложного:
Если немного подредактировать этот метод (убрать конечное
Вот и всё. Спасибо за потраченное время и простите за возможные ошибки.
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)
vmb Автор
17.07.2015 20:08Столкнулся с жуткими тормозами в работе функций в Твиттере. Посмотрел профайлер, обнаружил прожорливую функцию, присмотрелся к ней и понял, что допустил чудовищную ошибку. Спутал функции isEqualNode и устаревшую isSameNode, вместо которой сейчас употребляется точное сравнение объектов ( === ). Первая сравнивает два нетождественных объекта и совершает при этом кучу проверок. Отредактировал код, теперь тормозов нет. Прошу прощения.
vintage
Может лучше взять jpath и добавить надостающего?
vmb Автор
А разве jpath не для JSON? При её помощи можно и по DOM ходить?
vintage
Это другой jpath :-)
vmb Автор
Ага, посмотрел примеры. Вроде мощная маленькая штучка. Но это ведь как с гугловской библиотекой: нужно или подключать внешний файл, или включать всю библиотеку в букмарклет, или полностью изучать и заимствовать нужные функции, опять-таки созданные из подручных средств. Достаточно трудоёмкие занятия. Но спасибо, я про неё не знал, запомню.