- // ==UserScript==
- // @name Brazen Configuration Manager
- // @namespace brazenvoid
- // @version 1.6.0
- // @author brazenvoid
- // @license GPL-3.0-only
- // @description Configuration management and related UI creation module
- // ==/UserScript==
-
- const CONFIG_TYPE_CHECKBOXES_GROUP = 'checkboxes'
- const CONFIG_TYPE_FLAG = 'flag'
- const CONFIG_TYPE_NUMBER = 'number'
- const CONFIG_TYPE_RADIOS_GROUP = 'radios'
- const CONFIG_TYPE_RANGE = 'range'
- const CONFIG_TYPE_RULESET = 'ruleset'
- const CONFIG_TYPE_SELECT = 'select'
- const CONFIG_TYPE_TEXT = 'text'
-
- class BrazenConfigurationManager
- {
- /**
- * @typedef {{title: string, type: string, element: null|JQuery, value: *, maximum: int, minimum: int, options: string[], helpText: string,
- * onFormatForUI: ConfigurationManagerRulesetCallback, onTranslateFromUI: ConfigurationManagerRulesetCallback,
- * onOptimize: ConfigurationManagerRulesetCallback, createElement: Function, setFromUserInterface: Function, updateUserInterface: Function,
- * optimized?: *}} ConfigurationField
- */
-
- /**
- * @callback ConfigurationManagerRulesetCallback
- * @param {*} values
- */
-
- /**
- * @callback ExternalConfigurationChangeCallback
- * @param {BrazenConfigurationManager} manager
- */
-
- /**
- * @param {BrazenUIGenerator} uiGenerator
- */
- constructor(uiGenerator)
- {
- /**
- * @type {{}}
- * @private
- */
- this._config = {}
-
- /**
- * @type {ExternalConfigurationChangeCallback|null}
- * @private
- */
- this._onExternalConfigurationChange = null
-
- /**
- * @type {LocalStore}
- * @private
- */
- this._localStore = null
-
- /**
- * @type {LocalStore}
- * @private
- */
- this._localStoreId = null
-
- /**
- * @type {number}
- * @private
- */
- this._syncedLocalStoreId = 0
-
- /**
- * @type BrazenUIGenerator
- * @private
- */
- this._uiGen = uiGenerator
- }
-
- /**
- * @param {BrazenUIGenerator} uiGenerator
- * @return {BrazenConfigurationManager}
- */
- static create(uiGenerator)
- {
- return new BrazenConfigurationManager(uiGenerator)
- }
-
- /**
- * @param {string} type
- * @param {string} name
- * @param {*} value
- * @param {string|null} helpText
- * @return ConfigurationField
- * @private
- */
- _createField(type, name, value, helpText)
- {
- let fieldKey = this._formatFieldKey(name)
- let field = this._config[fieldKey]
- if (!field) {
- field = {
- element: null,
- helpText: helpText,
- title: name,
- type: type,
- value: value,
- createElement: null,
- setFromUserInterface: null,
- updateUserInterface: null,
- }
- this._config[fieldKey] = field
- } else {
- if (helpText) {
- field.helpText = helpText
- }
- field.value = value
- }
- return field
- }
-
- /**
- * @param {string} name
- * @return {string}
- * @private
- */
- _formatFieldKey(name)
- {
- return Utilities.toKebabCase(name)
- }
-
- /**
- * @param {boolean} ignoreIfDefaultsSet
- * @private
- */
- _syncLocalStore(ignoreIfDefaultsSet)
- {
- let field
- let storeObject = this._localStore.get()
-
- if (!ignoreIfDefaultsSet || !this._localStore.wereDefaultsSet()) {
- for (let key in this._config) {
-
- field = this._config[key]
- if (typeof storeObject[key] !== 'undefined') {
-
- field.value = storeObject[key]
- if (field.type === CONFIG_TYPE_RULESET) {
- field.optimized = Utilities.callEventHandler(field.onOptimize, [field.value])
- }
- }
- }
- this.updateInterface()
- }
- return this
- }
-
- /**
- * @return {{}}
- * @private
- */
- _toStoreObject()
- {
- let storeObject = {}
- for (let key in this._config) {
- storeObject[key] = this._config[key].value
- }
- return storeObject
- }
-
- /**
- * @param id
- * @private
- */
- _updateLocalStoreId(id = null)
- {
- if (id === null) {
- id = Utilities.generateId()
- }
- this._localStoreId.save({id: id})
- this._syncedLocalStoreId = id
- }
-
- /**
- * @param {string} name
- * @param {array} keyValuePairs
- * @param {string} helpText
- * @returns {BrazenConfigurationManager}
- */
- addCheckboxesGroup(name, keyValuePairs, helpText)
- {
- let field = this._createField(CONFIG_TYPE_CHECKBOXES_GROUP, name, [], helpText)
-
- field.options = keyValuePairs
-
- field.createElement = () => {
- field.element = this._uiGen.createFormCheckBoxesGroupSection(field.title, field.options, field.helpText)
- return field.element
- }
- field.setFromUserInterface = () => {
- field.value = []
- field.element.find('input:checked').each((index, element) => {
- field.value.push($(element).attr('data-value'))
- })
- }
- field.updateUserInterface = () => {
- let elements = field.element.find('input')
- for (let key of field.value) {
- elements.filter('[data-value="' + key + '"]').prop('checked', true)
- }
- }
- return this
- }
-
- /**
- * @param {string} name
- * @param {string} helpText
- * @returns {BrazenConfigurationManager}
- */
- addFlagField(name, helpText)
- {
- let field = this._createField(CONFIG_TYPE_FLAG, name, false, helpText)
-
- field.createElement = () => {
- let inputGroup = this._uiGen.createFormInputGroup(field.title, 'checkbox', field.helpText)
- field.element = inputGroup.find('input')
- return inputGroup
- }
- field.setFromUserInterface = () => {
- field.value = field.element.prop('checked')
- }
- field.updateUserInterface = () => {
- field.element.prop('checked', field.value)
- }
- return this
- }
-
- /**
- * @param {string} name
- * @param {int} minimum
- * @param {int} maximum
- * @param {string} helpText
- * @returns {BrazenConfigurationManager}
- */
- addNumberField(name, minimum, maximum, helpText)
- {
- let field = this._createField(CONFIG_TYPE_NUMBER, name, minimum, helpText)
-
- field.minimum = minimum
- field.maximum = maximum
-
- field.createElement = () => {
- let inputGroup = this._uiGen.createFormInputGroup(field.title, 'number', field.helpText).
- attr('min', field.minimum).
- attr('max', field.maximum)
- field.element = inputGroup.find('input')
- return inputGroup
- }
- field.setFromUserInterface = () => {
- field.value = parseInt(field.element.val().toString())
- }
- field.updateUserInterface = () => {
- field.element.val(field.value)
- }
- return this
- }
-
- /**
- * @param {string} name
- * @param {array} keyValuePairs
- * @param {string} helpText
- * @returns {BrazenConfigurationManager}
- */
- addRadiosGroup(name, keyValuePairs, helpText)
- {
- let field = this._createField(CONFIG_TYPE_RADIOS_GROUP, name, keyValuePairs[0][1], helpText)
-
- field.options = keyValuePairs
-
- field.createElement = () => {
- let inputGroup = this._uiGen.createFormRadiosGroupSection(field.title, field.options, field.helpText)
- field.element = inputGroup
- return inputGroup
- }
- field.setFromUserInterface = () => {
- field.value = field.element.find('input:checked').attr('data-value')
- }
- field.updateUserInterface = () => {
- field.element.find('input[data-value="' + field.value + '"]').prop('checked', true).trigger('change')
- }
- return this
- }
-
- /**
- * @param {string} name
- * @param {int} minimum
- * @param {int} maximum
- * @param {string} helpText
- * @returns {BrazenConfigurationManager}
- */
- addRangeField(name, minimum, maximum, helpText)
- {
- let field = this._createField(CONFIG_TYPE_RANGE, name, {minimum: minimum, maximum: minimum}, helpText)
-
- field.minimum = minimum
- field.maximum = maximum
-
- field.createElement = () => {
- let inputGroup = this._uiGen.createFormRangeInputGroup(field.title, 'number', field.minimum, field.maximum,
- field.helpText)
- field.element = inputGroup.find('input')
- return inputGroup
- }
- field.setFromUserInterface = () => {
- field.value = {
- minimum: field.element.first().val(),
- maximum: field.element.last().val(),
- }
- }
- field.updateUserInterface = () => {
- field.element.first().val(field.value.minimum)
- field.element.last().val(field.value.maximum)
- }
- return this
- }
-
- /**
- * @param {string} name
- * @param {number} rows
- * @param {string|null} helpText
- * @param {ConfigurationManagerRulesetCallback} onTranslateFromUI
- * @param {ConfigurationManagerRulesetCallback} onFormatForUI
- * @param {ConfigurationManagerRulesetCallback} onOptimize
- * @return {BrazenConfigurationManager}
- */
- addRulesetField(name, rows, helpText, onTranslateFromUI = null, onFormatForUI = null, onOptimize = null)
- {
- let field = this._createField(CONFIG_TYPE_RULESET, name, [], helpText)
-
- field.optimized = null
- field.onTranslateFromUI = onTranslateFromUI ?? field.onTranslateFromUI
- field.onFormatForUI = onFormatForUI ?? field.onFormatForUI
- field.onOptimize = onOptimize ?? field.onOptimize
-
- field.createElement = () => {
- let inputGroup = this._uiGen.createFormTextAreaGroup(field.title, rows, field.helpText)
- field.element = inputGroup.find('textarea')
- return inputGroup
- }
- field.setFromUserInterface = () => {
- let value = Utilities.trimAndKeepNonEmptyStrings(field.element.val().split(REGEX_LINE_BREAK))
- field.value = Utilities.callEventHandler(field.onTranslateFromUI, [value], value)
- field.optimized = Utilities.callEventHandler(field.onOptimize, [field.value])
- }
- field.updateUserInterface = () => {
- field.element.val(Utilities.callEventHandler(field.onFormatForUI, [field.value], field.value).join('\n'))
- }
- return this
- }
-
- /**
- * @param {string} name
- * @param {array} keyValuePairs
- * @param {string} helpText
- * @returns {BrazenConfigurationManager}
- */
- addSelectField(name, keyValuePairs, helpText)
- {
- let field = this._createField(CONFIG_TYPE_SELECT, name, keyValuePairs[0][1], helpText)
-
- field.options = keyValuePairs
-
- field.createElement = () => {
- let inputGroup = this._uiGen.createFormRadiosGroupSection(field.title, field.options, field.helpText)
- field.element = inputGroup.find('select')
- return inputGroup
- }
- field.setFromUserInterface = () => {
- field.value = field.element.val()
- }
- field.updateUserInterface = () => {
- field.element.val(field.value).trigger('change')
- }
- return this
- }
-
- /**
- * @param {string} name
- * @param {string} helpText
- * @param {string} defaultValue
- * @returns {BrazenConfigurationManager}
- */
- addTextField(name, helpText, defaultValue = '')
- {
- let field = this._createField(CONFIG_TYPE_TEXT, name, defaultValue, helpText)
-
- field.createElement = () => {
- let inputGroup = this._uiGen.createFormInputGroup(field.title, 'text', field.helpText)
- field.element = inputGroup.find('input')
- return inputGroup
- }
- field.setFromUserInterface = () => {
- let value = field.element.val()
- field.value = value === '' ? defaultValue : value
- }
- field.updateUserInterface = () => {
- field.element.val(field.value)
- }
- return this
- }
-
- /**
- * @returns {string}
- */
- backup()
- {
- let backupConfig = this._toStoreObject()
- backupConfig.id = this._syncedLocalStoreId
- return Utilities.objectToJSON(backupConfig)
- }
-
- /**
- * @param {string} name
- * @returns {JQuery}
- */
- createElement(name)
- {
- return this.getFieldOrFail(name).createElement()
- }
-
- /**
- * @param {string} configKey
- * @returns {function(*): boolean}
- */
- generateValidationCallback(configKey)
- {
- let validationCallback
- switch (this.getField(configKey).type) {
- case CONFIG_TYPE_FLAG:
- case CONFIG_TYPE_RADIOS_GROUP:
- case CONFIG_TYPE_SELECT:
- validationCallback = (value) => value
- break
- case CONFIG_TYPE_CHECKBOXES_GROUP:
- validationCallback = (valueKeys) => valueKeys.length
- break
- case CONFIG_TYPE_NUMBER:
- validationCallback = (value) => value > 0
- break
- case CONFIG_TYPE_RANGE:
- validationCallback = (range) => range.minimum > 0 || range.maximum > 0
- break
- case CONFIG_TYPE_RULESET:
- validationCallback = (rules) => rules.length
- break
- case CONFIG_TYPE_TEXT:
- validationCallback = (value) => value.length
- break
- default:
- throw new Error('Associated config type requires explicit validation callback definition.')
- }
- return validationCallback
- }
-
- /**
- * @param {string} name
- * @return {ConfigurationField|null}
- */
- getField(name)
- {
- return this._config[this._formatFieldKey(name)]
- }
-
- /**
- * @param {string} name
- * @return {ConfigurationField}
- */
- getFieldOrFail(name)
- {
- let field = this._config[this._formatFieldKey(name)]
- if (field) {
- return field
- }
- throw new Error('Field named "' + name + '" could not be found')
- }
-
- /**
- * @param {string} name
- * @returns {*}
- */
- getValue(name)
- {
- return this.getFieldOrFail(name).value
- }
-
- /**
- * @param {string} name
- * @return {boolean}
- */
- hasField(name)
- {
- return typeof this.getField(name) !== 'undefined'
- }
-
- /**
- * @param scriptPrefix
- * @return {BrazenConfigurationManager}
- */
- initialize(scriptPrefix)
- {
- this._localStore = new LocalStore(scriptPrefix + 'settings', this._toStoreObject())
- this._localStore.onChange(() => this.updateInterface())
-
- this._localStoreId = new LocalStore(scriptPrefix + 'settings-id', {id: Utilities.generateId()})
- this._syncedLocalStoreId = this._localStoreId.get().id
-
- $(document).on('visibilitychange', () => {
- if (!document.hidden && this._syncedLocalStoreId !== this._localStoreId.get().id) {
- this._syncLocalStore(true)
- Utilities.callEventHandler(this._onExternalConfigurationChange, [this])
- }
- })
- return this._syncLocalStore(true)
- }
-
- /**
- * @param {ExternalConfigurationChangeCallback} eventHandler
- * @return {BrazenConfigurationManager}
- */
- onExternalConfigurationChange(eventHandler)
- {
- this._onExternalConfigurationChange = eventHandler
- return this
- }
-
- /**
- * @param {string} backedUpConfiguration
- */
- restore(backedUpConfiguration)
- {
- let backupConfig = Utilities.objectFromJSON(backedUpConfiguration)
- let id = backupConfig.id
- delete backupConfig.id
-
- this._localStore.save(backupConfig)
- this._syncLocalStore(false)
- this._updateLocalStoreId(id)
-
- return this
- }
-
- /**
- * @return {BrazenConfigurationManager}
- */
- revertChanges()
- {
- return this._syncLocalStore(false)
- }
-
- /**
- * @return {BrazenConfigurationManager}
- */
- save()
- {
- this.update()._localStore.save(this._toStoreObject())
- this._updateLocalStoreId()
-
- return this
- }
-
- /**
- * @return {BrazenConfigurationManager}
- */
- update()
- {
- let field
- for (let fieldName in this._config) {
- field = this._config[fieldName]
- if (field.element) {
- field.setFromUserInterface()
- }
- }
- return this
- }
-
- /**
- * @return {BrazenConfigurationManager}
- */
- updateInterface()
- {
- let field
- for (let fieldName in this._config) {
- field = this._config[fieldName]
- if (field.element) {
- field.updateUserInterface()
- }
- }
- return this
- }
- }