Last active
June 6, 2024 06:35
-
-
Save camiloaa/d0ec27eb68f54ea01a5e8bb403078eac to your computer and use it in GitHub Desktop.
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
/** Wait until an specific element appears in the body section of HTML document | |
* Uses promises that are fulfilled when the element appears. | |
* | |
* Inspired by https://gist.githubusercontent.com/raw/2625891/waitForKeyElements.js | |
* | |
** How to use ** | |
* | |
* Define a callback_function that takes an HTML element as parameter | |
* callback_function = function (element) { | |
* do_something_with_found(element) | |
* } | |
* | |
* Class provides two methods: | |
* - waitForElement(callback_function): uses node.querySelector() | |
* - waitForAllElements(callback_function): uses node.querySelectorAll() | |
* All examples with waitForElement() can be used with waitForAllElements(), | |
* the difference being that waitForElement() will apply always for a single | |
* HTMLNode, while waitForAllElements() can apply to several. | |
* | |
* Example 1: Wait until an element with a specific class appears | |
* waiter = new waitForElement(".my-class") | |
* waiter.waitForElement(callback_function) | |
* | |
* Example 2: Find all elements with a specific class | |
* // All elements appear at once in the document | |
* waiter = new waitForElement(".my-class") | |
* waiter.waitForAllElements(callback_function) | |
* | |
* Example 3: Wait for elements with a specific class to appear | |
* // Elements might appear after the page is fully loaded | |
* // Use waiter.repeat attribute to keep waiting for new elements | |
* waiter = new waitForElement(".my-class") | |
* waiter.repeat = true | |
* waiter.waitForElement(callback_function) | |
* | |
* Example 4: Wait for any style section to appear. Styles appear in the <head> section | |
* waiter = new waitForElement("style", document.head) // Second parameter must have a querySelector method | |
* waiter.waitForElement(callback_function) | |
* | |
* Example 5: Limit how much time to wait | |
* waiter = new waitForElement(".my-class") | |
* waiter.timeout = 1500 // Wait 1500 ms | |
* waiter.waitForElement(callback_function) | |
* | |
* Example 6: React to changes in document.body attributes | |
* // observe <body>, search document | |
* waiter = new waitForElement(".my-class") | |
* waiter.scope = document | |
* waiter.observe_for = waitForElement.WAIT_FOR_ATTRIBUTES_ONLY | |
* waiter.waitForElement(callback_function) | |
* | |
* Example 7: Custom query selection function example: react to changes in document.body attributes | |
* // Maybe I should find a better example where custom selection is actually necessary | |
* // Function will be called from inside HTMLElement class | |
* // Use 'this' to access the observed node | |
* check_class_for_top_node = function(query_selector) { | |
* return this.classList.contains(query_selector) ? this : null // Return HTMLElement | |
* } | |
* | |
* waiter = new waitForElement("my-class") // Not a querySelector! No '.' before class name | |
* waiter.observe_for = waitForElement.WAIT_FOR_ATTRIBUTES_ONLY | |
* waiter.select_func = check_class_for_top_node | |
* waiter.waitForElement(callback_function) | |
*/ | |
class waitForElement { | |
static WAIT_FOR_CHILDREN = { | |
childList: true, | |
subtree: true | |
} | |
static WAIT_FOR_ANYTHING = { | |
childList: true, | |
subtree: true, | |
attributes: true | |
} | |
static WAIT_FOR_ATTRIBUTES_ONLY = { | |
attributes: true | |
} | |
constructor(selector, node = document.body) { | |
this.selector_array = selector.split(",").map(sel => sel.trim()); | |
this.observe_for = waitForElement.WAIT_FOR_ANYTHING; /* Any changes will be observed */ | |
this.select_func = null; /* It must return HTML element or collection | |
* It must be valid for the current node and scope */ | |
this.node = node; /* Where to set the observer */ | |
this.scope = node; /* Where to look for the selector */ | |
this.repeat = false; /* Repeat the search, otherwise fulfill the promise when we find one element matching the selector */ | |
this.repeat_times = -1; /* Limit the number of repeats. -1 means no limit */ | |
this.wait = true; /* Wait until the desired element appears */ | |
this.wait_times = -1; /* Limit the number of repeats we will wait. -1 means no limit */ | |
this.timeout = -1; /* Timeout for the promise to be fulfilled */ | |
this.negate = false; /* Fulfill the promise only when the selector returns false */ | |
this.only_new = true; /* Observe only new elements. Found items get a custom property mark. */ | |
this.timer = null; | |
this.marker_attribute = "wait-for-element"; | |
this.marker_value = "found"; | |
this.debug = false; | |
} | |
// Wait for a single element | |
async waitForElement(callback) { | |
// Fill-in missing parameters | |
this.selector = this.only_new ? | |
this.selector_array.map(sel => | |
sel + ":not(["+this.marker_attribute+"='"+this.marker_value+"'])" | |
).join(", ") : this.selector_array.join(", "); | |
if (!this.select_func) { | |
this.select_func = this.scope.querySelector; /* Default search function */ | |
} | |
if (this.wait_times > 0) { | |
this.wait = true; | |
this.wait_times--; | |
} else if (this.wait_times == 0) { | |
this.wait = false; | |
} | |
if (this.repeat_times > 0) { | |
this.repeat = true; | |
this.repeat_times--; | |
} | |
if (this.repeat_times == 0) { | |
this.repeat = false; | |
} | |
this.callback = callback; | |
this.promise = new Promise((resolve, reject) => this.resolver(resolve)); | |
if (this.timeout > 0) { | |
if (this.debug) console.debug("WaitForElements Make promise to find " + this.selector + " in " + this.timeout + "ms"); | |
let reject_promise = new Promise((resolve, reject) => this.rejecter(reject)); | |
await Promise.race([reject_promise, this.promise.then((result) => this._resolved(result))]); | |
} else { | |
if (this.debug) console.debug("WaitForElements Make promise to find " + this.selector); | |
await this.promise.then((result) => this._resolved(result)); | |
} | |
} | |
// Wait for all elements | |
waitForAllElements(callback) { | |
this.select_func = this.scope.querySelectorAll; | |
return this.waitForElement((elements) => { | |
elements.forEach(callback); | |
}) | |
} | |
resolver(resolve) { | |
this._resolve = resolve; | |
let result = this._select_func(); | |
let valid_res = this._check_result(result) | |
if (valid_res || !this.wait) { | |
if (!valid_res) { | |
this.repeat = false; | |
} | |
if (this.debug) console.debug("WaitForElements not waiting"); | |
this._mark_as_found(result); | |
return resolve(result); | |
} | |
this.observer = typeof (this.observer) === 'undefined' ? new MutationObserver(mutations => this._observe(mutations)) : this.observer; | |
if (this.debug) console.debug("WaitForElements We need to wait %o", this.observer) | |
// If you get "parameter 1 is not of type 'Node'" error, see https://stackoverflow.com/a/77855838/492336 | |
this.observer.observe(this.node, this.observe_for); | |
} | |
rejecter(reject) { | |
//this.timer = setTimeout(() => {reject(); if (this.debug) console.debug("WaitForElements: Reject")}, this.timeout); | |
this.timer = setTimeout(reject, this.timeout); | |
return this.timer; | |
} | |
_mark_as_found(element) { | |
if (!this.only_new) return; | |
if (typeof(element.setAttribute) == 'function') { | |
if (this.debug) console.debug("WaitForElements Marking node %o", element); | |
element.setAttribute(this.marker_attribute, this.marker_value); | |
} else if (typeof(element.forEach) == 'function') { | |
if (this.debug) console.debug("WaitForElements Marking node list %o", element); | |
element.forEach(elem => this._mark_as_found(elem)) | |
} else if (typeof(element.map) == 'function') { | |
if (this.debug) console.debug("WaitForElements Marking array %o", element); | |
element.map(elem => this._mark_as_found(elem)) | |
} else { | |
console.warn("WaitForElements cannot mark %o", element); | |
} | |
} | |
_select_func() { | |
if (this.debug) console.debug("WaitForElements selector %o", this.selector) | |
let result = this.select_func.call(this.scope, this.selector) | |
return result; | |
} | |
_check_result(result) { | |
let res = true; | |
if (typeof(result) == 'undefined' || result == null) { | |
res = false; | |
} else if (typeof(result) == "object" && typeof(result.length) != 'undefined') { | |
res = result.length > 0; | |
} else if (typeof(result) == 'boolean') { | |
res = result; | |
} else { | |
} | |
return this.negate ? !res : res; | |
} | |
_observe(mutations) { | |
if (this.debug) console.debug("WaitForElements %o", mutations) | |
let result = this._select_func(); | |
let valid_res = this._check_result(result) | |
if (this.debug) console.debug("WaitForElements %o", result) | |
if (valid_res) { | |
if (this.debug) console.debug("WaitForElements found %o", result) | |
this.observer.disconnect(); | |
this._mark_as_found(result); | |
if (this.debug) console.debug("WaitForElements resolving %o", this._resolve) | |
this._resolve(result); | |
} | |
} | |
_resolved(result) { | |
if (this.timer) clearTimeout(this.timer); | |
this.callback(result); | |
if (this.repeat) { | |
if (this.debug) console.debug("WaitForElements Repeating") | |
return this.waitForElement(this.callback); | |
} | |
return undefined; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment