Skip to content

Instantly share code, notes, and snippets.

@camiloaa
Last active June 6, 2024 06:35
Show Gist options
  • Save camiloaa/d0ec27eb68f54ea01a5e8bb403078eac to your computer and use it in GitHub Desktop.
Save camiloaa/d0ec27eb68f54ea01a5e8bb403078eac to your computer and use it in GitHub Desktop.
/** 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