Base library for my scripts
Tính đến
Script này sẽ không được không được cài đặt trực tiếp. Nó là một thư viện cho các script khác để bao gồm các chỉ thị meta
// @require https://update.greasyfork.org/scripts/411993/851645/grrrr.js
// ==UserScript==
// @name grrrr
// @namespace brazenvoid
// @version 1.0
// @author brazenvoid
// @license GPL-3.0-only
// @description Base library for my scripts
// @grant GM_addStyle
// @run-at document-end
// ==/UserScript==
/**
* @function GM_addStyle
* @param {string} style
*/
GM_addStyle(
`@keyframes fadeEffect{from{opacity:0}to{opacity:1}}button.form-button{padding:0 5px;width:100%}button.show-settings{background-color:#000000;border:0;margin:2px 5px;padding:2px 5px;width:100%}button.show-settings.fixed{color:#ffffff;font-size:.7rem;left:0;height:90vh;margin:0;padding:0;position:fixed;top:5vh;width:.1vw;writing-mode:sideways-lr;z-index:999}button.tab-button{background-color:#808080;border:1px solid #000000;border-bottom:0;border-top-left-radius:3px;border-top-right-radius:3px;cursor:pointer;float:left;outline:none;padding:5px 10px;transition:.3s}button.tab-button:hover{background-color:#fff}button.tab-button.active{background-color:#fff;display:block}div.form-actions{text-align:center}div.form-actions button.form-button{padding:0 15px;width:auto}div.form-actions-wrapper{display:inline-flex}div.form-actions-wrapper > div.form-group + *{margin-left:15px}div.form-group{min-height:15px;padding:4px 0}div.form-group.form-range-input-group > input{padding:0 5px;width:70px}div.form-group.form-range-input-group > input + input{margin-right:5px}div.form-section{text-align:center;solid #000000}div.form-section button + button{margin-left:5px}div.form-section label.title{display:block;height:20px;width:100%}div.form-section button.form-button{width:auto}div.tab-panel{animation:fadeEffect 1s;border:1px solid #000000;display:none;padding:5px 10px}div.tab-panel.active{display:block}div.tabs-nav{overflow:hidden}div.tabs-section{margin-bottom:5px}hr{margin:3px}input.form-input{height:18px;text-align:center}input.form-input.check-radio-input{float:left;margin-right:5px}input.form-input.regular-input{float:right;width:100px}label.form-label{color:#ffffff,padding:2px 0}label.form-label.regular-input{float:left}label.form-label.check-radio-input{float:left}label.form-stat-label{float:right;padding:2px 0}section.form-section{color:#ffffff;font-size:12px;font-weight:700;position:fixed;left:0;padding:5px 10px;z-index:1000000}select.form-dropdown{float:right;height:18px;text-align:center;width:100px}textarea.form-input{display:block;height:auto;position:relative;width:98%}`)
/**
* @param milliseconds
* @return {Promise<*>}
*/
const sleep = (milliseconds) => {
return new Promise(resolve => setTimeout(resolve, milliseconds))
}
/**
* @param {string} text
* @return {string}
*/
function toKebabCase (text)
{
return text.toLowerCase().replace(' ', '-')
}
class ChildObserver
{
/**
* @callback observerOnMutation
* @param {NodeList} nodes
*/
/**
* @return {ChildObserver}
*/
static create ()
{
return new ChildObserver
}
/**
* ChildObserver constructor
*/
constructor ()
{
this._node = null
this._observer = null
this._onNodesAdded = null
this._onNodesRemoved = null
}
/**
* @return {ChildObserver}
* @private
*/
_observeNodes ()
{
this._observer.observe(this._node, {childList: true})
return this
}
/**
* Attach an observer to the specified node(s)
* @param {Node} node
* @returns {ChildObserver}
*/
observe (node)
{
this._node = node
this._observer = new MutationObserver((mutations) => {
for (let mutation of mutations) {
if (mutation.addedNodes.length && this._onNodesAdded !== null) {
this._onNodesAdded(
mutation.addedNodes,
mutation.previousSibling,
mutation.nextSibling,
mutation.target,
)
}
if (mutation.removedNodes.length && this._onNodesRemoved !== null) {
this._onNodesRemoved(
mutation.removedNodes,
mutation.previousSibling,
mutation.nextSibling,
mutation.target,
)
}
}
})
return this._observeNodes()
}
/**
* @param {observerOnMutation} eventHandler
* @returns {ChildObserver}
*/
onNodesAdded (eventHandler)
{
this._onNodesAdded = eventHandler
return this
}
/**
* @param {observerOnMutation} eventHandler
* @returns {ChildObserver}
*/
onNodesRemoved (eventHandler)
{
this._onNodesRemoved = eventHandler
return this
}
pauseObservation ()
{
this._observer.disconnect()
}
resumeObservation ()
{
this._observeNodes()
}
}
class LocalStore
{
/**
* @callback storeEventHandler
* @param {Object} store
*/
/**
* @param {string} scriptPrefix
* @param {Object} defaults
* @return {LocalStore}
*/
static createGlobalConfigStore (scriptPrefix, defaults)
{
return new LocalStore(scriptPrefix + 'globals', defaults)
}
static createPresetConfigStore (scriptPrefix, defaults)
{
return new LocalStore(scriptPrefix + 'presets', [
{
name: 'default',
config: defaults,
},
])
}
/**
* @param {string} key
* @param {Object} defaults
*/
constructor (key, defaults)
{
/**
* @type {string}
* @private
*/
this._key = key
/**
* @type {Object}
* @private
*/
this._store = {}
/**
* @type {string}
* @private
*/
this._defaults = this._toJSON(defaults)
/**
* @type {storeEventHandler}
*/
this._onChange = null
}
/**
* @param {string} json
* @return {Object}
* @private
*/
_fromJSON (json)
{
/** @type {{arrays: Object, objects: Object, properties: Object}} */
let parsedJSON = JSON.parse(json)
let arrayObject = {}
let store = {}
for (let property in parsedJSON.arrays) {
arrayObject = JSON.parse(parsedJSON.arrays[property])
store[property] = []
for (let key in arrayObject) {
store[property].push(arrayObject[key])
}
}
for (let property in parsedJSON.objects) {
store[property] = this._fromJSON(parsedJSON.objects[property])
}
for (let property in parsedJSON.properties) {
store[property] = parsedJSON.properties[property]
}
return store
}
/**
* @return {string}
* @private
*/
_getStore ()
{
return window.localStorage.getItem(this._key)
}
/**
* @return {Object}
* @private
*/
_getDefaults ()
{
return this._fromJSON(this._defaults)
}
/**
* @param {Object} store
* @return {string}
* @private
*/
_toJSON (store)
{
let arrayToObject = {}
let json = {arrays: {}, objects: {}, properties: {}}
for (let property in store) {
if (typeof store[property] === 'object') {
if (Array.isArray(store[property])) {
for (let key in store[property]) {
arrayToObject[key] = store[property][key]
}
json.arrays[property] = JSON.stringify(arrayToObject)
} else {
json.objects[property] = this._toJSON(store[property])
}
} else {
json.properties[property] = store[property]
}
}
return JSON.stringify(json)
}
_handleOnChange ()
{
if (this._onChange !== null) {
this._onChange(this._store)
}
}
/**
* @return {LocalStore}
*/
delete ()
{
window.localStorage.removeItem(this._key)
return this
}
/**
* @return {*}
*/
get ()
{
return this._store
}
/**
* @return {boolean}
*/
isPurged ()
{
return this._getStore() === null
}
/**
* @param {storeEventHandler} handler
* @return {LocalStore}
*/
onChange (handler)
{
this._onChange = handler
return this
}
/**
* @return {LocalStore}
*/
restoreDefaults ()
{
this._store = this._getDefaults()
this._handleOnChange()
return this
}
/**
* @return {LocalStore}
*/
retrieve ()
{
let storedStore = this._getStore()
if (storedStore === null) {
this.restoreDefaults()
} else {
this._store = this._fromJSON(storedStore)
}
this._handleOnChange()
return this
}
/**
* @return {LocalStore}
*/
save ()
{
window.localStorage.setItem(this._key, this._toJSON(this._store))
this._handleOnChange()
return this
}
/**
* @param {*} data
* @return {LocalStore}
*/
update (data)
{
this._store = data
return this.save()
}
}
class SelectorGenerator
{
/**
* @param {string} selectorPrefix
*/
constructor (selectorPrefix)
{
/**
* @type {string}
* @private
*/
this._prefix = selectorPrefix
}
/**
* @param {string} selector
* @return {string}
*/
getSelector (selector)
{
return this._prefix + selector
}
/**
* @param {string} settingName
* @return {string}
*/
getSettingsInputSelector (settingName)
{
return this.getSelector(toKebabCase(settingName) + '-setting')
}
/**
* @param {string} settingName
* @param {boolean} getMinInputSelector
* @return {string}
*/
getSettingsRangeInputSelector (settingName, getMinInputSelector)
{
return this.getSelector(toKebabCase(settingName) + (getMinInputSelector ? '-min' : '-max') + '-setting')
}
/**
* @param {string} statisticType
* @return {string}
*/
getStatLabelSelector (statisticType)
{
return this.getSelector(toKebabCase(statisticType) + '-stat')
}
}
class StatisticsRecorder
{
/**
* @param {string} selectorPrefix
*/
constructor (selectorPrefix)
{
/**
* @type {SelectorGenerator}
* @private
*/
this._selectorGenerator = new SelectorGenerator(selectorPrefix)
/**
* @type {{Total: number}}
* @private
*/
this._statistics = {Total: 0}
}
/**
* @param {string} statisticType
* @param {boolean} validationResult
* @param {number} value
*/
record (statisticType, validationResult, value = 1)
{
if (!validationResult) {
if (typeof this._statistics[statisticType] !== 'undefined') {
this._statistics[statisticType] += value
} else {
this._statistics[statisticType] = value
}
this._statistics.Total += value
}
}
reset ()
{
for (const statisticType in this._statistics) {
this._statistics[statisticType] = 0
}
}
updateUI ()
{
let label, labelSelector
for (const statisticType in this._statistics) {
labelSelector = this._selectorGenerator.getStatLabelSelector(statisticType)
label = document.getElementById(labelSelector)
if (label !== null) {
label.textContent = this._statistics[statisticType]
}
}
}
}
class UIGenerator
{
/**
* @param {HTMLElement|Node} node
*/
static appendToBody (node)
{
document.getElementsByTagName('body')[0].appendChild(node)
}
/**
* @param {HTMLElement} node
* @param {HTMLElement[]} children
* @return {HTMLElement}
*/
static populateChildren (node, children)
{
for (let child of children) {
node.appendChild(child)
}
return node
}
/**
* @param {boolean} showUI
* @param {string} selectorPrefix
*/
constructor (showUI, selectorPrefix)
{
/**
* @type {*}
* @private
*/
this._buttonBackroundColor = null
/**
* @type {HTMLElement}
* @private
*/
this._section = null
/**
* @type {SelectorGenerator}
* @private
*/
this._selectorGenerator = new SelectorGenerator(selectorPrefix)
/**
* @type {string}
* @private
*/
this._selectorPrefix = selectorPrefix
/**
* @type {boolean}
* @private
*/
this._showUI = showUI
/**
* @type {HTMLLabelElement}
* @private
*/
this._statusLine = null
/**
* @type {string}
* @private
*/
this._statusText = ''
}
/**
* @param {HTMLElement} node
* @param {string} text
* @return {this}
* @private
*/
_addHelpTextOnHover (node, text)
{
node.addEventListener('mouseover', () => this.updateStatus(text, true))
node.addEventListener('mouseout', () => this.resetStatus())
}
/**
* @param {HTMLElement[]} children
* @return {HTMLElement}
*/
addSectionChildren (children)
{
return UIGenerator.populateChildren(this._section, children)
}
/**
* @return {HTMLBRElement}
*/
createBreakSeparator ()
{
return document.createElement('br')
}
/**
* @param {HTMLElement[]} children
* @return {HTMLDivElement}
*/
createFormActions (children)
{
let wrapperDiv = document.createElement('div')
wrapperDiv.classList.add('form-actions-wrapper')
UIGenerator.populateChildren(wrapperDiv, children)
let formActionsDiv = document.createElement('div')
formActionsDiv.classList.add('form-actions')
formActionsDiv.appendChild(wrapperDiv)
return formActionsDiv
}
/**
* @param {string} caption
* @param {EventListenerOrEventListenerObject} onClick
* @param {string} hoverHelp
* @return {HTMLButtonElement}
*/
createFormButton (caption, onClick, hoverHelp = '')
{
console.log (caption);
let button = document.createElement('button')
if (caption == 'Apply')
{
button.classList.add('grrapp')
} else {
button.classList.add('formapp')
}
if (caption == 'Update')
{
button.classList.add('fsdf')
}
button.textContent = caption
button.addEventListener('click', onClick)
if (hoverHelp !== '') {
this._addHelpTextOnHover(button, hoverHelp)
}
if (this._buttonBackroundColor !== null) {
button.style.backgroundColor = this._buttonBackroundColor
}
return button
}
/**
* @param {HTMLElement[]} children
* @return {HTMLElement}
*/
createFormGroup (children)
{
let divFormGroup = document.createElement('div')
divFormGroup.classList.add('form-group')
return UIGenerator.populateChildren(divFormGroup, children)
}
/**
* @param {string} id
* @param {Array} keyValuePairs
* @param {*} defaultValue
* @return {HTMLSelectElement}
*/
createFormGroupDropdown (id, keyValuePairs, defaultValue = null)
{
let dropdown = document.createElement('select'), item
dropdown.id = id
dropdown.classList.add('form-dropdown')
for (let [key, value] of keyValuePairs) {
item = document.createElement('option')
item.textContent = value
item.value = key
dropdown.appendChild(item)
}
dropdown.value = defaultValue === null ? keyValuePairs[0][0] : defaultValue
return dropdown
}
/**
* @param {string} id
* @param {string} type
* @param {*} defaultValue
* @return {HTMLInputElement}
*/
createFormGroupInput (id, type, defaultValue = null)
{
let inputFormGroup = document.createElement('input')
inputFormGroup.id = id
inputFormGroup.classList.add('form-input')
inputFormGroup.type = type
switch (type) {
case 'number':
case 'text':
inputFormGroup.classList.add('regular-input')
if (defaultValue !== null) {
inputFormGroup.value = defaultValue
}
break
case 'radio':
case 'checkbox':
inputFormGroup.classList.add('check-radio-input')
if (defaultValue !== null) {
inputFormGroup.checked = defaultValue
}
break
}
return inputFormGroup
}
/**
* @param {string} label
* @param {string} inputID
* @param {string} inputType
* @return {HTMLLabelElement}
*/
createFormGroupLabel (label, inputID = '', inputType = '')
{
let labelFormGroup = document.createElement('label')
labelFormGroup.classList.add('form-label')
labelFormGroup.textContent = label
if (inputID !== '') {
labelFormGroup.setAttribute('for', inputID)
}
if (inputType !== '') {
switch (inputType) {
case 'number':
case 'text':
labelFormGroup.classList.add('regular-input')
labelFormGroup.textContent += ': '
break
case 'radio':
case 'checkbox':
labelFormGroup.classList.add('check-radio-input')
break
}
}
return labelFormGroup
}
/**
* @param {string} statisticType
* @return {HTMLLabelElement}
*/
createFormGroupStatLabel (statisticType)
{
let labelFormGroup = document.createElement('label')
labelFormGroup.id = this._selectorGenerator.getStatLabelSelector(statisticType)
labelFormGroup.classList.add('form-stat-label')
labelFormGroup.textContent = '0'
return labelFormGroup
}
/**
* @param {string} label
* @param {string} inputType
* @param {string} hoverHelp
* @param {*} defaultValue
* @return {HTMLElement}
*/
createFormInputGroup (label, inputType = 'text', hoverHelp = '', defaultValue = null)
{
let divFormInputGroup
let inputID = this._selectorGenerator.getSettingsInputSelector(label)
let labelFormGroup = this.createFormGroupLabel(label, inputID, inputType)
let inputFormGroup = this.createFormGroupInput(inputID, inputType, defaultValue)
switch (inputType) {
case 'number':
case 'text':
divFormInputGroup = this.createFormGroup([labelFormGroup, inputFormGroup])
break
case 'radio':
case 'checkbox':
divFormInputGroup = this.createFormGroup([inputFormGroup, labelFormGroup])
break
}
if (hoverHelp !== '') {
this._addHelpTextOnHover(divFormInputGroup, hoverHelp)
}
return divFormInputGroup
}
/**
* @param {string} label
* @param {string} inputsType
* @param {int[]|string[]} defaultValues
* @return {HTMLElement}
*/
createFormRangeInputGroup (label, inputsType = 'text', defaultValues = [])
{
let maxInputSelector = this._selectorGenerator.getSettingsRangeInputSelector(label, false)
let minInputSelector = this._selectorGenerator.getSettingsRangeInputSelector(label, true)
let divFormInputGroup = this.createFormGroup([
this.createFormGroupLabel(label, '', inputsType),
this.createFormGroupInput(maxInputSelector, inputsType, defaultValues.length ? defaultValues[1] : null),
this.createFormGroupInput(minInputSelector, inputsType, defaultValues.length ? defaultValues[0] : null),
])
divFormInputGroup.classList.add('form-range-input-group')
return divFormInputGroup
}
/**
* @param {string} title
* @param {HTMLElement[]} children
* @return {HTMLElement|HTMLDivElement}
*/
createFormSection (title, children)
{
let sectionDiv = document.createElement('div')
sectionDiv.classList.add('form-section')
if (title !== '') {
let sectionTitle = document.createElement('label')
sectionTitle.textContent = title
sectionTitle.classList.add('title')
UIGenerator.populateChildren(sectionDiv, [sectionTitle])
}
return UIGenerator.populateChildren(sectionDiv, children)
}
/**
* @param {string} caption
* @param {string} tooltip
* @param {EventListenerOrEventListenerObject} onClick
* @param {string} hoverHelp
* @return {HTMLButtonElement}
*/
createFormSectionButton (caption, tooltip, onClick, hoverHelp = '', grr= '')
{
let button = this.createFormButton(caption, onClick, hoverHelp, grr)
button.title = tooltip
return button
}
/**
* @param {string} label
* @param {int} rows
* @param {string} hoverHelp
* @param {string} defaultValue
* @return {HTMLElement}
*/
createFormTextAreaGroup (label, rows, hoverHelp = '', defaultValue = '')
{
let labelElement = this.createFormGroupLabel(label)
labelElement.style.textAlign = 'center'
let textAreaElement = document.createElement('textarea')
textAreaElement.id = this._selectorGenerator.getSettingsInputSelector(label)
textAreaElement.classList.add('form-input')
textAreaElement.value = defaultValue
textAreaElement.setAttribute('rows', rows.toString())
let group = this.createFormGroup([labelElement, textAreaElement])
if (hoverHelp !== '') {
this._addHelpTextOnHover(group, hoverHelp)
}
return group
}
/**
* @param {string} IDSuffix
* @param {*} backgroundColor
* @param {*} top
* @param {*} width
* @return {this}
*/
createSection (IDSuffix, backgroundColor, top, width)
{
this._section = document.createElement('section')
this._section.id = this._selectorGenerator.getSelector(IDSuffix)
this._section.classList.add('form-section')
this._section.style.display = this._showUI ? 'block' : 'none'
this._section.style.top = top
this._section.style.width = width
this._section.style.backgroundColor = null
return this
}
/**
* @return {HTMLHRElement}
*/
createSeparator ()
{
return document.createElement('hr')
}
/**
* @param {LocalStore} localStore
* @param {EventListenerOrEventListenerObject|Function} onClick
* @param {boolean} addTopPadding
* @return {HTMLDivElement}
*/
createSettingsFormActions (localStore, onClick, addTopPadding = false)
{
let divFormActions = this.createFormSection('', [
this.createFormActions([
this.createFormButton('Apply', onClick, 'Filter items as per the settings in the dialog.', 'derpapply'),
this.createFormButton('Reset', () => {
localStore.retrieve()
onClick()
}, 'Restore and apply saved configuration.', 'derpreset'),
]),
])
if (addTopPadding) {
divFormActions.style.paddingTop = '10px'
}
return divFormActions
}
/**
* @param {string} label
* @param {Array} keyValuePairs
* @param {*} defaultValue
* @return {HTMLElement}
*/
createSettingsDropDownFormGroup (label, keyValuePairs, defaultValue = null)
{
let dropdownID = this._selectorGenerator.getSettingsInputSelector(label)
return this.createFormGroup([
this.createFormGroupLabel(label, dropdownID, 'text'),
this.createFormGroupDropdown(dropdownID, keyValuePairs, defaultValue),
])
}
/**
* @return {HTMLButtonElement}
*/
createSettingsHideButton ()
{
let section = this._section
return this.createFormButton('<< Hide', () => section.style.display = 'none')
}
/**
* @param {string} caption
* @param {HTMLElement} settingsSection
* @param {boolean} fixed
* @param {EventListenerOrEventListenerObject|Function|null} onMouseLeave
* @return {HTMLButtonElement}
*/
createSettingsShowButton (caption, settingsSection, fixed = true, onMouseLeave = null)
{
let controlButton = document.createElement('button')
controlButton.textContent = caption
controlButton.classList.add('show-settings')
if (fixed) {
controlButton.classList.add('fixed')
}
controlButton.addEventListener('click', () => {
let settingsUI = document.getElementById(settingsSection.id)
settingsUI.style.display = settingsUI.style.display === 'none' ? 'block' : 'none'
})
settingsSection.addEventListener('mouseleave', onMouseLeave ? () => onMouseLeave() : () => settingsSection.style.display = 'none')
return controlButton
}
/**
* @param {string} statisticsType
* @param {string} label
* @return {HTMLElement}
*/
createStatisticsFormGroup (statisticsType, label = '')
{
if (label === '') {
label = statisticsType
}
return this.createFormGroup([
this.createFormGroupLabel(label + ' Filter'),
this.createFormGroupStatLabel(statisticsType),
])
}
/**
* @return {HTMLElement}
*/
createStatisticsTotalsGroup ()
{
return this.createFormGroup([
this.createFormGroupLabel('Total'),
this.createFormGroupStatLabel('Total'),
])
}
/**
* @return {HTMLElement|HTMLDivElement}
*/
createStatusSection ()
{
this._statusLine = this.createFormGroupLabel('')
this._statusLine.id = this._selectorGenerator.getSelector('status')
return this.createFormSection('', [this._statusLine])
}
/**
* @param {LocalStore} localStore
* @return {HTMLElement}
*/
createStoreFormSection (localStore)
{
return this.createFormSection('Cached Configuration', [
this.createFormActions([
this.createFormSectionButton(
'Update', 'Save UI settings in store', () => localStore.save(), 'Saves applied settings.'),
this.createFormSectionButton(
'Purge', 'Purge store', () => localStore.delete(), 'Removes saved settings. Settings will then be sourced from the defaults defined in the script.'),
]),
])
}
/**
* @param {string} tabName
* @return {HTMLButtonElement}
*/
createTabButton (tabName)
{
let button = document.createElement('button')
button.classList.add('tab-button')
button.textContent = tabName
button.addEventListener('click', (event) => {
let button = event.currentTarget
let tabsSection = button.closest('.tabs-section')
let tabToOpen = tabsSection.querySelector('#' + toKebabCase(tabName))
for (let tabButton of tabsSection.querySelectorAll('.tab-button')) {
tabButton.classList.remove('active')
}
for (let tabPanel of tabsSection.querySelectorAll('.tab-panel')) {
tabPanel.classList.remove('active')
}
button.classList.add('active')
tabToOpen.classList.add('active')
})
return button
}
/**
* @param {string} tabName
* @param {HTMLElement[]} children
* @return {HTMLElement|HTMLDivElement}
*/
createTabPanel (tabName, children)
{
let panel = document.createElement('div')
panel.id = toKebabCase(tabName)
panel.classList.add('tab-panel')
return UIGenerator.populateChildren(panel, children)
}
/**
* @param {string[]} tabNames
* @param {HTMLElement[]} tabPanels
* @return {HTMLElement|HTMLDivElement}
*/
createTabsSection (tabNames, tabPanels)
{
let wrapper = document.createElement('div')
wrapper.classList.add('tabs-section')
let tabsDiv = document.createElement('div')
tabsDiv.classList.add('tabs-nav')
let tabButtons = []
for (let tabName of tabNames) {
tabButtons.push(this.createTabButton(tabName))
}
UIGenerator.populateChildren(tabsDiv, tabButtons)
UIGenerator.populateChildren(wrapper, [tabsDiv, ...tabPanels])
tabButtons[0].click()
return wrapper
}
/**
* @param {string} label
* @return {HTMLElement}
*/
getSettingsInput (label)
{
return document.getElementById(this._selectorGenerator.getSettingsInputSelector(label))
}
/**
* @param {string} label
* @return {boolean}
*/
getSettingsInputCheckedStatus (label)
{
return this.getSettingsInput(label).checked
}
/**
* @param {string} label
* @return {*}
*/
getSettingsInputValue (label)
{
return this.getSettingsInput(label).value
}
/**
* @param {string} label
* @param {boolean} getMinInput
* @return {HTMLElement}
*/
getSettingsRangeInput (label, getMinInput)
{
return document.getElementById(this._selectorGenerator.getSettingsRangeInputSelector(label, getMinInput))
}
/**
* @param {string} label
* @param {boolean} getMinInputValue
* @return {*}
*/
getSettingsRangeInputValue (label, getMinInputValue)
{
return this.getSettingsRangeInput(label, getMinInputValue).value
}
resetStatus ()
{
this._statusLine.textContent = this._statusText
}
/**
* @param {string} label
* @param {boolean} bool
*/
setSettingsInputCheckedStatus (label, bool)
{
this.getSettingsInput(label).checked = bool
}
/**
* @param {string} label
* @param {*} value
*/
setSettingsInputValue (label, value)
{
this.getSettingsInput(label).value = value
}
/**
* @param {string} label
* @param {number} lowerBound
* @param {number} upperBound
*/
setSettingsRangeInputValue (label, lowerBound, upperBound)
{
this.getSettingsRangeInput(label, true).value = lowerBound
this.getSettingsRangeInput(label, false).value = upperBound
}
/**
* @param {string} status
* @param {boolean} transient
*/
updateStatus (status, transient = false)
{
if (!transient) {
this._statusText = status
}
this._statusLine.textContent = status
}
}
class Validator
{
static iFramesRemover ()
{
GM_addStyle(' iframe { display: none !important; } ')
}
/**
* @param {StatisticsRecorder} statisticsRecorder
*/
constructor (statisticsRecorder)
{
/**
* @type {Array}
* @private
*/
this._filters = []
/**
* @type {RegExp|null}
* @private
*/
this._optimizedBlacklist = null
/**
* @type {Object}
* @private
*/
this._optimizedSanitizationRules = {}
/**
* @type {StatisticsRecorder}
* @private
*/
this._statisticsRecorder = statisticsRecorder
}
_buildWholeWordMatchingRegex (words)
{
let patternedWords = []
for (let i = 0; i < words.length; i++) {
patternedWords.push('\\b' + words[i] + '\\b')
}
return new RegExp('(' + patternedWords.join('|') + ')', 'gi')
}
/**
* @param {string} text
* @return {string}
*/
sanitize (text)
{
for (const substitute in this._optimizedSanitizationRules) {
text = text.replace(this._optimizedSanitizationRules[substitute], substitute)
}
return text.trim()
}
/**
* @param {HTMLElement} textNode
* @return {Validator}
*/
sanitizeTextNode (textNode)
{
textNode.textContent = this.sanitize(textNode.textContent)
return this
}
/**
* @param {string} selector
* @return {Validator}
*/
sanitizeNodeOfSelector (selector)
{
let node = document.querySelector(selector)
if (node) {
let sanitizedText = this.sanitize(node.textContent)
node.textContent = sanitizedText
document.title = sanitizedText
}
return this
}
/**
* @param {string[]} blacklistedWords
* @return {Validator}
*/
setBlacklist (blacklistedWords)
{
this._optimizedBlacklist = blacklistedWords.length ? this._buildWholeWordMatchingRegex(blacklistedWords) : null
return this
}
/**
* @param {Object} sanitizationRules
* @return {Validator}
*/
setSanitizationRules (sanitizationRules)
{
for (const substitute in sanitizationRules) {
this._optimizedSanitizationRules[substitute] = this._buildWholeWordMatchingRegex(sanitizationRules[substitute])
}
return this
}
/**
* @param {string} text
* @return {boolean}
*/
validateBlackList (text)
{
let validationCheck = true
if (this._optimizedBlacklist) {
validationCheck = text.match(this._optimizedBlacklist) === null
this._statisticsRecorder.record('Blacklist', validationCheck)
}
return validationCheck
}
/**
* @param {string} name
* @param {Node|HTMLElement} item
* @param {string} selector
* @return {boolean}
*/
validateNodeExistence (name, item, selector)
{
let validationCheck = item.querySelector(selector) !== null
this._statisticsRecorder.record(name, validationCheck)
return validationCheck
}
/**
* @param {string} name
* @param {Node|HTMLElement} item
* @param {string} selector
* @return {boolean}
*/
validateNodeNonExistence (name, item, selector)
{
let validationCheck = item.querySelector(selector) === null
this._statisticsRecorder.record(name, validationCheck)
return validationCheck
}
/**
* @param {string} name
* @param {number} value
* @param {number[]} bounds
* @return {boolean}
*/
validateRange (name, value, bounds)
{
let validationCheck = true
if (bounds[0] > 0 && bounds[1] > 0) {
validationCheck = value >= bounds[0] && value <= bounds[1]
} else {
if (bounds[0] > 0) {
validationCheck = value >= bounds[0]
}
if (bounds[1] > 0) {
validationCheck = value <= bounds[1]
}
}
this._statisticsRecorder.record(name, validationCheck)
return validationCheck
}
/**
* @param {string} name
* @param {number} lowerBound
* @param {number} upperBound
* @param getValueCallback
* @return {boolean}
*/
validateRangeFilter (name, lowerBound, upperBound, getValueCallback)
{
if (lowerBound > 0 || upperBound > 0) {
return this.validateRange(name, getValueCallback(), [lowerBound, upperBound])
}
return true
}
}
class PresetSwitcher
{
/**
* @param {string} scriptPrefix
* @param {Object} defaultPreset
* @param {Object} globalConfiguration
*/
static create (scriptPrefix, defaultPreset, globalConfiguration)
{
return new PresetSwitcher(scriptPrefix, defaultPreset, globalConfiguration)
}
/**
* @param {string} scriptPrefix
* @param {Object} defaultPreset
* @param {Object} globalConfiguration
*/
constructor (scriptPrefix, defaultPreset, globalConfiguration)
{
/**
* @type {Object}
* @private
*/
this._appliedPreset = null
/**
* @type {Object}
* @private
*/
this._defaultPreset = defaultPreset
/**
* {LocalStore}
*/
this._globalConfigurationStore = LocalStore.createGlobalConfigStore(scriptPrefix, globalConfiguration)
/**
* {Object}
*/
this._globalConfiguration = this._globalConfigurationStore.retrieve().get()
/**
* @type {LocalStore}
* @private
*/
this._presetsStore = LocalStore.createPresetConfigStore(scriptPrefix, defaultPreset)
/**
* @type {{name: string, config: Object}[]}
* @private
*/
this._presets = this._presetsStore.retrieve().get()
/**
* @type {string}
* @private
*/
this._scriptPrefix = scriptPrefix
}
/**
* @param {string} name
* @param {Object} config
* @return {this}
*/
createPreset (name, config)
{
this._presets.push({
name: name,
config: config,
})
this._presetsStore.update(this._presets)
return this
}
/**
* @param {string} name
* @return {this}
*/
deletePreset (name)
{
for (let i = 0; i < this._presets.length; i++) {
if (this._presets[i].name === name) {
this._presets.splice(i, 1)
this._presetsStore.update(this._presets)
break
}
}
return this
}
/**
* @param name
* @return {{name: string, config: Object}|null}
*/
findPreset (name)
{
for (let preset of this._presets) {
if (preset.name === name) {
return preset
}
}
return null
}
/**
* @return {{name: string, config: Object}}
*/
getAppliedPreset ()
{
return this._appliedPreset
}
}
class BaseHandler
{
static initialize ()
{
BaseHandler.throwOverrideError()
//return (new XNXXSearchFilters).init()
}
static throwOverrideError ()
{
throw new Error('override this method')
}
/**
* @param {string} scriptPrefix
* @param {string} itemClass
* @param {Object} settingsDefaults
*/
constructor (scriptPrefix, itemClass, settingsDefaults)
{
settingsDefaults.disableItemComplianceValidation = false
settingsDefaults.showUIAlways = false
/**
* Array of item compliance filters ordered in intended sequence of execution
* @type {Function[]}
* @protected
*/
this._complianceFilters = []
/**
* @type {string}
* @protected
*/
this._itemClass = itemClass
/**
* Operations to perform after script initialization
* @type {Function}
* @protected
*/
this._onAfterInitialization = null
/**
* Operations to perform after UI generation
* @type {Function}
* @protected
*/
this._onAfterUIBuild = null
/**
* Operations to perform before UI generation
* @type {Function}
* @protected
*/
this._onBeforeUIBuild = null
/**
* Operations to perform after compliance checks, the first time a item is retrieved
* @type {Function}
* @protected
*/
this._onFirstHitAfterCompliance = null
/**
* Operations to perform before compliance checks, the first time a item is retrieved
* @type {Function}
* @protected
*/
this._onFirstHitBeforeCompliance = null
/**
* Get item lists from the page
* @type {Function}
* @protected
*/
this._onGetItemLists = null
/**
* Logic to hide a non-compliant item
* @type {Function}
* @protected
*/
this._onItemHide = (item) => {item.style.display = 'none'}
/**
* Logic to show compliant item
* @type {Function}
* @protected
*/
this._onItemShow = (item) => {item.style.display = 'inline-block'}
/**
* Retrieve settings from UI and update settings object
* @type {Function}
* @private
*/
this._onSettingsApply = null
/**
* Settings to update in the UI or elsewhere when settings store is updated
* @type {Function}
* @protected
*/
this._onSettingsStoreUpdate = null
/**
* Must return the generated settings section node
* @type {Function}
* @protected
*/
this._onUIBuild = null
/**
* Validate initiating initialization.
* Can be used to stop script init on specific pages or vice versa
* @type {Function}
* @protected
*/
this._onValidateInit = () => true
/**
* @type {string}
* @private
*/
this._scriptPrefix = scriptPrefix
/**
* Local storage store with defaults
* @type {LocalStore}
* @protected
*/
this._settingsStore = new LocalStore(this._scriptPrefix + 'settings', settingsDefaults)
/**
* @type {Object}
* @protected
*/
this._settings = this._settingsStore.retrieve().get()
/**
* @type {StatisticsRecorder}
* @protected
*/
this._statistics = new StatisticsRecorder(this._scriptPrefix)
/**
* @type {UIGenerator}
* @protected
*/
this._uiGen = new UIGenerator(this._settings.showUIAlways, this._scriptPrefix)
/**
* @type {Validator}
* @protected
*/
this._validator = (new Validator(this._statistics))
}
/**
* @param {Function} eventHandler
* @param {*} parameters
* @return {null|NodeListOf<HTMLElement>|*}
* @private
*/
_callEventHandler (eventHandler, ...parameters)
{
if (eventHandler) {
return eventHandler(...parameters)
}
return null
}
/**
* Filters items as per settings
* @param {HTMLElement|NodeList<HTMLElement>} itemsList
* @protected
*/
_complyItemsList (itemsList)
{
for (let item of this._getItemsFromItemsList(itemsList)) {
if (typeof item.scriptProcessedOnce === 'undefined') {
item.scriptProcessedOnce = false
this._callEventHandler(this._onFirstHitBeforeCompliance, item)
}
this._validateItemCompliance(item)
if (!item.scriptProcessedOnce) {
this._callEventHandler(this._onFirstHitAfterCompliance, item)
item.scriptProcessedOnce = true
}
this._statistics.updateUI()
}
}
/**
* @protected
*/
_createSettingsFormActions ()
{
return this._uiGen.createSettingsFormActions(this._settingsStore, () => {
this._callEventHandler(this._onSettingsApply)
this._statistics.reset()
for (let itemsList of this._callEventHandler(this._onGetItemLists)) {
this._complyItemsList(itemsList)
}
})
}
/**
* @param {HTMLElement|null} UISection
* @private
*/
_embedUI (UISection)
{
if (UISection) {
this._uiGen.constructor.appendToBody(UISection)
this._uiGen.constructor.appendToBody(this._uiGen.createSettingsShowButton('', UISection, true, () => {
if (!this._settings.showUIAlways) {
UISection.style.display = 'none'
}
}))
this._callEventHandler(this._onSettingsStoreUpdate)
}
}
/**
* @param {HTMLElement|NodeList<HTMLElement>} itemsList
* @return {NodeListOf<HTMLElement>|HTMLElement[]}
* @protected
*/
_getItemsFromItemsList (itemsList)
{
let items = []
if (itemsList instanceof NodeList) {
itemsList.forEach((node) => {
if (typeof node.classList !== 'undefined' && node.classList.contains(this._itemClass)) {
items.push(node)
}
})
} else {
items = itemsList.querySelectorAll('.' + this._itemClass)
}
return items
}
/**
* @param {Object} sanitizationRules
* @return {string}
* @protected
*/
_transformSanitizationRulesToText (sanitizationRules)
{
let sanitizationRulesText = []
for (let substitute in sanitizationRules) {
sanitizationRulesText.push(substitute + '=' + sanitizationRules[substitute].join(','))
}
return sanitizationRulesText.join('\n')
}
/**
* @param {string[]} strings
* @protected
*/
_trimAndKeepNonEmptyStrings (strings)
{
let nonEmptyStrings = []
for (let string of strings) {
string = string.trim()
if (string !== '') {
nonEmptyStrings.push(string)
}
}
return nonEmptyStrings
}
/**
* @param {string[]} blacklistedWords
* @protected
*/
_validateAndSetBlacklistedWords (blacklistedWords)
{
this._settings.blacklist = this._trimAndKeepNonEmptyStrings(blacklistedWords)
this._validator.setBlacklist(this._settings.blacklist)
}
/**
* @param {string[]} sanitizationRules
* @protected
*/
_validateAndSetSanitizationRules (sanitizationRules)
{
let fragments, validatedTargetWords
this._settings.sanitize = {}
for (let sanitizationRule of sanitizationRules) {
if (sanitizationRule.includes('=')) {
fragments = sanitizationRule.split('=')
if (fragments[0] === '') {
fragments[0] = ' '
}
validatedTargetWords = this._trimAndKeepNonEmptyStrings(fragments[1].split(','))
if (validatedTargetWords.length) {
this._settings.sanitize[fragments[0]] = validatedTargetWords
}
}
}
this._validator.setSanitizationRules(this._settings.sanitize)
}
/**
* @param {HTMLElement|Node} item
* @protected
*/
_validateItemCompliance (item)
{
let itemComplies = true
if (!this._settings.disableItemComplianceValidation) {
for (let complianceFilter of this._complianceFilters) {
if (!complianceFilter(item)) {
itemComplies = false
break
}
}
}
itemComplies ? this._callEventHandler(this._onItemShow, item) : this._callEventHandler(this._onItemHide, item)
}
/**
* Initialize the script and do basic UI removals
*/
init ()
{
try {
if (this._callEventHandler(this._onValidateInit)) {
this._callEventHandler(this._onBeforeUIBuild)
this._embedUI(this._callEventHandler(this._onUIBuild))
this._callEventHandler(this._onAfterUIBuild)
for (let itemsList of this._callEventHandler(this._onGetItemLists)) {
ChildObserver.create().onNodesAdded((itemsAdded) => this._complyItemsList(itemsAdded)).observe(itemsList)
this._complyItemsList(itemsList)
}
this._callEventHandler(this._onAfterInitialization)
this._settingsStore.onChange(() => this._callEventHandler(this._onSettingsStoreUpdate))
}
} catch (error) {
console.error(this._scriptPrefix + 'script encountered an error: ' + error)
}
}
}