Last active
June 17, 2022 17:55
-
-
Save kitsune7/cd317ed0bda4e96b81febaf11b188d6d to your computer and use it in GitHub Desktop.
Tampermonkey/Greasemonkey utility functions
This file contains hidden or 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
/* | |
* ---------- 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