Skip to content

Instantly share code, notes, and snippets.

@kitsune7
Last active June 17, 2022 17:55
Show Gist options
  • Save kitsune7/cd317ed0bda4e96b81febaf11b188d6d to your computer and use it in GitHub Desktop.
Save kitsune7/cd317ed0bda4e96b81febaf11b188d6d to your computer and use it in GitHub Desktop.
Tampermonkey/Greasemonkey utility functions
/*
* ---------- Utility functions ----------
* addHotkeysToElement: (element: HTMLElement, keypressToAction: Record<string, (eventTarget?: HTMLElement) => void>)
*
* - Adds a `style` tag with the given CSS to the DOM. It will put it in `head` by default.
* addStyles: (cssString: string, parentSelector: string) => void
*
* appendChild: (
* selector: string,
* child: HTMLElement | string,
* selectorFunction: (selector?: string) => HTMLElement | undefined
* ) => void
*
* - Triggers a mouse click on the element as soon as it's available in the DOM.
* clickWhenReady: (selector: string, selectorFunction: (selector?: string) => HTMLElement | undefined) => void
*
* copyElementTextToClipboard: (inputSelector: string) => void
*
* createElement: (
* element: string,
* props?: Record<string, string> | null,
* children?: HTMLElement | HTMLElement[] | string | null
* ) => HTMLElement
*
* findElementByText: (searchText: string, rootElement?: HTMLElement) => HTMLElement | undefined
*
* findElementsByText: (searchText: string, rootElement?: HTMLElement) => HTMLElement[]
*
* getTextNodes: (rootElement: HTMLElement) => Text[]
* - Removes an element as soon as it shows up in the DOM.
* removeWhenReady: (selector: string, selectorFunction: (selector?: string) => HTMLElement | undefined) => void
*
* setInputValue: (inputElement: HTMLInputElement, value: string) => void;
*
* - This (efficiently) watches for when a selector is available in the DOM and executes the given function as soon as it is.
* - It's best used for times when an element won't always be immediately visible on the page for one reason or another.
* setReadyHandler: (
* selector: string,
* readyFunction: (targetElement: HTMLElement) => void
* selectorFunction: (selector?: string) => HTMLElement | undefined
* ) => void
*
* setTextareaValue: (textareaElement: HTMLTextAreaElement, value: string) => void;
*/
'use strict';
function addHotkeysToElement(element, keypressToAction) {
element.addEventListener('keydown', function handleKeypress(event) {
const keypresses = Object.keys(keypressToAction);
keypresses.forEach(keypress => {
if (isKeypress(keypress, event)) {
event.preventDefault()
keypressToAction[keypress](event.target)
}
})
})
function isKeypress(keypress, event) {
const keyToModifier = {
'ctrl': 'Control',
'control': 'Control',
'command': 'Meta',
'cmd': 'Meta',
'win': 'OS',
'winkey': 'OS',
'alt': 'Alt',
'option': 'Alt',
'shift': 'Shift'
};
const validModifiers = Object.keys(keyToModifier);
const keys = keypress.toLowerCase().split('+').map(key => key.trim())
while (keys.length) {
const key = keys.at(-1)
if (validModifiers.includes(key)) {
if (!event.getModifierState(keyToModifier[key])) {
return false;
}
} else if (event.key.toLowerCase() !== key) {
return false;
}
keys.pop()
}
return true;
}
}
function addStyles(cssString, parentSelector='head') {
const styleElement = document.createElement('style');
styleElement.type = 'text/css';
if (styleElement.styleSheet) {
styleElement.styleSheet.cssText = cssString;
} else {
styleElement.appendChild(document.createTextNode(cssString));
}
document.querySelector(parentSelector).appendChild(styleElement);
}
function appendChild(selector, child, selectorFunction = document.querySelector) {
selectorFunction(selector).appendChild(child);
}
function clickWhenReady(selector, selectorFunction) {
setReadyHandler(selector, (element) => element.click(), selectorFunction);
}
function copyElementTextToClipboard(inputSelector) {
const copyText = document.querySelector(inputSelector);
/* Select the text field */
copyText.select();
copyText.setSelectionRange(0, 99999); /* For mobile devices */
/* Copy the text inside the text field */
document.execCommand("copy");
}
function createElement(element, props=null, children=null) {
const newElement = document.createElement(element);
function addAttribute(name) {
const attribute = document.createAttribute(name);
attribute.value = props?.[name];
newElement.setAttributeNode(attribute);
}
for (const attribute in props) {
addAttribute(attribute);
}
if (children) {
if (typeof children === 'string') {
newElement.appendChild(document.createTextNode(children));
} else if (children?.length) { // HTMLElement[]
children.forEach(child => newElement.appendChild(child));
} else { // HTMLElement
newElement.appendChild(children);
}
}
return newElement;
}
function findElementByText(searchText = '', rootElement) {
return getTextNodes(rootElement).find(node => node.data.includes(searchText))?.parentNode;
}
function findElementsByText(searchText = '', rootElement) {
return getTextNodes(rootElement)
.filter(node => node.data.includes(searchText))
.map(node => node?.parentNode);
}
function getTextNodes(rootElement = document.body) {
const textNodes = [];
const nodeIterator = document.createNodeIterator(
rootElement,
NodeFilter.SHOW_TEXT,
null
);
let node;
while ((node = nodeIterator.nextNode())) {
textNodes.push(node);
}
return textNodes;
}
function removeWhenReady(selector, selectorFunction) {
setReadyHandler(selector, (element) => element.remove(), selectorFunction);
}
function setInputValue(inputElement, value) {
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
nativeInputValueSetter.call(inputElement, value);
const event = new Event('input', { bubbles: true});
inputElement.dispatchEvent(event);
}
function setReadyHandler(
selector = '*',
readyFunction,
selectorFunction = document.querySelector.bind(document)
) {
function runReadyHandler() {
const element = selectorFunction(selector);
if (element) {
readyFunction(element);
}
}
const nodeInsertedAnimation = `
@keyframes nodeInserted {
from {
outline-color: #fff;
}
to {
outline-color: #000;
}
}
${selector} {
animation-duration: 0.01s;
animation-name: nodeInserted;
}
`;
addStyles(nodeInsertedAnimation);
function handleNodeInsertion(event) {
if (event.animationName === 'nodeInserted') {
runReadyHandler();
}
}
document.addEventListener('animationstart', handleNodeInsertion, false);
if (document.querySelector(selector)) {
runReadyHandler();
}
}
function setTextareaValue(textareaElement, value) {
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set;
nativeInputValueSetter.call(textareaElement, value);
const event = new Event('input', { bubbles: true});
textareaElement.dispatchEvent(event);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment