grr

Base library for my scripts

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.cn-greasyfork.org/scripts/411595/849664/grr.js

  1. // ==UserScript==
  2. // @name grr
  3. // @namespace brazenvoid
  4. // @version 1.0
  5. // @author brazenvoid
  6. // @license GPL-3.0-only
  7. // @description Base library for my scripts
  8. // @grant GM_addStyle
  9. // @run-at document-end
  10. // ==/UserScript==
  11.  
  12. /**
  13. * @function GM_addStyle
  14. * @param {string} style
  15. */
  16. GM_addStyle(
  17. `@keyframes fadeEffect{from{opacity:0}to{opacity:1}}button.form-button{padding:0 5px;width:100%}button.show-settings{background-color:#ffa31a;border:0;margin:2px 5px;padding:2px 5px;width:100%}button.show-settings.fixed{color:#000;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:inherit;border:1px solid #000;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}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 #000;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{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:#000;font-size:12px;font-weight:700;position:fixed;left:0;padding:5px 10px;z-index:1000}select.form-dropdown{float:right;height:18px;text-align:center;width:100px}textarea.form-input{display:block;height:auto;position:relative;width:98%}`)
  18.  
  19. /**
  20. * @param milliseconds
  21. * @return {Promise<*>}
  22. */
  23. const sleep = (milliseconds) => {
  24. return new Promise(resolve => setTimeout(resolve, milliseconds))
  25. }
  26.  
  27. /**
  28. * @param {string} text
  29. * @return {string}
  30. */
  31. function toKebabCase (text)
  32. {
  33. return text.toLowerCase().replace(' ', '-')
  34. }
  35.  
  36. class ChildObserver
  37. {
  38. /**
  39. * @callback observerOnMutation
  40. * @param {NodeList} nodes
  41. */
  42.  
  43. /**
  44. * @return {ChildObserver}
  45. */
  46. static create ()
  47. {
  48. return new ChildObserver
  49. }
  50.  
  51. /**
  52. * ChildObserver constructor
  53. */
  54. constructor ()
  55. {
  56. this._node = null
  57. this._observer = null
  58. this._onNodesAdded = null
  59. this._onNodesRemoved = null
  60. }
  61.  
  62. /**
  63. * @return {ChildObserver}
  64. * @private
  65. */
  66. _observeNodes ()
  67. {
  68. this._observer.observe(this._node, {childList: true})
  69. return this
  70. }
  71.  
  72. /**
  73. * Attach an observer to the specified node(s)
  74. * @param {Node} node
  75. * @returns {ChildObserver}
  76. */
  77. observe (node)
  78. {
  79. this._node = node
  80. this._observer = new MutationObserver((mutations) => {
  81. for (let mutation of mutations) {
  82. if (mutation.addedNodes.length && this._onNodesAdded !== null) {
  83. this._onNodesAdded(
  84. mutation.addedNodes,
  85. mutation.previousSibling,
  86. mutation.nextSibling,
  87. mutation.target,
  88. )
  89. }
  90. if (mutation.removedNodes.length && this._onNodesRemoved !== null) {
  91. this._onNodesRemoved(
  92. mutation.removedNodes,
  93. mutation.previousSibling,
  94. mutation.nextSibling,
  95. mutation.target,
  96. )
  97. }
  98. }
  99. })
  100. return this._observeNodes()
  101. }
  102.  
  103. /**
  104. * @param {observerOnMutation} eventHandler
  105. * @returns {ChildObserver}
  106. */
  107. onNodesAdded (eventHandler)
  108. {
  109. this._onNodesAdded = eventHandler
  110. return this
  111. }
  112.  
  113. /**
  114. * @param {observerOnMutation} eventHandler
  115. * @returns {ChildObserver}
  116. */
  117. onNodesRemoved (eventHandler)
  118. {
  119. this._onNodesRemoved = eventHandler
  120. return this
  121. }
  122.  
  123. pauseObservation ()
  124. {
  125. this._observer.disconnect()
  126. }
  127.  
  128. resumeObservation ()
  129. {
  130. this._observeNodes()
  131. }
  132. }
  133.  
  134. class LocalStore
  135. {
  136. /**
  137. * @callback storeEventHandler
  138. * @param {Object} store
  139. */
  140.  
  141. /**
  142. * @param {string} scriptPrefix
  143. * @param {Object} defaults
  144. * @return {LocalStore}
  145. */
  146. static createGlobalConfigStore (scriptPrefix, defaults)
  147. {
  148. return new LocalStore(scriptPrefix + 'globals', defaults)
  149. }
  150.  
  151. static createPresetConfigStore (scriptPrefix, defaults)
  152. {
  153. return new LocalStore(scriptPrefix + 'presets', [
  154. {
  155. name: 'default',
  156. config: defaults,
  157. },
  158. ])
  159. }
  160.  
  161. /**
  162. * @param {string} key
  163. * @param {Object} defaults
  164. */
  165. constructor (key, defaults)
  166. {
  167. /**
  168. * @type {string}
  169. * @private
  170. */
  171. this._key = key
  172.  
  173. /**
  174. * @type {Object}
  175. * @private
  176. */
  177. this._store = {}
  178.  
  179. /**
  180. * @type {string}
  181. * @private
  182. */
  183. this._defaults = this._toJSON(defaults)
  184.  
  185. /**
  186. * @type {storeEventHandler}
  187. */
  188. this._onChange = null
  189. }
  190.  
  191. /**
  192. * @param {string} json
  193. * @return {Object}
  194. * @private
  195. */
  196. _fromJSON (json)
  197. {
  198. /** @type {{arrays: Object, objects: Object, properties: Object}} */
  199. let parsedJSON = JSON.parse(json)
  200. let arrayObject = {}
  201. let store = {}
  202.  
  203. for (let property in parsedJSON.arrays) {
  204. arrayObject = JSON.parse(parsedJSON.arrays[property])
  205. store[property] = []
  206.  
  207. for (let key in arrayObject) {
  208. store[property].push(arrayObject[key])
  209. }
  210. }
  211. for (let property in parsedJSON.objects) {
  212. store[property] = this._fromJSON(parsedJSON.objects[property])
  213. }
  214. for (let property in parsedJSON.properties) {
  215. store[property] = parsedJSON.properties[property]
  216. }
  217. return store
  218. }
  219.  
  220. /**
  221. * @return {string}
  222. * @private
  223. */
  224. _getStore ()
  225. {
  226. return window.localStorage.getItem(this._key)
  227. }
  228.  
  229. /**
  230. * @return {Object}
  231. * @private
  232. */
  233. _getDefaults ()
  234. {
  235. return this._fromJSON(this._defaults)
  236. }
  237.  
  238. /**
  239. * @param {Object} store
  240. * @return {string}
  241. * @private
  242. */
  243. _toJSON (store)
  244. {
  245. let arrayToObject = {}
  246. let json = {arrays: {}, objects: {}, properties: {}}
  247.  
  248. for (let property in store) {
  249. if (typeof store[property] === 'object') {
  250. if (Array.isArray(store[property])) {
  251. for (let key in store[property]) {
  252. arrayToObject[key] = store[property][key]
  253. }
  254. json.arrays[property] = JSON.stringify(arrayToObject)
  255. } else {
  256. json.objects[property] = this._toJSON(store[property])
  257. }
  258. } else {
  259. json.properties[property] = store[property]
  260. }
  261. }
  262. return JSON.stringify(json)
  263. }
  264.  
  265. _handleOnChange ()
  266. {
  267. if (this._onChange !== null) {
  268. this._onChange(this._store)
  269. }
  270. }
  271.  
  272. /**
  273. * @return {LocalStore}
  274. */
  275. delete ()
  276. {
  277. window.localStorage.removeItem(this._key)
  278. return this
  279. }
  280.  
  281. /**
  282. * @return {*}
  283. */
  284. get ()
  285. {
  286. return this._store
  287. }
  288.  
  289. /**
  290. * @return {boolean}
  291. */
  292. isPurged ()
  293. {
  294. return this._getStore() === null
  295. }
  296.  
  297. /**
  298. * @param {storeEventHandler} handler
  299. * @return {LocalStore}
  300. */
  301. onChange (handler)
  302. {
  303. this._onChange = handler
  304. return this
  305. }
  306.  
  307. /**
  308. * @return {LocalStore}
  309. */
  310. restoreDefaults ()
  311. {
  312. this._store = this._getDefaults()
  313. this._handleOnChange()
  314. return this
  315. }
  316.  
  317. /**
  318. * @return {LocalStore}
  319. */
  320. retrieve ()
  321. {
  322. let storedStore = this._getStore()
  323. if (storedStore === null) {
  324. this.restoreDefaults()
  325. } else {
  326. this._store = this._fromJSON(storedStore)
  327. }
  328. this._handleOnChange()
  329. return this
  330. }
  331.  
  332. /**
  333. * @return {LocalStore}
  334. */
  335. save ()
  336. {
  337. window.localStorage.setItem(this._key, this._toJSON(this._store))
  338. this._handleOnChange()
  339. return this
  340. }
  341.  
  342. /**
  343. * @param {*} data
  344. * @return {LocalStore}
  345. */
  346. update (data)
  347. {
  348. this._store = data
  349. return this.save()
  350. }
  351. }
  352.  
  353. class SelectorGenerator
  354. {
  355. /**
  356. * @param {string} selectorPrefix
  357. */
  358. constructor (selectorPrefix)
  359. {
  360. /**
  361. * @type {string}
  362. * @private
  363. */
  364. this._prefix = selectorPrefix
  365. }
  366.  
  367. /**
  368. * @param {string} selector
  369. * @return {string}
  370. */
  371. getSelector (selector)
  372. {
  373. return this._prefix + selector
  374. }
  375.  
  376. /**
  377. * @param {string} settingName
  378. * @return {string}
  379. */
  380. getSettingsInputSelector (settingName)
  381. {
  382. return this.getSelector(toKebabCase(settingName) + '-setting')
  383. }
  384.  
  385. /**
  386. * @param {string} settingName
  387. * @param {boolean} getMinInputSelector
  388. * @return {string}
  389. */
  390. getSettingsRangeInputSelector (settingName, getMinInputSelector)
  391. {
  392. return this.getSelector(toKebabCase(settingName) + (getMinInputSelector ? '-min' : '-max') + '-setting')
  393. }
  394.  
  395. /**
  396. * @param {string} statisticType
  397. * @return {string}
  398. */
  399. getStatLabelSelector (statisticType)
  400. {
  401. return this.getSelector(toKebabCase(statisticType) + '-stat')
  402. }
  403. }
  404.  
  405. class StatisticsRecorder
  406. {
  407. /**
  408. * @param {string} selectorPrefix
  409. */
  410. constructor (selectorPrefix)
  411. {
  412. /**
  413. * @type {SelectorGenerator}
  414. * @private
  415. */
  416. this._selectorGenerator = new SelectorGenerator(selectorPrefix)
  417.  
  418. /**
  419. * @type {{Total: number}}
  420. * @private
  421. */
  422. this._statistics = {Total: 0}
  423. }
  424.  
  425. /**
  426. * @param {string} statisticType
  427. * @param {boolean} validationResult
  428. * @param {number} value
  429. */
  430. record (statisticType, validationResult, value = 1)
  431. {
  432. if (!validationResult) {
  433. if (typeof this._statistics[statisticType] !== 'undefined') {
  434. this._statistics[statisticType] += value
  435. } else {
  436. this._statistics[statisticType] = value
  437. }
  438. this._statistics.Total += value
  439. }
  440. }
  441.  
  442. reset ()
  443. {
  444. for (const statisticType in this._statistics) {
  445. this._statistics[statisticType] = 0
  446. }
  447. }
  448.  
  449. updateUI ()
  450. {
  451. let label, labelSelector
  452.  
  453. for (const statisticType in this._statistics) {
  454. labelSelector = this._selectorGenerator.getStatLabelSelector(statisticType)
  455. label = document.getElementById(labelSelector)
  456. if (label !== null) {
  457. label.textContent = this._statistics[statisticType]
  458. }
  459. }
  460. }
  461. }
  462.  
  463. class UIGenerator
  464. {
  465. /**
  466. * @param {HTMLElement|Node} node
  467. */
  468. static appendToBody (node)
  469. {
  470. document.getElementsByTagName('body')[0].appendChild(node)
  471. }
  472.  
  473. /**
  474. * @param {HTMLElement} node
  475. * @param {HTMLElement[]} children
  476. * @return {HTMLElement}
  477. */
  478. static populateChildren (node, children)
  479. {
  480. for (let child of children) {
  481. node.appendChild(child)
  482. }
  483. return node
  484. }
  485.  
  486. /**
  487. * @param {boolean} showUI
  488. * @param {string} selectorPrefix
  489. */
  490. constructor (showUI, selectorPrefix)
  491. {
  492. /**
  493. * @type {*}
  494. * @private
  495. */
  496. this._buttonBackroundColor = null
  497.  
  498. /**
  499. * @type {HTMLElement}
  500. * @private
  501. */
  502. this._section = null
  503.  
  504. /**
  505. * @type {SelectorGenerator}
  506. * @private
  507. */
  508. this._selectorGenerator = new SelectorGenerator(selectorPrefix)
  509.  
  510. /**
  511. * @type {string}
  512. * @private
  513. */
  514. this._selectorPrefix = selectorPrefix
  515.  
  516. /**
  517. * @type {boolean}
  518. * @private
  519. */
  520. this._showUI = showUI
  521.  
  522. /**
  523. * @type {HTMLLabelElement}
  524. * @private
  525. */
  526. this._statusLine = null
  527.  
  528. /**
  529. * @type {string}
  530. * @private
  531. */
  532. this._statusText = ''
  533. }
  534.  
  535. /**
  536. * @param {HTMLElement} node
  537. * @param {string} text
  538. * @return {this}
  539. * @private
  540. */
  541. _addHelpTextOnHover (node, text)
  542. {
  543. node.addEventListener('mouseover', () => this.updateStatus(text, true))
  544. node.addEventListener('mouseout', () => this.resetStatus())
  545. }
  546.  
  547. /**
  548. * @param {HTMLElement[]} children
  549. * @return {HTMLElement}
  550. */
  551. addSectionChildren (children)
  552. {
  553. return UIGenerator.populateChildren(this._section, children)
  554. }
  555.  
  556. /**
  557. * @return {HTMLBRElement}
  558. */
  559. createBreakSeparator ()
  560. {
  561. return document.createElement('br')
  562. }
  563.  
  564. /**
  565. * @param {HTMLElement[]} children
  566. * @return {HTMLDivElement}
  567. */
  568. createFormActions (children)
  569. {
  570. let wrapperDiv = document.createElement('div')
  571. wrapperDiv.classList.add('form-actions-wrapper')
  572.  
  573. UIGenerator.populateChildren(wrapperDiv, children)
  574.  
  575. let formActionsDiv = document.createElement('div')
  576. formActionsDiv.classList.add('form-actions')
  577. formActionsDiv.appendChild(wrapperDiv)
  578.  
  579. return formActionsDiv
  580. }
  581.  
  582. /**
  583. * @param {string} caption
  584. * @param {EventListenerOrEventListenerObject} onClick
  585. * @param {string} hoverHelp
  586. * @return {HTMLButtonElement}
  587. */
  588. createFormButton (caption, onClick, hoverHelp = '')
  589. {
  590. console.log (caption);
  591.  
  592. let button = document.createElement('button')
  593. if (caption == 'Apply')
  594. {
  595. button.classList.add('grrapp')
  596. } else {
  597. button.classList.add('formapp')
  598. }
  599. button.textContent = caption
  600. button.addEventListener('click', onClick)
  601.  
  602. if (hoverHelp !== '') {
  603. this._addHelpTextOnHover(button, hoverHelp)
  604. }
  605. if (this._buttonBackroundColor !== null) {
  606. button.style.backgroundColor = this._buttonBackroundColor
  607. }
  608. return button
  609. }
  610.  
  611. /**
  612. * @param {HTMLElement[]} children
  613. * @return {HTMLElement}
  614. */
  615. createFormGroup (children)
  616. {
  617. let divFormGroup = document.createElement('div')
  618. divFormGroup.classList.add('form-group')
  619.  
  620. return UIGenerator.populateChildren(divFormGroup, children)
  621. }
  622.  
  623. /**
  624. * @param {string} id
  625. * @param {Array} keyValuePairs
  626. * @param {*} defaultValue
  627. * @return {HTMLSelectElement}
  628. */
  629. createFormGroupDropdown (id, keyValuePairs, defaultValue = null)
  630. {
  631. let dropdown = document.createElement('select'), item
  632. dropdown.id = id
  633. dropdown.classList.add('form-dropdown')
  634.  
  635. for (let [key, value] of keyValuePairs) {
  636. item = document.createElement('option')
  637. item.textContent = value
  638. item.value = key
  639. dropdown.appendChild(item)
  640. }
  641. dropdown.value = defaultValue === null ? keyValuePairs[0][0] : defaultValue
  642.  
  643. return dropdown
  644. }
  645.  
  646. /**
  647. * @param {string} id
  648. * @param {string} type
  649. * @param {*} defaultValue
  650. * @return {HTMLInputElement}
  651. */
  652. createFormGroupInput (id, type, defaultValue = null)
  653. {
  654. let inputFormGroup = document.createElement('input')
  655. inputFormGroup.id = id
  656. inputFormGroup.classList.add('form-input')
  657. inputFormGroup.type = type
  658.  
  659. switch (type) {
  660. case 'number':
  661. case 'text':
  662. inputFormGroup.classList.add('regular-input')
  663.  
  664. if (defaultValue !== null) {
  665. inputFormGroup.value = defaultValue
  666. }
  667. break
  668. case 'radio':
  669. case 'checkbox':
  670. inputFormGroup.classList.add('check-radio-input')
  671.  
  672. if (defaultValue !== null) {
  673. inputFormGroup.checked = defaultValue
  674. }
  675. break
  676. }
  677. return inputFormGroup
  678. }
  679.  
  680. /**
  681. * @param {string} label
  682. * @param {string} inputID
  683. * @param {string} inputType
  684. * @return {HTMLLabelElement}
  685. */
  686. createFormGroupLabel (label, inputID = '', inputType = '')
  687. {
  688. let labelFormGroup = document.createElement('label')
  689. labelFormGroup.classList.add('form-label')
  690. labelFormGroup.textContent = label
  691.  
  692. if (inputID !== '') {
  693. labelFormGroup.setAttribute('for', inputID)
  694. }
  695. if (inputType !== '') {
  696. switch (inputType) {
  697. case 'number':
  698. case 'text':
  699. labelFormGroup.classList.add('regular-input')
  700. labelFormGroup.textContent += ': '
  701. break
  702. case 'radio':
  703. case 'checkbox':
  704. labelFormGroup.classList.add('check-radio-input')
  705. break
  706. }
  707. }
  708. return labelFormGroup
  709. }
  710.  
  711. /**
  712. * @param {string} statisticType
  713. * @return {HTMLLabelElement}
  714. */
  715. createFormGroupStatLabel (statisticType)
  716. {
  717. let labelFormGroup = document.createElement('label')
  718. labelFormGroup.id = this._selectorGenerator.getStatLabelSelector(statisticType)
  719. labelFormGroup.classList.add('form-stat-label')
  720. labelFormGroup.textContent = '0'
  721.  
  722. return labelFormGroup
  723. }
  724.  
  725. /**
  726. * @param {string} label
  727. * @param {string} inputType
  728. * @param {string} hoverHelp
  729. * @param {*} defaultValue
  730. * @return {HTMLElement}
  731. */
  732. createFormInputGroup (label, inputType = 'text', hoverHelp = '', defaultValue = null)
  733. {
  734. let divFormInputGroup
  735. let inputID = this._selectorGenerator.getSettingsInputSelector(label)
  736. let labelFormGroup = this.createFormGroupLabel(label, inputID, inputType)
  737. let inputFormGroup = this.createFormGroupInput(inputID, inputType, defaultValue)
  738.  
  739. switch (inputType) {
  740. case 'number':
  741. case 'text':
  742. divFormInputGroup = this.createFormGroup([labelFormGroup, inputFormGroup])
  743. break
  744. case 'radio':
  745. case 'checkbox':
  746. divFormInputGroup = this.createFormGroup([inputFormGroup, labelFormGroup])
  747. break
  748. }
  749. if (hoverHelp !== '') {
  750. this._addHelpTextOnHover(divFormInputGroup, hoverHelp)
  751. }
  752. return divFormInputGroup
  753. }
  754.  
  755. /**
  756. * @param {string} label
  757. * @param {string} inputsType
  758. * @param {int[]|string[]} defaultValues
  759. * @return {HTMLElement}
  760. */
  761. createFormRangeInputGroup (label, inputsType = 'text', defaultValues = [])
  762. {
  763. let maxInputSelector = this._selectorGenerator.getSettingsRangeInputSelector(label, false)
  764. let minInputSelector = this._selectorGenerator.getSettingsRangeInputSelector(label, true)
  765.  
  766. let divFormInputGroup = this.createFormGroup([
  767. this.createFormGroupLabel(label, '', inputsType),
  768. this.createFormGroupInput(maxInputSelector, inputsType, defaultValues.length ? defaultValues[1] : null),
  769. this.createFormGroupInput(minInputSelector, inputsType, defaultValues.length ? defaultValues[0] : null),
  770. ])
  771. divFormInputGroup.classList.add('form-range-input-group')
  772.  
  773. return divFormInputGroup
  774. }
  775.  
  776. /**
  777. * @param {string} title
  778. * @param {HTMLElement[]} children
  779. * @return {HTMLElement|HTMLDivElement}
  780. */
  781. createFormSection (title, children)
  782. {
  783. let sectionDiv = document.createElement('div')
  784. sectionDiv.classList.add('form-section')
  785.  
  786. if (title !== '') {
  787. let sectionTitle = document.createElement('label')
  788. sectionTitle.textContent = title
  789. sectionTitle.classList.add('title')
  790. UIGenerator.populateChildren(sectionDiv, [sectionTitle])
  791. }
  792. return UIGenerator.populateChildren(sectionDiv, children)
  793. }
  794.  
  795. /**
  796. * @param {string} caption
  797. * @param {string} tooltip
  798. * @param {EventListenerOrEventListenerObject} onClick
  799. * @param {string} hoverHelp
  800. * @return {HTMLButtonElement}
  801. */
  802. createFormSectionButton (caption, tooltip, onClick, hoverHelp = '', grr= '')
  803. {
  804. let button = this.createFormButton(caption, onClick, hoverHelp, grr)
  805. button.title = tooltip
  806.  
  807. return button
  808. }
  809.  
  810. /**
  811. * @param {string} label
  812. * @param {int} rows
  813. * @param {string} hoverHelp
  814. * @param {string} defaultValue
  815. * @return {HTMLElement}
  816. */
  817. createFormTextAreaGroup (label, rows, hoverHelp = '', defaultValue = '')
  818. {
  819. let labelElement = this.createFormGroupLabel(label)
  820. labelElement.style.textAlign = 'center'
  821.  
  822. let textAreaElement = document.createElement('textarea')
  823. textAreaElement.id = this._selectorGenerator.getSettingsInputSelector(label)
  824. textAreaElement.classList.add('form-input')
  825. textAreaElement.value = defaultValue
  826. textAreaElement.setAttribute('rows', rows.toString())
  827.  
  828. let group = this.createFormGroup([labelElement, textAreaElement])
  829.  
  830. if (hoverHelp !== '') {
  831. this._addHelpTextOnHover(group, hoverHelp)
  832. }
  833. return group
  834. }
  835.  
  836. /**
  837. * @param {string} IDSuffix
  838. * @param {*} backgroundColor
  839. * @param {*} top
  840. * @param {*} width
  841. * @return {this}
  842. */
  843. createSection (IDSuffix, backgroundColor, top, width)
  844. {
  845. this._section = document.createElement('section')
  846. this._section.id = this._selectorGenerator.getSelector(IDSuffix)
  847. this._section.classList.add('form-section')
  848. this._section.style.display = this._showUI ? 'block' : 'none'
  849. this._section.style.top = top
  850. this._section.style.width = width
  851. this._section.style.backgroundColor = backgroundColor
  852.  
  853. return this
  854. }
  855.  
  856. /**
  857. * @return {HTMLHRElement}
  858. */
  859. createSeparator ()
  860. {
  861. return document.createElement('hr')
  862. }
  863.  
  864. /**
  865. * @param {LocalStore} localStore
  866. * @param {EventListenerOrEventListenerObject|Function} onClick
  867. * @param {boolean} addTopPadding
  868. * @return {HTMLDivElement}
  869. */
  870. createSettingsFormActions (localStore, onClick, addTopPadding = false)
  871. {
  872. let divFormActions = this.createFormSection('', [
  873. this.createFormActions([
  874. this.createFormButton('Apply', onClick, 'Filter items as per the settings in the dialog.', 'derpapply'),
  875. this.createFormButton('Reset', () => {
  876. localStore.retrieve()
  877. onClick()
  878. }, 'Restore and apply saved configuration.', 'derpreset'),
  879. ]),
  880. ])
  881. if (addTopPadding) {
  882. divFormActions.style.paddingTop = '10px'
  883. }
  884. return divFormActions
  885. }
  886.  
  887. /**
  888. * @param {string} label
  889. * @param {Array} keyValuePairs
  890. * @param {*} defaultValue
  891. * @return {HTMLElement}
  892. */
  893. createSettingsDropDownFormGroup (label, keyValuePairs, defaultValue = null)
  894. {
  895. let dropdownID = this._selectorGenerator.getSettingsInputSelector(label)
  896.  
  897. return this.createFormGroup([
  898. this.createFormGroupLabel(label, dropdownID, 'text'),
  899. this.createFormGroupDropdown(dropdownID, keyValuePairs, defaultValue),
  900. ])
  901. }
  902.  
  903. /**
  904. * @return {HTMLButtonElement}
  905. */
  906. createSettingsHideButton ()
  907. {
  908. let section = this._section
  909. return this.createFormButton('<< Hide', () => section.style.display = 'none')
  910. }
  911.  
  912. /**
  913. * @param {string} caption
  914. * @param {HTMLElement} settingsSection
  915. * @param {boolean} fixed
  916. * @param {EventListenerOrEventListenerObject|Function|null} onMouseLeave
  917. * @return {HTMLButtonElement}
  918. */
  919. createSettingsShowButton (caption, settingsSection, fixed = true, onMouseLeave = null)
  920. {
  921. let controlButton = document.createElement('button')
  922. controlButton.textContent = caption
  923. controlButton.classList.add('show-settings')
  924.  
  925. if (fixed) {
  926. controlButton.classList.add('fixed')
  927. }
  928. controlButton.addEventListener('click', () => {
  929. let settingsUI = document.getElementById(settingsSection.id)
  930. settingsUI.style.display = settingsUI.style.display === 'none' ? 'block' : 'none'
  931. })
  932. settingsSection.addEventListener('mouseleave', onMouseLeave ? () => onMouseLeave() : () => settingsSection.style.display = 'none')
  933.  
  934. return controlButton
  935. }
  936.  
  937. /**
  938. * @param {string} statisticsType
  939. * @param {string} label
  940. * @return {HTMLElement}
  941. */
  942. createStatisticsFormGroup (statisticsType, label = '')
  943. {
  944. if (label === '') {
  945. label = statisticsType
  946. }
  947. return this.createFormGroup([
  948. this.createFormGroupLabel(label + ' Filter'),
  949. this.createFormGroupStatLabel(statisticsType),
  950. ])
  951. }
  952.  
  953. /**
  954. * @return {HTMLElement}
  955. */
  956. createStatisticsTotalsGroup ()
  957. {
  958. return this.createFormGroup([
  959. this.createFormGroupLabel('Total'),
  960. this.createFormGroupStatLabel('Total'),
  961. ])
  962. }
  963.  
  964. /**
  965. * @return {HTMLElement|HTMLDivElement}
  966. */
  967. createStatusSection ()
  968. {
  969. this._statusLine = this.createFormGroupLabel('Status')
  970. this._statusLine.id = this._selectorGenerator.getSelector('status')
  971.  
  972. return this.createFormSection('', [this._statusLine])
  973. }
  974.  
  975. /**
  976. * @param {LocalStore} localStore
  977. * @return {HTMLElement}
  978. */
  979. createStoreFormSection (localStore)
  980. {
  981. return this.createFormSection('Cached Configuration', [
  982. this.createFormActions([
  983. this.createFormSectionButton(
  984. 'Update', 'Save UI settings in store', () => localStore.save(), 'Saves applied settings.'),
  985. this.createFormSectionButton(
  986. 'Purge', 'Purge store', () => localStore.delete(), 'Removes saved settings. Settings will then be sourced from the defaults defined in the script.'),
  987. ]),
  988. ])
  989. }
  990.  
  991. /**
  992. * @param {string} tabName
  993. * @return {HTMLButtonElement}
  994. */
  995. createTabButton (tabName)
  996. {
  997. let button = document.createElement('button')
  998. button.classList.add('tab-button')
  999. button.textContent = tabName
  1000. button.addEventListener('click', (event) => {
  1001.  
  1002. let button = event.currentTarget
  1003. let tabsSection = button.closest('.tabs-section')
  1004. let tabToOpen = tabsSection.querySelector('#' + toKebabCase(tabName))
  1005.  
  1006. for (let tabButton of tabsSection.querySelectorAll('.tab-button')) {
  1007. tabButton.classList.remove('active')
  1008. }
  1009. for (let tabPanel of tabsSection.querySelectorAll('.tab-panel')) {
  1010. tabPanel.classList.remove('active')
  1011. }
  1012.  
  1013. button.classList.add('active')
  1014. tabToOpen.classList.add('active')
  1015. })
  1016. return button
  1017. }
  1018.  
  1019. /**
  1020. * @param {string} tabName
  1021. * @param {HTMLElement[]} children
  1022. * @return {HTMLElement|HTMLDivElement}
  1023. */
  1024. createTabPanel (tabName, children)
  1025. {
  1026. let panel = document.createElement('div')
  1027. panel.id = toKebabCase(tabName)
  1028. panel.classList.add('tab-panel')
  1029.  
  1030. return UIGenerator.populateChildren(panel, children)
  1031. }
  1032.  
  1033. /**
  1034. * @param {string[]} tabNames
  1035. * @param {HTMLElement[]} tabPanels
  1036. * @return {HTMLElement|HTMLDivElement}
  1037. */
  1038. createTabsSection (tabNames, tabPanels)
  1039. {
  1040. let wrapper = document.createElement('div')
  1041. wrapper.classList.add('tabs-section')
  1042.  
  1043. let tabsDiv = document.createElement('div')
  1044. tabsDiv.classList.add('tabs-nav')
  1045.  
  1046. let tabButtons = []
  1047. for (let tabName of tabNames) {
  1048. tabButtons.push(this.createTabButton(tabName))
  1049. }
  1050.  
  1051. UIGenerator.populateChildren(tabsDiv, tabButtons)
  1052. UIGenerator.populateChildren(wrapper, [tabsDiv, ...tabPanels])
  1053. tabButtons[0].click()
  1054.  
  1055. return wrapper
  1056. }
  1057.  
  1058. /**
  1059. * @param {string} label
  1060. * @return {HTMLElement}
  1061. */
  1062. getSettingsInput (label)
  1063. {
  1064. return document.getElementById(this._selectorGenerator.getSettingsInputSelector(label))
  1065. }
  1066.  
  1067. /**
  1068. * @param {string} label
  1069. * @return {boolean}
  1070. */
  1071. getSettingsInputCheckedStatus (label)
  1072. {
  1073. return this.getSettingsInput(label).checked
  1074. }
  1075.  
  1076. /**
  1077. * @param {string} label
  1078. * @return {*}
  1079. */
  1080. getSettingsInputValue (label)
  1081. {
  1082. return this.getSettingsInput(label).value
  1083. }
  1084.  
  1085. /**
  1086. * @param {string} label
  1087. * @param {boolean} getMinInput
  1088. * @return {HTMLElement}
  1089. */
  1090. getSettingsRangeInput (label, getMinInput)
  1091. {
  1092. return document.getElementById(this._selectorGenerator.getSettingsRangeInputSelector(label, getMinInput))
  1093. }
  1094.  
  1095. /**
  1096. * @param {string} label
  1097. * @param {boolean} getMinInputValue
  1098. * @return {*}
  1099. */
  1100. getSettingsRangeInputValue (label, getMinInputValue)
  1101. {
  1102. return this.getSettingsRangeInput(label, getMinInputValue).value
  1103. }
  1104.  
  1105. resetStatus ()
  1106. {
  1107. this._statusLine.textContent = this._statusText
  1108. }
  1109.  
  1110. /**
  1111. * @param {string} label
  1112. * @param {boolean} bool
  1113. */
  1114. setSettingsInputCheckedStatus (label, bool)
  1115. {
  1116. this.getSettingsInput(label).checked = bool
  1117. }
  1118.  
  1119. /**
  1120. * @param {string} label
  1121. * @param {*} value
  1122. */
  1123. setSettingsInputValue (label, value)
  1124. {
  1125. this.getSettingsInput(label).value = value
  1126. }
  1127.  
  1128. /**
  1129. * @param {string} label
  1130. * @param {number} lowerBound
  1131. * @param {number} upperBound
  1132. */
  1133. setSettingsRangeInputValue (label, lowerBound, upperBound)
  1134. {
  1135. this.getSettingsRangeInput(label, true).value = lowerBound
  1136. this.getSettingsRangeInput(label, false).value = upperBound
  1137. }
  1138.  
  1139. /**
  1140. * @param {string} status
  1141. * @param {boolean} transient
  1142. */
  1143. updateStatus (status, transient = false)
  1144. {
  1145. if (!transient) {
  1146. this._statusText = status
  1147. }
  1148. this._statusLine.textContent = status
  1149. }
  1150. }
  1151.  
  1152. class Validator
  1153. {
  1154. static iFramesRemover ()
  1155. {
  1156. GM_addStyle(' iframe { display: none !important; } ')
  1157. }
  1158.  
  1159. /**
  1160. * @param {StatisticsRecorder} statisticsRecorder
  1161. */
  1162. constructor (statisticsRecorder)
  1163. {
  1164. /**
  1165. * @type {Array}
  1166. * @private
  1167. */
  1168. this._filters = []
  1169.  
  1170. /**
  1171. * @type {RegExp|null}
  1172. * @private
  1173. */
  1174. this._optimizedBlacklist = null
  1175.  
  1176. /**
  1177. * @type {Object}
  1178. * @private
  1179. */
  1180. this._optimizedSanitizationRules = {}
  1181.  
  1182. /**
  1183. * @type {StatisticsRecorder}
  1184. * @private
  1185. */
  1186. this._statisticsRecorder = statisticsRecorder
  1187. }
  1188.  
  1189. _buildWholeWordMatchingRegex (words)
  1190. {
  1191. let patternedWords = []
  1192. for (let i = 0; i < words.length; i++) {
  1193. patternedWords.push('\\b' + words[i] + '\\b')
  1194. }
  1195. return new RegExp('(' + patternedWords.join('|') + ')', 'gi')
  1196. }
  1197.  
  1198. /**
  1199. * @param {string} text
  1200. * @return {string}
  1201. */
  1202. sanitize (text)
  1203. {
  1204. for (const substitute in this._optimizedSanitizationRules) {
  1205. text = text.replace(this._optimizedSanitizationRules[substitute], substitute)
  1206. }
  1207. return text.trim()
  1208. }
  1209.  
  1210. /**
  1211. * @param {HTMLElement} textNode
  1212. * @return {Validator}
  1213. */
  1214. sanitizeTextNode (textNode)
  1215. {
  1216. textNode.textContent = this.sanitize(textNode.textContent)
  1217. return this
  1218. }
  1219.  
  1220. /**
  1221. * @param {string} selector
  1222. * @return {Validator}
  1223. */
  1224. sanitizeNodeOfSelector (selector)
  1225. {
  1226. let node = document.querySelector(selector)
  1227. if (node) {
  1228. let sanitizedText = this.sanitize(node.textContent)
  1229. node.textContent = sanitizedText
  1230. document.title = sanitizedText
  1231. }
  1232. return this
  1233. }
  1234.  
  1235. /**
  1236. * @param {string[]} blacklistedWords
  1237. * @return {Validator}
  1238. */
  1239. setBlacklist (blacklistedWords)
  1240. {
  1241. this._optimizedBlacklist = blacklistedWords.length ? this._buildWholeWordMatchingRegex(blacklistedWords) : null
  1242. return this
  1243. }
  1244.  
  1245. /**
  1246. * @param {Object} sanitizationRules
  1247. * @return {Validator}
  1248. */
  1249. setSanitizationRules (sanitizationRules)
  1250. {
  1251. for (const substitute in sanitizationRules) {
  1252. this._optimizedSanitizationRules[substitute] = this._buildWholeWordMatchingRegex(sanitizationRules[substitute])
  1253. }
  1254. return this
  1255. }
  1256.  
  1257. /**
  1258. * @param {string} text
  1259. * @return {boolean}
  1260. */
  1261. validateBlackList (text)
  1262. {
  1263. let validationCheck = true
  1264.  
  1265. if (this._optimizedBlacklist) {
  1266. validationCheck = text.match(this._optimizedBlacklist) === null
  1267. this._statisticsRecorder.record('Blacklist', validationCheck)
  1268. }
  1269. return validationCheck
  1270. }
  1271.  
  1272. /**
  1273. * @param {string} name
  1274. * @param {Node|HTMLElement} item
  1275. * @param {string} selector
  1276. * @return {boolean}
  1277. */
  1278. validateNodeExistence (name, item, selector)
  1279. {
  1280. let validationCheck = item.querySelector(selector) !== null
  1281. this._statisticsRecorder.record(name, validationCheck)
  1282.  
  1283. return validationCheck
  1284. }
  1285.  
  1286. /**
  1287. * @param {string} name
  1288. * @param {Node|HTMLElement} item
  1289. * @param {string} selector
  1290. * @return {boolean}
  1291. */
  1292. validateNodeNonExistence (name, item, selector)
  1293. {
  1294. let validationCheck = item.querySelector(selector) === null
  1295. this._statisticsRecorder.record(name, validationCheck)
  1296.  
  1297. return validationCheck
  1298. }
  1299.  
  1300. /**
  1301. * @param {string} name
  1302. * @param {number} value
  1303. * @param {number[]} bounds
  1304. * @return {boolean}
  1305. */
  1306. validateRange (name, value, bounds)
  1307. {
  1308. let validationCheck = true
  1309.  
  1310. if (bounds[0] > 0 && bounds[1] > 0) {
  1311. validationCheck = value >= bounds[0] && value <= bounds[1]
  1312. } else {
  1313. if (bounds[0] > 0) {
  1314. validationCheck = value >= bounds[0]
  1315. }
  1316. if (bounds[1] > 0) {
  1317. validationCheck = value <= bounds[1]
  1318. }
  1319. }
  1320. this._statisticsRecorder.record(name, validationCheck)
  1321.  
  1322. return validationCheck
  1323. }
  1324.  
  1325. /**
  1326. * @param {string} name
  1327. * @param {number} lowerBound
  1328. * @param {number} upperBound
  1329. * @param getValueCallback
  1330. * @return {boolean}
  1331. */
  1332. validateRangeFilter (name, lowerBound, upperBound, getValueCallback)
  1333. {
  1334. if (lowerBound > 0 || upperBound > 0) {
  1335. return this.validateRange(name, getValueCallback(), [lowerBound, upperBound])
  1336. }
  1337. return true
  1338. }
  1339. }
  1340.  
  1341. class PresetSwitcher
  1342. {
  1343. /**
  1344. * @param {string} scriptPrefix
  1345. * @param {Object} defaultPreset
  1346. * @param {Object} globalConfiguration
  1347. */
  1348. static create (scriptPrefix, defaultPreset, globalConfiguration)
  1349. {
  1350. return new PresetSwitcher(scriptPrefix, defaultPreset, globalConfiguration)
  1351. }
  1352.  
  1353. /**
  1354. * @param {string} scriptPrefix
  1355. * @param {Object} defaultPreset
  1356. * @param {Object} globalConfiguration
  1357. */
  1358. constructor (scriptPrefix, defaultPreset, globalConfiguration)
  1359. {
  1360. /**
  1361. * @type {Object}
  1362. * @private
  1363. */
  1364. this._appliedPreset = null
  1365.  
  1366. /**
  1367. * @type {Object}
  1368. * @private
  1369. */
  1370. this._defaultPreset = defaultPreset
  1371.  
  1372. /**
  1373. * {LocalStore}
  1374. */
  1375. this._globalConfigurationStore = LocalStore.createGlobalConfigStore(scriptPrefix, globalConfiguration)
  1376.  
  1377. /**
  1378. * {Object}
  1379. */
  1380. this._globalConfiguration = this._globalConfigurationStore.retrieve().get()
  1381.  
  1382. /**
  1383. * @type {LocalStore}
  1384. * @private
  1385. */
  1386. this._presetsStore = LocalStore.createPresetConfigStore(scriptPrefix, defaultPreset)
  1387.  
  1388. /**
  1389. * @type {{name: string, config: Object}[]}
  1390. * @private
  1391. */
  1392. this._presets = this._presetsStore.retrieve().get()
  1393.  
  1394. /**
  1395. * @type {string}
  1396. * @private
  1397. */
  1398. this._scriptPrefix = scriptPrefix
  1399. }
  1400.  
  1401. /**
  1402. * @param {string} name
  1403. * @param {Object} config
  1404. * @return {this}
  1405. */
  1406. createPreset (name, config)
  1407. {
  1408. this._presets.push({
  1409. name: name,
  1410. config: config,
  1411. })
  1412. this._presetsStore.update(this._presets)
  1413. return this
  1414. }
  1415.  
  1416. /**
  1417. * @param {string} name
  1418. * @return {this}
  1419. */
  1420. deletePreset (name)
  1421. {
  1422. for (let i = 0; i < this._presets.length; i++) {
  1423. if (this._presets[i].name === name) {
  1424. this._presets.splice(i, 1)
  1425. this._presetsStore.update(this._presets)
  1426. break
  1427. }
  1428. }
  1429. return this
  1430. }
  1431.  
  1432. /**
  1433. * @param name
  1434. * @return {{name: string, config: Object}|null}
  1435. */
  1436. findPreset (name)
  1437. {
  1438. for (let preset of this._presets) {
  1439. if (preset.name === name) {
  1440. return preset
  1441. }
  1442. }
  1443. return null
  1444. }
  1445.  
  1446. /**
  1447. * @return {{name: string, config: Object}}
  1448. */
  1449. getAppliedPreset ()
  1450. {
  1451. return this._appliedPreset
  1452. }
  1453. }
  1454.  
  1455. class BaseHandler
  1456. {
  1457. static initialize ()
  1458. {
  1459. BaseHandler.throwOverrideError()
  1460. //return (new XNXXSearchFilters).init()
  1461. }
  1462.  
  1463. static throwOverrideError ()
  1464. {
  1465. throw new Error('override this method')
  1466. }
  1467.  
  1468. /**
  1469. * @param {string} scriptPrefix
  1470. * @param {string} itemClass
  1471. * @param {Object} settingsDefaults
  1472. */
  1473. constructor (scriptPrefix, itemClass, settingsDefaults)
  1474. {
  1475. settingsDefaults.disableItemComplianceValidation = false
  1476. settingsDefaults.showUIAlways = false
  1477.  
  1478. /**
  1479. * Array of item compliance filters ordered in intended sequence of execution
  1480. * @type {Function[]}
  1481. * @protected
  1482. */
  1483. this._complianceFilters = []
  1484.  
  1485. /**
  1486. * @type {string}
  1487. * @protected
  1488. */
  1489. this._itemClass = itemClass
  1490.  
  1491. /**
  1492. * Operations to perform after script initialization
  1493. * @type {Function}
  1494. * @protected
  1495. */
  1496. this._onAfterInitialization = null
  1497.  
  1498. /**
  1499. * Operations to perform after UI generation
  1500. * @type {Function}
  1501. * @protected
  1502. */
  1503. this._onAfterUIBuild = null
  1504.  
  1505. /**
  1506. * Operations to perform before UI generation
  1507. * @type {Function}
  1508. * @protected
  1509. */
  1510. this._onBeforeUIBuild = null
  1511.  
  1512. /**
  1513. * Operations to perform after compliance checks, the first time a item is retrieved
  1514. * @type {Function}
  1515. * @protected
  1516. */
  1517. this._onFirstHitAfterCompliance = null
  1518.  
  1519. /**
  1520. * Operations to perform before compliance checks, the first time a item is retrieved
  1521. * @type {Function}
  1522. * @protected
  1523. */
  1524. this._onFirstHitBeforeCompliance = null
  1525.  
  1526. /**
  1527. * Get item lists from the page
  1528. * @type {Function}
  1529. * @protected
  1530. */
  1531. this._onGetItemLists = null
  1532.  
  1533. /**
  1534. * Logic to hide a non-compliant item
  1535. * @type {Function}
  1536. * @protected
  1537. */
  1538. this._onItemHide = (item) => {item.style.display = 'none'}
  1539.  
  1540. /**
  1541. * Logic to show compliant item
  1542. * @type {Function}
  1543. * @protected
  1544. */
  1545. this._onItemShow = (item) => {item.style.display = 'inline-block'}
  1546.  
  1547. /**
  1548. * Retrieve settings from UI and update settings object
  1549. * @type {Function}
  1550. * @private
  1551. */
  1552. this._onSettingsApply = null
  1553.  
  1554. /**
  1555. * Settings to update in the UI or elsewhere when settings store is updated
  1556. * @type {Function}
  1557. * @protected
  1558. */
  1559. this._onSettingsStoreUpdate = null
  1560.  
  1561. /**
  1562. * Must return the generated settings section node
  1563. * @type {Function}
  1564. * @protected
  1565. */
  1566. this._onUIBuild = null
  1567.  
  1568. /**
  1569. * Validate initiating initialization.
  1570. * Can be used to stop script init on specific pages or vice versa
  1571. * @type {Function}
  1572. * @protected
  1573. */
  1574. this._onValidateInit = () => true
  1575.  
  1576. /**
  1577. * @type {string}
  1578. * @private
  1579. */
  1580. this._scriptPrefix = scriptPrefix
  1581.  
  1582. /**
  1583. * Local storage store with defaults
  1584. * @type {LocalStore}
  1585. * @protected
  1586. */
  1587. this._settingsStore = new LocalStore(this._scriptPrefix + 'settings', settingsDefaults)
  1588.  
  1589. /**
  1590. * @type {Object}
  1591. * @protected
  1592. */
  1593. this._settings = this._settingsStore.retrieve().get()
  1594.  
  1595. /**
  1596. * @type {StatisticsRecorder}
  1597. * @protected
  1598. */
  1599. this._statistics = new StatisticsRecorder(this._scriptPrefix)
  1600.  
  1601. /**
  1602. * @type {UIGenerator}
  1603. * @protected
  1604. */
  1605. this._uiGen = new UIGenerator(this._settings.showUIAlways, this._scriptPrefix)
  1606.  
  1607. /**
  1608. * @type {Validator}
  1609. * @protected
  1610. */
  1611. this._validator = (new Validator(this._statistics))
  1612. }
  1613.  
  1614. /**
  1615. * @param {Function} eventHandler
  1616. * @param {*} parameters
  1617. * @return {null|NodeListOf<HTMLElement>|*}
  1618. * @private
  1619. */
  1620. _callEventHandler (eventHandler, ...parameters)
  1621. {
  1622. if (eventHandler) {
  1623. return eventHandler(...parameters)
  1624. }
  1625. return null
  1626. }
  1627.  
  1628. /**
  1629. * Filters items as per settings
  1630. * @param {HTMLElement|NodeList<HTMLElement>} itemsList
  1631. * @protected
  1632. */
  1633. _complyItemsList (itemsList)
  1634. {
  1635. for (let item of this._getItemsFromItemsList(itemsList)) {
  1636.  
  1637. if (typeof item.scriptProcessedOnce === 'undefined') {
  1638. item.scriptProcessedOnce = false
  1639. this._callEventHandler(this._onFirstHitBeforeCompliance, item)
  1640. }
  1641.  
  1642. this._validateItemCompliance(item)
  1643.  
  1644. if (!item.scriptProcessedOnce) {
  1645. this._callEventHandler(this._onFirstHitAfterCompliance, item)
  1646. item.scriptProcessedOnce = true
  1647. }
  1648.  
  1649. this._statistics.updateUI()
  1650. }
  1651. }
  1652.  
  1653. /**
  1654. * @protected
  1655. */
  1656. _createSettingsFormActions ()
  1657. {
  1658. return this._uiGen.createSettingsFormActions(this._settingsStore, () => {
  1659. this._callEventHandler(this._onSettingsApply)
  1660. this._statistics.reset()
  1661. for (let itemsList of this._callEventHandler(this._onGetItemLists)) {
  1662. this._complyItemsList(itemsList)
  1663. }
  1664. })
  1665. }
  1666.  
  1667. /**
  1668. * @param {HTMLElement|null} UISection
  1669. * @private
  1670. */
  1671. _embedUI (UISection)
  1672. {
  1673. if (UISection) {
  1674. this._uiGen.constructor.appendToBody(UISection)
  1675. this._uiGen.constructor.appendToBody(this._uiGen.createSettingsShowButton('', UISection, true, () => {
  1676. if (!this._settings.showUIAlways) {
  1677. UISection.style.display = 'none'
  1678. }
  1679. }))
  1680. this._callEventHandler(this._onSettingsStoreUpdate)
  1681. }
  1682. }
  1683.  
  1684. /**
  1685. * @param {HTMLElement|NodeList<HTMLElement>} itemsList
  1686. * @return {NodeListOf<HTMLElement>|HTMLElement[]}
  1687. * @protected
  1688. */
  1689. _getItemsFromItemsList (itemsList)
  1690. {
  1691. let items = []
  1692. if (itemsList instanceof NodeList) {
  1693. itemsList.forEach((node) => {
  1694. if (typeof node.classList !== 'undefined' && node.classList.contains(this._itemClass)) {
  1695. items.push(node)
  1696. }
  1697. })
  1698. } else {
  1699. items = itemsList.querySelectorAll('.' + this._itemClass)
  1700. }
  1701. return items
  1702. }
  1703.  
  1704. /**
  1705. * @param {Object} sanitizationRules
  1706. * @return {string}
  1707. * @protected
  1708. */
  1709. _transformSanitizationRulesToText (sanitizationRules)
  1710. {
  1711. let sanitizationRulesText = []
  1712. for (let substitute in sanitizationRules) {
  1713. sanitizationRulesText.push(substitute + '=' + sanitizationRules[substitute].join(','))
  1714. }
  1715. return sanitizationRulesText.join('\n')
  1716. }
  1717.  
  1718. /**
  1719. * @param {string[]} strings
  1720. * @protected
  1721. */
  1722. _trimAndKeepNonEmptyStrings (strings)
  1723. {
  1724. let nonEmptyStrings = []
  1725. for (let string of strings) {
  1726. string = string.trim()
  1727. if (string !== '') {
  1728. nonEmptyStrings.push(string)
  1729. }
  1730. }
  1731. return nonEmptyStrings
  1732. }
  1733.  
  1734. /**
  1735. * @param {string[]} blacklistedWords
  1736. * @protected
  1737. */
  1738. _validateAndSetBlacklistedWords (blacklistedWords)
  1739. {
  1740. this._settings.blacklist = this._trimAndKeepNonEmptyStrings(blacklistedWords)
  1741. this._validator.setBlacklist(this._settings.blacklist)
  1742. }
  1743.  
  1744. /**
  1745. * @param {string[]} sanitizationRules
  1746. * @protected
  1747. */
  1748. _validateAndSetSanitizationRules (sanitizationRules)
  1749. {
  1750. let fragments, validatedTargetWords
  1751. this._settings.sanitize = {}
  1752.  
  1753. for (let sanitizationRule of sanitizationRules) {
  1754. if (sanitizationRule.includes('=')) {
  1755.  
  1756. fragments = sanitizationRule.split('=')
  1757. if (fragments[0] === '') {
  1758. fragments[0] = ' '
  1759. }
  1760.  
  1761. validatedTargetWords = this._trimAndKeepNonEmptyStrings(fragments[1].split(','))
  1762. if (validatedTargetWords.length) {
  1763. this._settings.sanitize[fragments[0]] = validatedTargetWords
  1764. }
  1765. }
  1766. }
  1767. this._validator.setSanitizationRules(this._settings.sanitize)
  1768. }
  1769.  
  1770. /**
  1771. * @param {HTMLElement|Node} item
  1772. * @protected
  1773. */
  1774. _validateItemCompliance (item)
  1775. {
  1776. let itemComplies = true
  1777.  
  1778. if (!this._settings.disableItemComplianceValidation) {
  1779. for (let complianceFilter of this._complianceFilters) {
  1780. if (!complianceFilter(item)) {
  1781. itemComplies = false
  1782. break
  1783. }
  1784. }
  1785. }
  1786. itemComplies ? this._callEventHandler(this._onItemShow, item) : this._callEventHandler(this._onItemHide, item)
  1787. }
  1788.  
  1789. /**
  1790. * Initialize the script and do basic UI removals
  1791. */
  1792. init ()
  1793. {
  1794. try {
  1795. if (this._callEventHandler(this._onValidateInit)) {
  1796.  
  1797. this._callEventHandler(this._onBeforeUIBuild)
  1798. this._embedUI(this._callEventHandler(this._onUIBuild))
  1799. this._callEventHandler(this._onAfterUIBuild)
  1800.  
  1801. for (let itemsList of this._callEventHandler(this._onGetItemLists)) {
  1802. ChildObserver.create().onNodesAdded((itemsAdded) => this._complyItemsList(itemsAdded)).observe(itemsList)
  1803. this._complyItemsList(itemsList)
  1804. }
  1805.  
  1806. this._uiGen.updateStatus('Initial run completed.')
  1807.  
  1808. this._callEventHandler(this._onAfterInitialization)
  1809.  
  1810. this._settingsStore.onChange(() => this._callEventHandler(this._onSettingsStoreUpdate))
  1811. }
  1812. } catch (error) {
  1813. console.error(this._scriptPrefix + 'script encountered an error: ' + error)
  1814. }
  1815. }
  1816. }