aboutsummaryrefslogtreecommitdiff
path: root/ext/js/pages/settings/generic-setting-controller.js
diff options
context:
space:
mode:
Diffstat (limited to 'ext/js/pages/settings/generic-setting-controller.js')
-rw-r--r--ext/js/pages/settings/generic-setting-controller.js180
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)) {