Skip to content

Instantly share code, notes, and snippets.

@sidneys
Last active July 4, 2023 06:16
Show Gist options
  • Save sidneys/ae661c222ebf6ce4cd9c7b4235f1dcc2 to your computer and use it in GitHub Desktop.
Save sidneys/ae661c222ebf6ce4cd9c7b4235f1dcc2 to your computer and use it in GitHub Desktop.
Greasemonkey | waitForKeyElements-ES6
// ==UserScript==
// @name waitForKeyElements 2018 Userscript Library
// @namespace de.sidneys.userscripts
// @homepage https://gist.githubusercontent.com/sidneys/ae661c222ebf6ce4cd9c7b4235f1dcc2/raw/
// @version 28.1.0
// @description High-performance 2018 version of 'waitForKeyElements' based on requestAnimationFrame. Detect dynamically inserted page elements from within Greasemonkey scripts. Fully ES6, no JQuery or other frameworks required.
// @author sidneys
// @icon https://www.greasespot.net/favicon.ico
// @include http*://*/*
// @grant unsafeWindow
// ==/UserScript==
/**
* Delay
* @constant
*/
const delay = 100
/**
* Unique identifier
* @constant
*/
const uuid = `wfke_${(GM_info.script.name).replace(/[^a-zA-Z0-9-_.]/gi, '').toLowerCase()}_${Math.random().toString(36).substring(2, 15)}`
/**
* Request id of requestAnimationFrame
* @global
*/
let requestId = unsafeWindow.requestId || null
unsafeWindow.requestId = requestId
/**
* Selector dictionary
* @global
*/
const selectorMap = unsafeWindow.selectorMap || {}
unsafeWindow.selectorMap = selectorMap
/**
* didStartLookup
* @global
*/
let didStartLookup = false
unsafeWindow.didStartLookup = didStartLookup
/**
* Has started lookup
* @returns {Boolean} - Yes/No
*/
let hasStartedLookup = () => unsafeWindow.didStartLookup
/**
* Stop lookup
*/
let stopLookup = () => {
window.cancelAnimationFrame(unsafeWindow.requestId)
unsafeWindow.requestId = null
delete unsafeWindow.requestId
}
/**
* Register selector
* @param {String} selector - CSS selector of elements to search / monitor ('.comment')
* @param {function} callback - Callback executed on element detection (called with element as argument)
* @param {Boolean=} findOnce - Stop lookup after the last currently available element has been found
* @param {String=} targetSelector - CSS selector of nested element to limit search to ('#footer')
*/
let registerSelector = (selector, callback, findOnce, targetSelector) => {
unsafeWindow.selectorMap[selector] = {
callback,
findOnce,
targetSelector
}
// DEBUG
console.debug('[waitForKeyElements]', `[${selector}]`, 'selector added')
}
/**
* Register selector
* @param {String} selector - CSS selector
* @returns {Boolean} - Yes/No
*/
let hasRegisteredSelector = (selector) => unsafeWindow.selectorMap[selector]
/**
* Unregister selector
* @param {String} selector - CSS selector
*/
let unregisterSelector = (selector) => {
unsafeWindow.selectorMap[selector] = null
delete unsafeWindow.selectorMap[selector]
// DEBUG
// console.log('[waitForKeyElements]', `[${selector}]`, 'selector complete');
// Last selector? Cancel requestAnimationFrame
if (Object.keys(unsafeWindow.selectorMap).length === 0) {
stopLookup()
// DEBUG
console.debug('[waitForKeyElements]', 'all selectors complete')
}
}
/**
* waitForKeyElements
*
* @param {String} selector - CSS selector of elements to search / monitor ('.comment')
* @param {function} callback - Callback executed on element detection (called with element as argument)
* @param {Boolean=} findOnce - Stop lookup after the last currently available element has been found
* @param {String=} targetSelector - CSS selector of nested element to limit search to ('#footer')
*
* @example
* waitForKeyElements('.item', (el) => {
* el.innerText = 'new .item detected';
* });
*
* @example
* waitForKeyElements('img', (el) => {
* console.log('First <img> detected:', el);
* }, true);
*/
let waitForKeyElements = (selector, callback, findOnce = false, targetSelector = '') => {
// Init
let elementList
let didFindNewElement = false
// Find elements
if (Boolean(targetSelector)) {
elementList = document.querySelector(targetSelector).querySelectorAll(selector) || []
} else {
elementList = document.querySelectorAll(selector) || []
}
// Is element new?
if (elementList.length > 0) {
elementList.forEach((element) => {
// Add <data> attribute to newly discovered nodes
if (element.dataset[uuid] !== 'found') {
element.dataset[uuid] = 'found'
didFindNewElement = true
// Callback
callback(element)
// DEBUG
console.debug('[waitForKeyElements]', `[${selector}"]`, 'element found')
}
})
}
// Register selector
if (!hasRegisteredSelector(selector)) {
registerSelector(selector, callback, findOnce, targetSelector)
}
// Running once? Check if element found
if (hasRegisteredSelector(selector) && findOnce && didFindNewElement) {
unregisterSelector(selector)
} else {
if (!hasStartedLookup()) {
unsafeWindow.didStartLookup = true
let lastTime = Date.now()
let lookupLoop = () => {
if (Date.now() >= (lastTime + delay)) {
lastTime = Date.now()
// Loop through registered selectors
Object.keys(unsafeWindow.selectorMap).forEach((selector) => {
waitForKeyElements(selector, unsafeWindow.selectorMap[selector].callback, unsafeWindow.selectorMap[selector].findOnce, unsafeWindow.selectorMap[selector].targetSelector)
})
// Verbose
// console.debug('[waitForKeyElements]', lastTime, `[${selector}]`, 'lookup cycle complete');
}
unsafeWindow.requestId = window.requestAnimationFrame(lookupLoop)
}
lookupLoop()
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment