Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Automatic javascript in-page search and highlight
// ==UserScript==
// @name Find - In-line search highlighter
// @namespace https://wol.ph
// @version 1.1
// @description Automatic javascript in-page search and highlight
// @author wolph
// @grant GM_addStyle
// @grant GM_unsafeWindow
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/mark.js/8.11.1/mark.es6.min.js
// @include *
// ==/UserScript==
/* globals Mark, jQuery, GM_config */
'use strict';
const TLD_RE = /[-\w]+\.(?:[-\w]+\.xn--[-\w]+|[-\w]{3,}|[-\w]+\.[-\w]{2})$/i
const TLD = TLD_RE.exec(window.location.host)[0];
const HOSTNAME = window.location.host;
const $ = jQuery;
const body = window.document.body;
const $body = jQuery(body);
const mark = new Mark(body)
let keywords = GM_getValue('keywords', 'default|keywords');
function saveDomains(value){
GM_setValue('domains', JSON.stringify(domains));
createDomainCommand(value);
}
function loadDomains(){
return JSON.parse(GM_getValue('domains', '{}'));
}
let domains = loadDomains();
const commands = {};
function createDomainCommand(value){
if(commands[value]){
GM_unregisterMenuCommand(commands[value]);
delete commands[value];
}
if(domains[value]){
commands[value] = GM_registerMenuCommand('Disable for ' + value, () => {
domains = loadDomains();
delete domains[value];
saveDomains(value);
if(mark)mark.unmark();
});
}else{
commands[value] = GM_registerMenuCommand('Enable for ' + value, () => {
domains = loadDomains();
domains[value] = true;
saveDomains(value);
search();
});
}
}
createDomainCommand(TLD);
if(TLD !== HOSTNAME){
createDomainCommand(HOSTNAME);
}
GM_addStyle(`
.search_results {
position: fixed;
top: 0;
right: 0;
z-index: 10000;
}
.search_results button, .search_results input, mark, mark.current{
color: #000 !important;
background: #fff !important;
}
mark {
background: yellow !important;
}
mark.current {
background: orange !important;
}
`);
;
GM_registerMenuCommand('Set search query', () => {
keywords = GM_getValue('keywords', 'default|keywords');
var new_keywords = prompt('Set the search query regular expression (separate keywords by | )', keywords);
if(new_keywords !== null){
GM_setValue('keywords', new_keywords);
keywords = new_keywords;
search();
}
});
function search(){
$body.append(`
<div class="search_results">
<button data-search="prev">&uarr;</button>
<input type="text" data-search="currentIndex" value="0">
<button data-search="total">0</button>
<button data-search="next">&darr;</button>
</div>
`);
var $prevBtn = $('button[data-search="prev"]'),
// next button
$nextBtn = $('button[data-search="next"]'),
// the context where to search
$currentIndexInput = $("input[data-search='currentIndex']"),
$totalButton = $("button[data-search='total']"),
$content = $('.content'),
// jQuery object to save <mark> elements
$results,
// the class that will be appended to the current
// focused element
currentClass = 'current',
// top offset for the jump (the search bar)
offsetTop = 100,
// the current index of the focused element
currentIndex = 0,
// store the page height so we know when to search again with autoscroll
pageHeight = document.body.clientHeight,
// current scroll offset
scrollY = window.scrollY
;
// Jumps to the element matching the currentIndex
function jumpTo(disableScrolling) {
if ($results.length) {
var position,
$current = $results.eq(currentIndex);
$results.removeClass(currentClass);
$currentIndexInput.val(currentIndex);
if ($current.length) {
$current.addClass(currentClass);
position = $current.offset().top - offsetTop;
if(disableScrolling !== true){
window.scrollTo(0, position);
}
}
}
}
// Jump to next/previous result on click
$nextBtn.add($prevBtn).on("click", function() {
if ($results.length) {
currentIndex += $(this).is($prevBtn) ? -1 : 1;
if (currentIndex < 0) {
currentIndex = $results.length - 1;
}
if (currentIndex > $results.length - 1) {
currentIndex = 0;
}
jumpTo();
}
});
$currentIndexInput.on('change', value => {
currentIndex = 1 * value;
jumpTo();
});
$totalButton.on('click', search);
const query = new RegExp('\\b(' + keywords + ')\\w*', 'i');
mark.unmark();
mark.markRegExp(query, {
separateWordSearch: true,
done: function() {
$results = $body.find("mark");
unsafeWindow.results = $results;
currentIndex = 0;
$totalButton.text($results.length);
}
});
function isVisible(el) {
var rect = el.getBoundingClientRect();
return (rect.top - offsetTop) >= 0 && rect.bottom <= window.innerHeight;
}
// Setup a timer
var scrollTimeout;
// Listen for resize events once every animation frame to prevent locking
// up the CPU
window.addEventListener('scroll', function (event) {
// If there's a timer, cancel it
if (scrollTimeout) {
window.cancelAnimationFrame(scrollTimeout);
}
// Setup the new requestAnimationFrame()
scrollTimeout = window.requestAnimationFrame(function () {
let oldScrollY = scrollY;
scrollY = window.scrollY;
if(pageHeight != document.body.clientHeight){
pageHeight != document.body.clientHeight;
search();
}
/* If the current item is still visible, don't change the index */
if($results.length > 0 && isVisible($results[currentIndex])){
jumpTo(true);
return;
}
/* search for the item currently visible, could be replaced with a
binary search if performance becomes an issue */
let i;
if(scrollY > oldScrollY){
for(i=currentIndex; i<$results.length; i++){
if(isVisible($results[i])){
currentIndex = i;
jumpTo(true);
break;
}
}
}else{
for(i=currentIndex; i>0; i--){
if(isVisible($results[i])){
currentIndex = i;
jumpTo(true);
break;
}
}
}
});
}, false);
// export variables to page for debugging
unsafeWindow.Mark = Mark;
unsafeWindow.mark = mark;
unsafeWindow.jQuery = jQuery;
}
if(domains[TLD] || domains[HOSTNAME]){
search();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.