diff options
Diffstat (limited to 'ext/js/pages/settings/generic-setting-controller.js')
-rw-r--r-- | ext/js/pages/settings/generic-setting-controller.js | 180 |
1 files changed, 150 insertions, 30 deletions
diff --git a/ext/js/pages/settings/generic-setting-controller.js b/ext/js/pages/settings/generic-setting-controller.js index c4104874..3c6104a9 100644 --- a/ext/js/pages/settings/generic-setting-controller.js +++ b/ext/js/pages/settings/generic-setting-controller.js @@ -16,14 +16,19 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import {deserializeError, isObject} from '../../core.js'; import {DocumentUtil} from '../../dom/document-util.js'; import {DOMDataBinder} from '../../dom/dom-data-binder.js'; export class GenericSettingController { + /** + * @param {SettingsController} settingsController + */ constructor(settingsController) { + /** @type {SettingsController} */ this._settingsController = settingsController; + /** @type {import('settings-modifications').OptionsScopeType} */ this._defaultScope = 'profile'; + /** @type {DOMDataBinder<import('generic-setting-controller').ElementMetadata>} */ this._dataBinder = new DOMDataBinder({ selector: '[data-setting]', createElementMetadata: this._createElementMetadata.bind(this), @@ -31,7 +36,8 @@ export class GenericSettingController { getValues: this._getValues.bind(this), setValues: this._setValues.bind(this) }); - this._transforms = new Map([ + /** @type {Map<import('generic-setting-controller').TransformType, import('generic-setting-controller').TransformFunction>} */ + this._transforms = new Map(/** @type {[key: import('generic-setting-controller').TransformType, value: import('generic-setting-controller').TransformFunction][]} */ ([ ['setAttribute', this._setAttribute.bind(this)], ['setVisibility', this._setVisibility.bind(this)], ['splitTags', this._splitTags.bind(this)], @@ -40,41 +46,49 @@ export class GenericSettingController { ['toBoolean', this._toBoolean.bind(this)], ['toString', this._toString.bind(this)], ['conditionalConvert', this._conditionalConvert.bind(this)] - ]); + ])); } + /** */ async prepare() { this._dataBinder.observe(document.body); this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); } + /** */ async refresh() { await this._dataBinder.refresh(); } // Private + /** */ _onOptionsChanged() { this._dataBinder.refresh(); } + /** + * @param {Element} element + * @returns {import('generic-setting-controller').ElementMetadata|undefined} + */ _createElementMetadata(element) { - const {dataset: {setting: path, scope, transform: transformRaw}} = element; - let transforms; - if (typeof transformRaw === 'string') { - transforms = JSON.parse(transformRaw); - if (!Array.isArray(transforms)) { transforms = [transforms]; } - } else { - transforms = []; - } + if (!(element instanceof HTMLElement)) { return void 0; } + const {setting: path, scope, transform: transformRaw} = element.dataset; + if (typeof path !== 'string') { return void 0; } + const scope2 = this._normalizeScope(scope); return { path, - scope, - transforms, + scope: scope2 !== null ? scope2 : this._defaultScope, + transforms: this._getTransformDataArray(transformRaw), transformRaw }; } + /** + * @param {import('generic-setting-controller').ElementMetadata} metadata1 + * @param {import('generic-setting-controller').ElementMetadata} metadata2 + * @returns {boolean} + */ _compareElementMetadata(metadata1, metadata2) { return ( metadata1.path === metadata2.path && @@ -83,45 +97,71 @@ export class GenericSettingController { ); } + /** + * @param {import('dom-data-binder').GetValuesDetails<import('generic-setting-controller').ElementMetadata>[]} targets + * @returns {Promise<import('dom-data-binder').TaskResult[]>} + */ async _getValues(targets) { const defaultScope = this._defaultScope; + /** @type {import('settings-modifications').ScopedRead[]} */ const settingsTargets = []; for (const {metadata: {path, scope}} of targets) { + /** @type {import('settings-modifications').ScopedRead} */ const target = { path, - scope: scope || defaultScope + scope: typeof scope === 'string' ? scope : defaultScope, + optionsContext: null }; settingsTargets.push(target); } return this._transformResults(await this._settingsController.getSettings(settingsTargets), targets); } + /** + * @param {import('dom-data-binder').SetValuesDetails<import('generic-setting-controller').ElementMetadata>[]} targets + * @returns {Promise<import('dom-data-binder').TaskResult[]>} + */ async _setValues(targets) { const defaultScope = this._defaultScope; + /** @type {import('settings-modifications').ScopedModification[]} */ const settingsTargets = []; for (const {metadata: {path, scope, transforms}, value, element} of targets) { const transformedValue = this._applyTransforms(value, transforms, 'pre', element); + /** @type {import('settings-modifications').ScopedModification} */ const target = { path, - scope: scope || defaultScope, + scope: typeof scope === 'string' ? scope : defaultScope, action: 'set', - value: transformedValue + value: transformedValue, + optionsContext: null }; settingsTargets.push(target); } return this._transformResults(await this._settingsController.modifySettings(settingsTargets), targets); } + /** + * @param {import('settings-controller').ModifyResult[]} values + * @param {import('dom-data-binder').GetValuesDetails<import('generic-setting-controller').ElementMetadata>[]|import('dom-data-binder').SetValuesDetails<import('generic-setting-controller').ElementMetadata>[]} targets + * @returns {import('dom-data-binder').TaskResult[]} + */ _transformResults(values, targets) { return values.map((value, i) => { const error = value.error; - if (error) { return deserializeError(error); } + if (error) { return {error: ExtensionError.deserialize(error)}; } const {metadata: {transforms}, element} = targets[i]; const result = this._applyTransforms(value.result, transforms, 'post', element); return {result}; }); } + /** + * @param {unknown} value + * @param {import('generic-setting-controller').TransformData[]} transforms + * @param {import('generic-setting-controller').TransformStep} step + * @param {Element} element + * @returns {unknown} + */ _applyTransforms(value, transforms, step, element) { for (const transform of transforms) { const transformStep = transform.step; @@ -135,6 +175,11 @@ export class GenericSettingController { return value; } + /** + * @param {?Node} node + * @param {number} ancestorDistance + * @returns {?Node} + */ _getAncestor(node, ancestorDistance) { if (ancestorDistance < 0) { return document.documentElement; @@ -145,6 +190,12 @@ export class GenericSettingController { return node; } + /** + * @param {?Node} node + * @param {number|undefined} ancestorDistance + * @param {string|undefined} selector + * @returns {?Node} + */ _getRelativeElement(node, ancestorDistance, selector) { const selectorRoot = ( typeof ancestorDistance === 'number' ? @@ -154,12 +205,17 @@ export class GenericSettingController { if (selectorRoot === null) { return null; } return ( - typeof selector === 'string' ? + typeof selector === 'string' && (selectorRoot instanceof Element || selectorRoot instanceof Document) ? selectorRoot.querySelector(selector) : (selectorRoot === document ? document.documentElement : selectorRoot) ); } + /** + * @param {import('generic-setting-controller').OperationData} operationData + * @param {unknown} lhs + * @returns {unknown} + */ _evaluateSimpleOperation(operationData, lhs) { const {op: operation, value: rhs} = operationData; switch (operation) { @@ -167,18 +223,18 @@ export class GenericSettingController { case '!!': return !!lhs; case '===': return lhs === rhs; case '!==': return lhs !== rhs; - case '>=': return lhs >= rhs; - case '<=': return lhs <= rhs; - case '>': return lhs > rhs; - case '<': return lhs < rhs; + case '>=': return /** @type {number} */ (lhs) >= /** @type {number} */ (rhs); + case '<=': return /** @type {number} */ (lhs) <= /** @type {number} */ (rhs); + case '>': return /** @type {number} */ (lhs) > /** @type {number} */ (rhs); + case '<': return /** @type {number} */ (lhs) < /** @type {number} */ (rhs); case '&&': - for (const operationData2 of rhs) { + for (const operationData2 of /** @type {import('generic-setting-controller').OperationData[]} */ (rhs)) { const result = this._evaluateSimpleOperation(operationData2, lhs); if (!result) { return result; } } return true; case '||': - for (const operationData2 of rhs) { + for (const operationData2 of /** @type {import('generic-setting-controller').OperationData[]} */ (rhs)) { const result = this._evaluateSimpleOperation(operationData2, lhs); if (result) { return result; } } @@ -188,48 +244,112 @@ export class GenericSettingController { } } + /** + * @param {string|undefined} value + * @returns {?import('settings-modifications').OptionsScopeType} + */ + _normalizeScope(value) { + switch (value) { + case 'profile': + case 'global': + return value; + default: + return null; + } + } + + /** + * @param {string|undefined} transformRaw + * @returns {import('generic-setting-controller').TransformData[]} + */ + _getTransformDataArray(transformRaw) { + if (typeof transformRaw === 'string') { + const transforms = JSON.parse(transformRaw); + return Array.isArray(transforms) ? transforms : [transforms]; + } + return []; + } + // Transforms + /** + * @param {unknown} value + * @param {import('generic-setting-controller').SetAttributeTransformData} data + * @param {Element} element + * @returns {unknown} + */ _setAttribute(value, data, element) { const {ancestorDistance, selector, attribute} = data; const relativeElement = this._getRelativeElement(element, ancestorDistance, selector); - if (relativeElement !== null) { + if (relativeElement !== null && relativeElement instanceof Element) { relativeElement.setAttribute(attribute, `${value}`); } return value; } + /** + * @param {unknown} value + * @param {import('generic-setting-controller').SetVisibilityTransformData} data + * @param {Element} element + * @returns {unknown} + */ _setVisibility(value, data, element) { const {ancestorDistance, selector, condition} = data; const relativeElement = this._getRelativeElement(element, ancestorDistance, selector); - if (relativeElement !== null) { + if (relativeElement !== null && relativeElement instanceof HTMLElement) { relativeElement.hidden = !this._evaluateSimpleOperation(condition, value); } return value; } + /** + * @param {unknown} value + * @returns {string[]} + */ _splitTags(value) { return `${value}`.split(/[,; ]+/).filter((v) => !!v); } + /** + * @param {unknown} value + * @returns {string} + */ _joinTags(value) { - return value.join(' '); + return Array.isArray(value) ? value.join(' ') : ''; } + /** + * @param {unknown} value + * @param {import('generic-setting-controller').ToNumberConstraintsTransformData} data + * @returns {number} + */ _toNumber(value, data) { - let {constraints} = data; - if (!isObject(constraints)) { constraints = {}; } - return DocumentUtil.convertElementValueToNumber(value, constraints); + /** @type {import('document-util').ToNumberConstraints} */ + const constraints = typeof data.constraints === 'object' && data.constraints !== null ? data.constraints : {}; + return typeof value === 'string' ? DocumentUtil.convertElementValueToNumber(value, constraints) : 0; } + /** + * @param {string} value + * @returns {boolean} + */ _toBoolean(value) { return (value === 'true'); } + /** + * @param {unknown} value + * @returns {string} + */ _toString(value) { return `${value}`; } + /** + * @param {unknown} value + * @param {import('generic-setting-controller').ConditionalConvertTransformData} data + * @returns {unknown} + */ _conditionalConvert(value, data) { const {cases} = data; if (Array.isArray(cases)) { |