Last active
July 10, 2023 10:45
-
-
Save wolph/18bf7dcaa24fe24991b7ecfc2be7793d to your computer and use it in GitHub Desktop.
Automatic javascript in-page search and highlight
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Find - In-line search highlighter | |
// @namespace https://wol.ph | |
// @version 1.2 | |
// @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">↑</button> | |
<input type="text" data-search="currentIndex" value="0"> | |
<button data-search="total">0</button> | |
<button data-search="next">↓</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