📜 ⬆️ ⬇️

Bookmarklets: if XPath is not available, and selectors and DOM navigation methods are missing

Recently, I tried to write several conditionally cross-browser bookmarks with selections and navigation of medium complexity. I decided to limit myself to the latest versions of Google Chrome, Firefox and Internet Explorer. Having started testing in the last browser, I sadly discovered that even in IE 11 there is still no XPath support.

It seems that full support is promised in Edge : "Microsoft Edge supports the XML Path Language Version 1.0 with no variations or extensions." And even, it seems, the implementation was added to the Internet Explorer Developer Channel ( no one checked ?). But this is still not enough consolation.

(PS Verification via the virtual machine confirms the XPath implementation in MS Edge version 11.00.10240.16397 from 07.22.2015 (according to the file version), it is 20.10240.16384.0 (according to the information in the settings)).
')
The next step was to discover the library from Google . I even checked my conscience with the implantation of the library on the pages in IE 11 ( according to the method described here ) - everything works fine even on paranoid sites like Twitter (by the way, if you didn’t know, you still can’t start Bookmarklet on Twitter or, for example, in Gitkhab, due to a bug that has not yet been fixed ). But this method is very cumbersome. It is well suited for site development, but it burdens small custom bookmarklets with unnecessary asynchrony, complication of logic, and additional time to download a file.

I had to look for simpler replacements for some XPath tools I XPath .

At the same time, I tried to refrain from some useful new methods that are still not cross-browser (like Element.closest() , for which, however, there is a poly file ).

When searching for ready-made solutions to some problems, I ran across quite large chunks of code with cycles that were hard to be considered a compact replacement. Therefore, for the first time, I created a small set of small functions that I would like to propose for discussion. The fact is that I am not a professional programmer, rather a curious user, and I really would not like to invent an ugly bike. Therefore, if you know any more compact and elegant polyfills that can be used in small bookmarklets, please share. If you have ideas on how to improve these functions, I will also be grateful for the advice.

So far there are only six of them, for those specific XPath features that I lacked. Functions are not very convenient to use, it would be good for them to implement cheining (the possibility of call chains), but as far as I heard, it is not safe to extend the DOM , so I did not decide to add them to Element.prototype .

1. Substitution for /following-sibling::subject[predicate]

Let's say we have a tree of elements:

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


And we need to get from the first p to the unknown which account of the neighboring p with the required class. You can organize a loop with checks of all neighbors. And you can do everything in the conditional one touch. Create a function:

 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(); } 


And then we can call it like this:

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


Perhaps this is not the best solution in terms of speed and consumed resources (creating and traversing large temporary collections and arrays), but in terms of compactness and convenience, it seems to me tolerant. Moreover, bookmarklets are often used for small one-time actions, for which saving time and resources is not so critical.

2. Substitution for /preceding-sibling::subject[predicate]

The same in reverse order (and we will return the last element of the array, it is the closest of the previous ones):

 <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. Substitute for /following::subject[predicate]

This task seems to be more complicated than the previous one (it is not so easy to obtain a collection of elements that follow this element as a direct DOM traversal, regardless of hierarchy relationships), but the implementation of our method will be simpler, minus one condition.

 <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. Substitution for /preceding::subject[predicate]

In the opposite direction, returning the last element of the array of preceding elements:

 <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. Substitution for /ancestor-or-self::subject[predicate]

This axis is often used to find the desired event initiator rising from the bottom up, as well as for other adjustments (for example, you need to get to a certain element from the value getSelection().focusNode , since this property often corresponds to a text node). I could use the mentioned polyfil for Element.closest() , but for the sake of consistency, I added a function in the style of the previous ones.

For both cases, the function will return the same element:

 <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. Substitution for /descendant::subject[node-predicate]

This is a temporary simplified replacement for the upcoming CSS4 :has() selector, which is still not supported by any of the browsers, yep .

For example, you need to select a link that contains the code element, like this:

 <div id='point-of-view'> ... <a href='#target'> </a> ... <a href='#target'><code>     </code></a> ... <div> 


Arguments will be added, but still nothing complicated:

 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') 


If you edit this method a little (remove the final .shift() ), you can get arrays of the necessary elements, and if contextNode set document as the contextNode , then the selection will be made from the entire document.

That's all. Thank you for your time and sorry for any mistakes.

Source: https://habr.com/ru/post/262853/


All Articles