Greasy Fork is available in English.
Base library for my scripts
สคริปต์นี้ไม่ควรถูกติดตั้งโดยตรง มันเป็นคลังสำหรับสคริปต์อื่น ๆ เพื่อบรรจุด้วยคำสั่งเมทา // @require https://update.greasyfork.org/scripts/412286/853259/RRG.js
// ==UserScript== // @name RRG // @namespace brazenvoid // @version 2.0.1 // @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() } r###meObservation () { 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} validationR###lt * @param {number} value */ record (statisticType, validationR###lt, value = 1) { if (!validationR###lt) { 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 = '') { 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 = '') { let button = this.createFormButton(caption, onClick, hoverHelp) 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.'), this.createFormButton('Reset', () => { localStore.retrieve() onClick() }, 'Restore and apply saved configuration.'), ]), ]) 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('Status') 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._uiGen.updateStatus('Initial run completed.') this._callEventHandler(this._onAfterInitialization) this._settingsStore.onChange(() => this._callEventHandler(this._onSettingsStoreUpdate)) } } catch (error) { console.error(this._scriptPrefix + 'script encountered an error: ' + error) } } }