- // ==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)
- }
- }
- }