Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs

name: Node.js CI

on:
pull_request:

jobs:
build:
runs-on: ubuntu-latest

strategy:
matrix:
node-version: [16.x, 18.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/

steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm run build --if-present
- run: npm test
43 changes: 43 additions & 0 deletions _Utils/Observer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* whenElementAppears() - A reusable, single-line MutationObserver utility function
*
* Sets up a mutation observer to watch for the *first* appearance of a specified selector in the DOM.
*
* When the selector is found, the provided callback function is invoked with the detected element as a parameter.
*
* @version 0.0.2
*
* @param {string} selector - CSS selector to be monitored
* @param {function(Node)} fn - Callback function to invoke when the selector is observed.
* @param {Element} [observationTarget=document] - Optional node to observe for the appearance of the selector. If not provided, the entire DOM will be observed.
* @param {Number} [timeout] - Optional timeout in ms to disable observer. If set to 0, mutationobserver will continue to run until the selector is seen.
* @returns {void}
*/
function whenElementAppears(selector, fn, observationTarget = document, timeout = 0) {
const observer = new MutationObserver(function (mutations, mutationInstance) {

// TBD: should an existing selector yield an immediate invocation and not set up an observer?

const record = mutations.find(mutation => mutation.target.querySelector(selector))
if (record) {
try {
const target = record.target.querySelector(selector)
console.debug(`whenElementAppears: observer of ${selector} invoking callback`)
fn(target)
mutationInstance.disconnect()
} catch (error) {
console.error('whenElementAppears: unexpected error...', error)
}
}
})
observer.observe(observationTarget, {
childList: true,
subtree: true,
})
if (timeout) {
setTimeout(() => { observer.disconnect(); console.log("Mutation Observer disconnected") }, timeout)
}
};

module.exports = whenElementAppears;

75 changes: 75 additions & 0 deletions _Utils/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# _Utils
Utility functions for reuse in userscripts

This code is vanilla JS and imports no runtime dependencies

## Observer.js

Provides whenElementAppears() - A reusable, single-line MutationObserver utility function

Allows scripts to wait for a specific selector to appear on the page prior to running a provided callback function. Whether due to lazy loading, or a high-latency script download.

Replaces the numerous lines of fairly boilerplate code which is otherwise required to set up a single-mutation observer, a better technique than the less reliable timeout or interval implementation a developer would otherwise use as a hack.

Parameters:
@param {string} selector - CSS selector to be monitored
@param {function} fn - The callback function to invoke the selector is observed.
@param {DOM element} [observationTarget=document] - Optional node to observe for the appearance of the selector. If not provided, the entire DOM will be observed. Limiting the scope can yield better performance.
@returns {void}

Employs a [MutationObserver (MDN docs)](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) to monitor DOM changes. See the documentation to understand the implementation.

### Usage

1. Import the code by adding the following to your userscript header:

// @require https://raw.githubusercontent.com/mkazin/OhMonkey/main/_Utils/Observer.js
1. Pass delayed-execution code as a callback to the `whenElementAppears()`

Example: see my [YoutubeAutoSpeed.user.js](../Google/YoutubeAutoSpeed.user.js) userscript where this was first developed.

The function's original code was:

const container = document.querySelector("div#start")
const button = document.createElement("button")
button.onclick = () => {
setSpeed(1.0)
button.textContent = `Speed = 1.0x`
}
button.textContent = `Speed = ${currentSpeed}x`
container.appendChild(button)

To delay the execution until after the `container` element appears in the DOM, it was wrapped with the following code which passes in that element:

whenElementAppears("div#start", (container) => {
const button = document.createElement("button")
...
container.appendChild(button)
}

### Development / Contributions
Pull requests, bug reports, and suggestions are welcome.

The dev environment is NodeJS. See [package.json](../package.json) in the root folder which includes the three dependencies used in testing (jest, jest-environment-jsdom, jsdom).

Use the `npm run` commands: `test`, `watch`, and `coverage`

Pull requests should pass the existing [unit tests](/__tests__/) and have additional tests to cover updates.

### Security, Forward-compatablity
Due to the nature of git repositories, the content of the above imported link can change, including changes which may break your code, or even mallicious code if the repository is compromised. The utility might even be moved to a different repository.

To avoid this you may lock your code to a specific version by using a commit ID rather than the "main" branch name.

### Possible future development

This implementation serves several possible use-cases, even allowing for having multiple callbacks run on the same selector. That implementation may benefit from an upgrade which reuses the MutationObserver object. It may not be adequate for some more advanced use-cases.

I may decide to switch this function (or add another one) to return a Promise. For one this would move error handling to be performed by the caller.
I expect code using it would also look better, especially as it's currently taking a function as a second parameter. For example:

.then(waitForSelector("#desiredElement"))
.then(desiredElement => ...)

Timeouts are another possible addition to allow developers to improve user experience when the code will not execute, such as running some fallback or cleanup code.

Loading
Loading