Json schema profile conditions (#758)
* Add clearCache function * Add upgrade * Use schema-based profile condition validation * Update profile conditions settings controller * Remove unnecessary async * Remove old * Remove unused templates
This commit is contained in:
parent
74edf462ab
commit
f3dd2270a5
@ -38,7 +38,7 @@
|
||||
<script src="/bg/js/json-schema.js"></script>
|
||||
<script src="/bg/js/media-utility.js"></script>
|
||||
<script src="/bg/js/options.js"></script>
|
||||
<script src="/bg/js/profile-conditions.js"></script>
|
||||
<script src="/bg/js/profile-conditions2.js"></script>
|
||||
<script src="/bg/js/request-builder.js"></script>
|
||||
<script src="/bg/js/template-renderer.js"></script>
|
||||
<script src="/bg/js/text-source-map.js"></script>
|
||||
|
@ -68,39 +68,39 @@ html:root:not([data-options-general-result-output-mode=merge]) #dict-main-group
|
||||
content: "AND";
|
||||
}
|
||||
|
||||
.input-group .condition-prefix {
|
||||
.condition-prefix {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.input-group .condition-prefix,
|
||||
.input-group .condition-group-separator-label {
|
||||
.condition-prefix,
|
||||
.condition-group-separator-label {
|
||||
width: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
.input-group .condition-group-separator-label {
|
||||
.condition-group-separator-label {
|
||||
padding: 6px 12px;
|
||||
font-weight: bold;
|
||||
display: inline-block;
|
||||
}
|
||||
.input-group .condition-type,
|
||||
.input-group .condition-operator {
|
||||
.condition-type,
|
||||
.condition-operator {
|
||||
width: auto;
|
||||
text-align: center;
|
||||
text-align-last: center;
|
||||
}
|
||||
|
||||
.condition-group>.condition>*:first-child,
|
||||
.condition-list>.condition>*:first-child,
|
||||
.audio-source-list>.audio-source>*:first-child {
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
.condition-group>.condition:nth-child(n+2)>*:first-child,
|
||||
.condition-list>.condition:nth-child(n+2)>*:first-child,
|
||||
.audio-source-list>.audio-source:nth-child(n+2)>*:first-child {
|
||||
border-top-left-radius: 0;
|
||||
}
|
||||
.condition-group>.condition:nth-child(n+2)>div:last-child>button,
|
||||
.condition-list>.condition:nth-child(n+2)>div:last-child>button,
|
||||
.audio-source-list>.audio-source:nth-child(n+2)>*:last-child>button {
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
.condition-group>.condition:nth-last-child(n+2)>div:last-child>button,
|
||||
.condition-list>.condition:nth-last-child(n+2)>div:last-child>button,
|
||||
.audio-source-list>.audio-source:nth-last-child(n+2)>*:last-child>button {
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
@ -110,7 +110,7 @@ html:root:not([data-options-general-result-output-mode=merge]) #dict-main-group
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
.condition-groups>*:last-of-type {
|
||||
.condition-groups>.condition-group:last-child>.condition-group-separator-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
@ -28,13 +28,11 @@
|
||||
* Mecab
|
||||
* ObjectPropertyAccessor
|
||||
* OptionsUtil
|
||||
* ProfileConditions
|
||||
* RequestBuilder
|
||||
* TemplateRenderer
|
||||
* Translator
|
||||
* conditionsTestValue
|
||||
* jp
|
||||
* profileConditionsDescriptor
|
||||
* profileConditionsDescriptorPromise
|
||||
*/
|
||||
|
||||
class Backend {
|
||||
@ -49,6 +47,8 @@ class Backend {
|
||||
this._options = null;
|
||||
this._optionsSchema = null;
|
||||
this._optionsSchemaValidator = new JsonSchemaValidator();
|
||||
this._profileConditionsSchemaCache = [];
|
||||
this._profileConditionsUtil = new ProfileConditions();
|
||||
this._defaultAnkiFieldTemplates = null;
|
||||
this._requestBuilder = new RequestBuilder();
|
||||
this._audioUriBuilder = new AudioUriBuilder({
|
||||
@ -200,8 +200,6 @@ class Backend {
|
||||
}
|
||||
await this._translator.prepare();
|
||||
|
||||
await profileConditionsDescriptorPromise;
|
||||
|
||||
this._optionsSchema = await this._fetchAsset('/bg/data/options-schema.json', true);
|
||||
this._defaultAnkiFieldTemplates = (await this._fetchAsset('/bg/data/default-anki-field-templates.handlebars')).trim();
|
||||
this._options = await OptionsUtil.load();
|
||||
@ -397,6 +395,7 @@ class Backend {
|
||||
}
|
||||
|
||||
async _onApiOptionsSave({source}) {
|
||||
this._clearProfileConditionsSchemaCache();
|
||||
const options = this.getFullOptions();
|
||||
await OptionsUtil.save(options);
|
||||
this._applyOptions(source);
|
||||
@ -1006,35 +1005,32 @@ class Backend {
|
||||
}
|
||||
|
||||
_getProfileFromContext(options, optionsContext) {
|
||||
optionsContext = this._profileConditionsUtil.normalizeContext(optionsContext);
|
||||
|
||||
let index = 0;
|
||||
for (const profile of options.profiles) {
|
||||
const conditionGroups = profile.conditionGroups;
|
||||
if (conditionGroups.length > 0 && this._testConditionGroups(conditionGroups, optionsContext)) {
|
||||
|
||||
let schema;
|
||||
if (index < this._profileConditionsSchemaCache.length) {
|
||||
schema = this._profileConditionsSchemaCache[index];
|
||||
} else {
|
||||
schema = this._profileConditionsUtil.createSchema(conditionGroups);
|
||||
this._profileConditionsSchemaCache.push(schema);
|
||||
}
|
||||
|
||||
if (conditionGroups.length > 0 && this._optionsSchemaValidator.isValid(optionsContext, schema)) {
|
||||
return profile;
|
||||
}
|
||||
++index;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
_testConditionGroups(conditionGroups, data) {
|
||||
if (conditionGroups.length === 0) { return false; }
|
||||
|
||||
for (const conditionGroup of conditionGroups) {
|
||||
const conditions = conditionGroup.conditions;
|
||||
if (conditions.length > 0 && this._testConditions(conditions, data)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
_testConditions(conditions, data) {
|
||||
for (const condition of conditions) {
|
||||
if (!conditionsTestValue(profileConditionsDescriptor, condition.type, condition.operator, condition.value, data)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
_clearProfileConditionsSchemaCache() {
|
||||
this._profileConditionsSchemaCache = [];
|
||||
this._optionsSchemaValidator.clearCache();
|
||||
}
|
||||
|
||||
_checkLastError() {
|
||||
|
@ -181,6 +181,10 @@ class JsonSchemaValidator {
|
||||
return this._getPropertySchema(schema, property, value, null);
|
||||
}
|
||||
|
||||
clearCache() {
|
||||
this._regexCache.clear();
|
||||
}
|
||||
|
||||
// Private
|
||||
|
||||
_getPropertySchema(schema, property, value, path) {
|
||||
|
@ -389,6 +389,10 @@ class OptionsUtil {
|
||||
{
|
||||
async: true,
|
||||
update: this._updateVersion3.bind(this)
|
||||
},
|
||||
{
|
||||
async: false,
|
||||
update: this._updateVersion4.bind(this)
|
||||
}
|
||||
];
|
||||
}
|
||||
@ -459,4 +463,22 @@ class OptionsUtil {
|
||||
}
|
||||
return fieldTemplates;
|
||||
}
|
||||
|
||||
static _updateVersion4(options) {
|
||||
// Version 4 changes:
|
||||
// Options conditions converted to string representations.
|
||||
for (const {conditionGroups} of options.profiles) {
|
||||
for (const {conditions} of conditionGroups) {
|
||||
for (const condition of conditions) {
|
||||
const value = condition.value;
|
||||
condition.value = (
|
||||
Array.isArray(value) ?
|
||||
value.join(', ') :
|
||||
`${value}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
@ -1,178 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2019-2020 Yomichan Authors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* global
|
||||
* Environment
|
||||
*/
|
||||
|
||||
let profileConditionsDescriptor = null;
|
||||
|
||||
const profileConditionsDescriptorPromise = (async () => {
|
||||
function profileConditionTestDomain(urlDomain, domain) {
|
||||
return (
|
||||
urlDomain.endsWith(domain) &&
|
||||
(
|
||||
domain.length === urlDomain.length ||
|
||||
urlDomain[urlDomain.length - domain.length - 1] === '.'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function profileConditionTestDomainList(url, domainList) {
|
||||
const urlDomain = new URL(url).hostname.toLowerCase();
|
||||
for (const domain of domainList) {
|
||||
if (profileConditionTestDomain(urlDomain, domain)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const environment = new Environment();
|
||||
await environment.prepare();
|
||||
|
||||
const modifiers = environment.getInfo().modifiers;
|
||||
const modifierSeparator = modifiers.separator;
|
||||
const modifierKeyValues = modifiers.keys.map(
|
||||
({value, name}) => ({optionValue: value, name})
|
||||
);
|
||||
|
||||
const modifierValueToName = new Map(
|
||||
modifierKeyValues.map(({optionValue, name}) => [optionValue, name])
|
||||
);
|
||||
|
||||
const modifierNameToValue = new Map(
|
||||
modifierKeyValues.map(({optionValue, name}) => [name, optionValue])
|
||||
);
|
||||
|
||||
profileConditionsDescriptor = {
|
||||
popupLevel: {
|
||||
name: 'Popup Level',
|
||||
description: 'Use profile depending on the level of the popup.',
|
||||
placeholder: 'Number',
|
||||
type: 'number',
|
||||
step: 1,
|
||||
defaultValue: 0,
|
||||
defaultOperator: 'equal',
|
||||
transform: (optionValue) => parseInt(optionValue, 10),
|
||||
transformReverse: (transformedOptionValue) => `${transformedOptionValue}`,
|
||||
validateTransformed: (transformedOptionValue) => Number.isFinite(transformedOptionValue),
|
||||
operators: {
|
||||
equal: {
|
||||
name: '=',
|
||||
test: ({depth}, optionValue) => (depth === optionValue)
|
||||
},
|
||||
notEqual: {
|
||||
name: '\u2260',
|
||||
test: ({depth}, optionValue) => (depth !== optionValue)
|
||||
},
|
||||
lessThan: {
|
||||
name: '<',
|
||||
test: ({depth}, optionValue) => (depth < optionValue)
|
||||
},
|
||||
greaterThan: {
|
||||
name: '>',
|
||||
test: ({depth}, optionValue) => (depth > optionValue)
|
||||
},
|
||||
lessThanOrEqual: {
|
||||
name: '\u2264',
|
||||
test: ({depth}, optionValue) => (depth <= optionValue)
|
||||
},
|
||||
greaterThanOrEqual: {
|
||||
name: '\u2265',
|
||||
test: ({depth}, optionValue) => (depth >= optionValue)
|
||||
}
|
||||
}
|
||||
},
|
||||
url: {
|
||||
name: 'URL',
|
||||
description: 'Use profile depending on the URL of the current website.',
|
||||
defaultOperator: 'matchDomain',
|
||||
operators: {
|
||||
matchDomain: {
|
||||
name: 'Matches Domain',
|
||||
placeholder: 'Comma separated list of domains',
|
||||
defaultValue: 'example.com',
|
||||
transformCache: {},
|
||||
transform: (optionValue) => optionValue.split(/[,;\s]+/).map((v) => v.trim().toLowerCase()).filter((v) => v.length > 0),
|
||||
transformReverse: (transformedOptionValue) => transformedOptionValue.join(', '),
|
||||
validateTransformed: (transformedOptionValue) => (transformedOptionValue.length > 0),
|
||||
test: ({url}, transformedOptionValue) => profileConditionTestDomainList(url, transformedOptionValue)
|
||||
},
|
||||
matchRegExp: {
|
||||
name: 'Matches RegExp',
|
||||
placeholder: 'Regular expression',
|
||||
defaultValue: 'example\\.com',
|
||||
transformCache: {},
|
||||
transform: (optionValue) => new RegExp(optionValue, 'i'),
|
||||
transformReverse: (transformedOptionValue) => transformedOptionValue.source,
|
||||
test: ({url}, transformedOptionValue) => (transformedOptionValue !== null && transformedOptionValue.test(url))
|
||||
}
|
||||
}
|
||||
},
|
||||
modifierKeys: {
|
||||
name: 'Modifier Keys',
|
||||
description: 'Use profile depending on the active modifier keys.',
|
||||
values: modifierKeyValues,
|
||||
defaultOperator: 'are',
|
||||
operators: {
|
||||
are: {
|
||||
name: 'are',
|
||||
placeholder: 'Press one or more modifier keys here',
|
||||
defaultValue: [],
|
||||
type: 'keyMulti',
|
||||
keySeparator: modifierSeparator,
|
||||
transformInput: (optionValue) => optionValue
|
||||
.split(modifierSeparator)
|
||||
.filter((v) => v.length > 0)
|
||||
.map((v) => modifierNameToValue.get(v)),
|
||||
transformReverse: (transformedOptionValue) => transformedOptionValue
|
||||
.map((v) => modifierValueToName.get(v))
|
||||
.join(modifierSeparator),
|
||||
test: ({modifierKeys}, optionValue) => areSetsEqual(new Set(modifierKeys), new Set(optionValue))
|
||||
},
|
||||
areNot: {
|
||||
name: 'are not',
|
||||
placeholder: 'Press one or more modifier keys here',
|
||||
defaultValue: [],
|
||||
type: 'keyMulti',
|
||||
keySeparator: modifierSeparator,
|
||||
transformInput: (optionValue) => optionValue
|
||||
.split(modifierSeparator)
|
||||
.filter((v) => v.length > 0)
|
||||
.map((v) => modifierNameToValue.get(v)),
|
||||
transformReverse: (transformedOptionValue) => transformedOptionValue
|
||||
.map((v) => modifierValueToName.get(v))
|
||||
.join(modifierSeparator),
|
||||
test: ({modifierKeys}, optionValue) => !areSetsEqual(new Set(modifierKeys), new Set(optionValue))
|
||||
},
|
||||
include: {
|
||||
name: 'include',
|
||||
type: 'select',
|
||||
defaultValue: 'alt',
|
||||
test: ({modifierKeys}, optionValue) => modifierKeys.includes(optionValue)
|
||||
},
|
||||
notInclude: {
|
||||
name: 'don\'t include',
|
||||
type: 'select',
|
||||
defaultValue: 'alt',
|
||||
test: ({modifierKeys}, optionValue) => !modifierKeys.includes(optionValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
})();
|
@ -1,449 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2019-2020 Yomichan Authors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* global
|
||||
* DocumentUtil
|
||||
* conditionsNormalizeOptionValue
|
||||
*/
|
||||
|
||||
class ConditionsUI {
|
||||
static instantiateTemplate(templateSelector) {
|
||||
const template = document.querySelector(templateSelector);
|
||||
const content = document.importNode(template.content, true);
|
||||
return $(content.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
ConditionsUI.Container = class Container {
|
||||
constructor(conditionDescriptors, conditionNameDefault, conditionGroups, container, addButton) {
|
||||
this.children = [];
|
||||
this.conditionDescriptors = conditionDescriptors;
|
||||
this.conditionNameDefault = conditionNameDefault;
|
||||
this.conditionGroups = conditionGroups;
|
||||
this.container = container;
|
||||
this.addButton = addButton;
|
||||
|
||||
this.container.empty();
|
||||
|
||||
for (const conditionGroup of toIterable(conditionGroups)) {
|
||||
this.children.push(new ConditionsUI.ConditionGroup(this, conditionGroup));
|
||||
}
|
||||
|
||||
this.addButton.on('click', this.onAddConditionGroup.bind(this));
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
for (const child of this.children) {
|
||||
child.cleanup();
|
||||
}
|
||||
|
||||
this.addButton.off('click');
|
||||
this.container.empty();
|
||||
}
|
||||
|
||||
save() {
|
||||
// Override
|
||||
}
|
||||
|
||||
isolate(object) {
|
||||
// Override
|
||||
return object;
|
||||
}
|
||||
|
||||
remove(child) {
|
||||
const index = this.children.indexOf(child);
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
child.cleanup();
|
||||
this.children.splice(index, 1);
|
||||
this.conditionGroups.splice(index, 1);
|
||||
}
|
||||
|
||||
onAddConditionGroup() {
|
||||
const conditionGroup = this.isolate({
|
||||
conditions: [this.createDefaultCondition(this.conditionNameDefault)]
|
||||
});
|
||||
this.conditionGroups.push(conditionGroup);
|
||||
this.save();
|
||||
this.children.push(new ConditionsUI.ConditionGroup(this, conditionGroup));
|
||||
}
|
||||
|
||||
createDefaultCondition(type) {
|
||||
let operator = '';
|
||||
let value = '';
|
||||
if (hasOwn(this.conditionDescriptors, type)) {
|
||||
const conditionDescriptor = this.conditionDescriptors[type];
|
||||
operator = conditionDescriptor.defaultOperator;
|
||||
({value} = this.getOperatorDefaultValue(type, operator));
|
||||
if (typeof value === 'undefined') {
|
||||
value = '';
|
||||
}
|
||||
}
|
||||
return {type, operator, value};
|
||||
}
|
||||
|
||||
getOperatorDefaultValue(type, operator) {
|
||||
if (hasOwn(this.conditionDescriptors, type)) {
|
||||
const conditionDescriptor = this.conditionDescriptors[type];
|
||||
if (hasOwn(conditionDescriptor.operators, operator)) {
|
||||
const operatorDescriptor = conditionDescriptor.operators[operator];
|
||||
if (hasOwn(operatorDescriptor, 'defaultValue')) {
|
||||
return {value: this.isolate(operatorDescriptor.defaultValue), fromOperator: true};
|
||||
}
|
||||
}
|
||||
if (hasOwn(conditionDescriptor, 'defaultValue')) {
|
||||
return {value: this.isolate(conditionDescriptor.defaultValue), fromOperator: false};
|
||||
}
|
||||
}
|
||||
return {fromOperator: false};
|
||||
}
|
||||
};
|
||||
|
||||
ConditionsUI.ConditionGroup = class ConditionGroup {
|
||||
constructor(parent, conditionGroup) {
|
||||
this.parent = parent;
|
||||
this.children = [];
|
||||
this.conditionGroup = conditionGroup;
|
||||
this.container = $('<div>').addClass('condition-group').appendTo(parent.container);
|
||||
this.options = ConditionsUI.instantiateTemplate('#condition-group-options-template').appendTo(parent.container);
|
||||
this.separator = ConditionsUI.instantiateTemplate('#condition-group-separator-template').appendTo(parent.container);
|
||||
this.addButton = this.options.find('.condition-add');
|
||||
|
||||
for (const condition of toIterable(conditionGroup.conditions)) {
|
||||
this.children.push(new ConditionsUI.Condition(this, condition));
|
||||
}
|
||||
|
||||
this.addButton.on('click', this.onAddCondition.bind(this));
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
for (const child of this.children) {
|
||||
child.cleanup();
|
||||
}
|
||||
|
||||
this.addButton.off('click');
|
||||
this.container.remove();
|
||||
this.options.remove();
|
||||
this.separator.remove();
|
||||
}
|
||||
|
||||
save() {
|
||||
this.parent.save();
|
||||
}
|
||||
|
||||
isolate(object) {
|
||||
return this.parent.isolate(object);
|
||||
}
|
||||
|
||||
remove(child) {
|
||||
const index = this.children.indexOf(child);
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
child.cleanup();
|
||||
this.children.splice(index, 1);
|
||||
this.conditionGroup.conditions.splice(index, 1);
|
||||
|
||||
if (this.children.length === 0) {
|
||||
this.parent.remove(this, false);
|
||||
}
|
||||
}
|
||||
|
||||
onAddCondition() {
|
||||
const condition = this.isolate(this.parent.createDefaultCondition(this.parent.conditionNameDefault));
|
||||
this.conditionGroup.conditions.push(condition);
|
||||
this.children.push(new ConditionsUI.Condition(this, condition));
|
||||
}
|
||||
};
|
||||
|
||||
ConditionsUI.Condition = class Condition {
|
||||
constructor(parent, condition) {
|
||||
this.parent = parent;
|
||||
this.condition = condition;
|
||||
this.container = ConditionsUI.instantiateTemplate('#condition-template').appendTo(parent.container);
|
||||
this.input = this.container.find('.condition-input');
|
||||
this.inputInner = null;
|
||||
this.typeSelect = this.container.find('.condition-type');
|
||||
this.operatorSelect = this.container.find('.condition-operator');
|
||||
this.removeButton = this.container.find('.condition-remove');
|
||||
|
||||
this.updateTypes();
|
||||
this.updateOperators();
|
||||
this.updateInput();
|
||||
|
||||
this.typeSelect.on('change', this.onConditionTypeChanged.bind(this));
|
||||
this.operatorSelect.on('change', this.onConditionOperatorChanged.bind(this));
|
||||
this.removeButton.on('click', this.onRemoveClicked.bind(this));
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this.inputInner.off('change');
|
||||
this.typeSelect.off('change');
|
||||
this.operatorSelect.off('change');
|
||||
this.removeButton.off('click');
|
||||
this.container.remove();
|
||||
}
|
||||
|
||||
save() {
|
||||
this.parent.save();
|
||||
}
|
||||
|
||||
isolate(object) {
|
||||
return this.parent.isolate(object);
|
||||
}
|
||||
|
||||
updateTypes() {
|
||||
const conditionDescriptors = this.parent.parent.conditionDescriptors;
|
||||
const optionGroup = this.typeSelect.find('optgroup');
|
||||
optionGroup.empty();
|
||||
for (const type of Object.keys(conditionDescriptors)) {
|
||||
const conditionDescriptor = conditionDescriptors[type];
|
||||
$('<option>').val(type).text(conditionDescriptor.name).appendTo(optionGroup);
|
||||
}
|
||||
this.typeSelect.val(this.condition.type);
|
||||
}
|
||||
|
||||
updateOperators() {
|
||||
const conditionDescriptors = this.parent.parent.conditionDescriptors;
|
||||
const optionGroup = this.operatorSelect.find('optgroup');
|
||||
optionGroup.empty();
|
||||
|
||||
const type = this.condition.type;
|
||||
if (hasOwn(conditionDescriptors, type)) {
|
||||
const conditionDescriptor = conditionDescriptors[type];
|
||||
const operators = conditionDescriptor.operators;
|
||||
for (const operatorName of Object.keys(operators)) {
|
||||
const operatorDescriptor = operators[operatorName];
|
||||
$('<option>').val(operatorName).text(operatorDescriptor.name).appendTo(optionGroup);
|
||||
}
|
||||
}
|
||||
|
||||
this.operatorSelect.val(this.condition.operator);
|
||||
}
|
||||
|
||||
updateInput() {
|
||||
const conditionDescriptors = this.parent.parent.conditionDescriptors;
|
||||
const {type, operator} = this.condition;
|
||||
|
||||
const objects = [];
|
||||
let inputType = null;
|
||||
if (hasOwn(conditionDescriptors, type)) {
|
||||
const conditionDescriptor = conditionDescriptors[type];
|
||||
objects.push(conditionDescriptor);
|
||||
if (hasOwn(conditionDescriptor, 'type')) {
|
||||
inputType = conditionDescriptor.type;
|
||||
}
|
||||
if (hasOwn(conditionDescriptor.operators, operator)) {
|
||||
const operatorDescriptor = conditionDescriptor.operators[operator];
|
||||
objects.push(operatorDescriptor);
|
||||
if (hasOwn(operatorDescriptor, 'type')) {
|
||||
inputType = operatorDescriptor.type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.input.empty();
|
||||
if (inputType === 'select') {
|
||||
this.inputInner = this.createSelectElement(objects);
|
||||
} else if (inputType === 'keyMulti') {
|
||||
this.inputInner = this.createInputKeyMultiElement(objects);
|
||||
} else {
|
||||
this.inputInner = this.createInputElement(objects);
|
||||
}
|
||||
this.inputInner.appendTo(this.input);
|
||||
this.inputInner.on('change', this.onInputChanged.bind(this));
|
||||
|
||||
const {valid, value} = this.validateValue(this.condition.value);
|
||||
this.inputInner.toggleClass('is-invalid', !valid);
|
||||
this.inputInner.val(value);
|
||||
}
|
||||
|
||||
createInputElement(objects) {
|
||||
const inputInner = ConditionsUI.instantiateTemplate('#condition-input-text-template');
|
||||
|
||||
const props = new Map([
|
||||
['placeholder', ''],
|
||||
['type', 'text']
|
||||
]);
|
||||
|
||||
for (const object of objects) {
|
||||
if (hasOwn(object, 'placeholder')) {
|
||||
props.set('placeholder', object.placeholder);
|
||||
}
|
||||
if (object.type === 'number') {
|
||||
props.set('type', 'number');
|
||||
for (const prop of ['step', 'min', 'max']) {
|
||||
if (hasOwn(object, prop)) {
|
||||
props.set(prop, object[prop]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [prop, value] of props.entries()) {
|
||||
inputInner.prop(prop, value);
|
||||
}
|
||||
|
||||
return inputInner;
|
||||
}
|
||||
|
||||
createInputKeyMultiElement(objects) {
|
||||
const inputInner = this.createInputElement(objects);
|
||||
|
||||
inputInner.prop('readonly', true);
|
||||
|
||||
let values = [];
|
||||
let keySeparator = ' + ';
|
||||
for (const object of objects) {
|
||||
if (hasOwn(object, 'values')) {
|
||||
values = object.values;
|
||||
}
|
||||
if (hasOwn(object, 'keySeparator')) {
|
||||
keySeparator = object.keySeparator;
|
||||
}
|
||||
}
|
||||
|
||||
const pressedKeyIndices = new Set();
|
||||
|
||||
const onKeyDown = ({originalEvent}) => {
|
||||
const pressedKeyEventName = DocumentUtil.getKeyFromEvent(originalEvent);
|
||||
if (pressedKeyEventName === 'Escape' || pressedKeyEventName === 'Backspace') {
|
||||
pressedKeyIndices.clear();
|
||||
inputInner.val('');
|
||||
inputInner.change();
|
||||
return;
|
||||
}
|
||||
|
||||
const pressedModifiers = DocumentUtil.getActiveModifiers(originalEvent);
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/metaKey
|
||||
// https://askubuntu.com/questions/567731/why-is-shift-alt-being-mapped-to-meta
|
||||
// It works with mouse events on some platforms, so try to determine if metaKey is pressed
|
||||
// hack; only works when Shift and Alt are not pressed
|
||||
const isMetaKeyChrome = (
|
||||
pressedKeyEventName === 'Meta' &&
|
||||
getSetDifference(new Set(['shift', 'alt']), pressedModifiers).size !== 0
|
||||
);
|
||||
if (isMetaKeyChrome) {
|
||||
pressedModifiers.add('meta');
|
||||
}
|
||||
|
||||
for (const modifier of pressedModifiers) {
|
||||
const foundIndex = values.findIndex(({optionValue}) => optionValue === modifier);
|
||||
if (foundIndex !== -1) {
|
||||
pressedKeyIndices.add(foundIndex);
|
||||
}
|
||||
}
|
||||
|
||||
const inputValue = [...pressedKeyIndices].map((i) => values[i].name).join(keySeparator);
|
||||
inputInner.val(inputValue);
|
||||
inputInner.change();
|
||||
};
|
||||
|
||||
inputInner.on('keydown', onKeyDown);
|
||||
|
||||
return inputInner;
|
||||
}
|
||||
|
||||
createSelectElement(objects) {
|
||||
const inputInner = ConditionsUI.instantiateTemplate('#condition-input-select-template');
|
||||
|
||||
const data = new Map([
|
||||
['values', []],
|
||||
['defaultValue', null]
|
||||
]);
|
||||
|
||||
for (const object of objects) {
|
||||
if (hasOwn(object, 'values')) {
|
||||
data.set('values', object.values);
|
||||
}
|
||||
if (hasOwn(object, 'defaultValue')) {
|
||||
data.set('defaultValue', this.isolate(object.defaultValue));
|
||||
}
|
||||
}
|
||||
|
||||
for (const {optionValue, name} of data.get('values')) {
|
||||
const option = ConditionsUI.instantiateTemplate('#condition-input-option-template');
|
||||
option.attr('value', optionValue);
|
||||
option.text(name);
|
||||
option.appendTo(inputInner);
|
||||
}
|
||||
|
||||
const defaultValue = data.get('defaultValue');
|
||||
if (defaultValue !== null) {
|
||||
inputInner.val(this.isolate(defaultValue));
|
||||
}
|
||||
|
||||
return inputInner;
|
||||
}
|
||||
|
||||
validateValue(value, isInput=false) {
|
||||
const conditionDescriptors = this.parent.parent.conditionDescriptors;
|
||||
let valid = true;
|
||||
let inputTransformedValue = null;
|
||||
try {
|
||||
[value, inputTransformedValue] = conditionsNormalizeOptionValue(
|
||||
conditionDescriptors,
|
||||
this.condition.type,
|
||||
this.condition.operator,
|
||||
value,
|
||||
isInput
|
||||
);
|
||||
} catch (e) {
|
||||
valid = false;
|
||||
}
|
||||
return {valid, value, inputTransformedValue};
|
||||
}
|
||||
|
||||
onInputChanged() {
|
||||
const {valid, value, inputTransformedValue} = this.validateValue(this.inputInner.val(), true);
|
||||
this.inputInner.toggleClass('is-invalid', !valid);
|
||||
this.inputInner.val(value);
|
||||
this.condition.value = inputTransformedValue !== null ? inputTransformedValue : value;
|
||||
this.save();
|
||||
}
|
||||
|
||||
onConditionTypeChanged() {
|
||||
const type = this.typeSelect.val();
|
||||
const {operator, value} = this.parent.parent.createDefaultCondition(type);
|
||||
this.condition.type = type;
|
||||
this.condition.operator = operator;
|
||||
this.condition.value = value;
|
||||
this.save();
|
||||
this.updateOperators();
|
||||
this.updateInput();
|
||||
}
|
||||
|
||||
onConditionOperatorChanged() {
|
||||
const type = this.condition.type;
|
||||
const operator = this.operatorSelect.val();
|
||||
const {value, fromOperator} = this.parent.parent.getOperatorDefaultValue(type, operator);
|
||||
this.condition.operator = operator;
|
||||
if (fromOperator) {
|
||||
this.condition.value = value;
|
||||
}
|
||||
this.save();
|
||||
this.updateInput();
|
||||
}
|
||||
|
||||
onRemoveClicked() {
|
||||
this.parent.remove(this);
|
||||
this.save();
|
||||
}
|
||||
};
|
686
ext/bg/js/settings/profile-conditions-ui.js
Normal file
686
ext/bg/js/settings/profile-conditions-ui.js
Normal file
@ -0,0 +1,686 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Yomichan Authors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* global
|
||||
* DocumentUtil
|
||||
*/
|
||||
|
||||
class ProfileConditionsUI {
|
||||
constructor(settingsController) {
|
||||
this._settingsController = settingsController;
|
||||
this._keySeparator = '';
|
||||
this._keyNames = new Map();
|
||||
this._conditionGroupsContainer = null;
|
||||
this._addConditionGroupButton = null;
|
||||
this._children = [];
|
||||
this._eventListeners = new EventListenerCollection();
|
||||
this._defaultType = 'popupLevel';
|
||||
this._descriptors = new Map([
|
||||
[
|
||||
'popupLevel',
|
||||
{
|
||||
displayName: 'Popup Level',
|
||||
defaultOperator: 'equal',
|
||||
operators: new Map([
|
||||
['equal', {displayName: '=', type: 'integer', defaultValue: '0', validate: this._validateInteger.bind(this), normalize: this._normalizeInteger.bind(this)}],
|
||||
['notEqual', {displayName: '\u2260', type: 'integer', defaultValue: '0', validate: this._validateInteger.bind(this), normalize: this._normalizeInteger.bind(this)}],
|
||||
['lessThan', {displayName: '<', type: 'integer', defaultValue: '0', validate: this._validateInteger.bind(this), normalize: this._normalizeInteger.bind(this)}],
|
||||
['greaterThan', {displayName: '>', type: 'integer', defaultValue: '0', validate: this._validateInteger.bind(this), normalize: this._normalizeInteger.bind(this)}],
|
||||
['lessThanOrEqual', {displayName: '\u2264', type: 'integer', defaultValue: '0', validate: this._validateInteger.bind(this), normalize: this._normalizeInteger.bind(this)}],
|
||||
['greaterThanOrEqual', {displayName: '\u2265', type: 'integer', defaultValue: '0', validate: this._validateInteger.bind(this), normalize: this._normalizeInteger.bind(this)}]
|
||||
])
|
||||
}
|
||||
],
|
||||
[
|
||||
'url',
|
||||
{
|
||||
displayName: 'URL',
|
||||
defaultOperator: 'matchDomain',
|
||||
operators: new Map([
|
||||
['matchDomain', {displayName: 'Matches Domain', type: 'string', defaultValue: 'example.com', resetDefaultOnChange: true, validate: this._validateDomains.bind(this), normalize: this._normalizeDomains.bind(this)}],
|
||||
['matchRegExp', {displayName: 'Matches RegExp', type: 'string', defaultValue: 'example\\.com', resetDefaultOnChange: true, validate: this._validateRegExp.bind(this)}]
|
||||
])
|
||||
}
|
||||
],
|
||||
[
|
||||
'modifierKeys',
|
||||
{
|
||||
displayName: 'Modifier Keys',
|
||||
defaultOperator: 'are',
|
||||
operators: new Map([
|
||||
['are', {displayName: 'Are', type: 'modifierKeys', defaultValue: ''}],
|
||||
['areNot', {displayName: 'Are Not', type: 'modifierKeys', defaultValue: ''}],
|
||||
['include', {displayName: 'Include', type: 'modifierKeys', defaultValue: ''}],
|
||||
['notInclude', {displayName: 'Don\'t Include', type: 'modifierKeys', defaultValue: ''}]
|
||||
])
|
||||
}
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
get settingsController() {
|
||||
return this._settingsController;
|
||||
}
|
||||
|
||||
get index() {
|
||||
return this._settingsController.profileIndex;
|
||||
}
|
||||
|
||||
setKeyInfo(separator, keyNames) {
|
||||
this._keySeparator = separator;
|
||||
this._keyNames.clear();
|
||||
for (const {value, name} of keyNames) {
|
||||
this._keyNames.set(value, name);
|
||||
}
|
||||
}
|
||||
|
||||
prepare(conditionGroups) {
|
||||
this._conditionGroupsContainer = document.querySelector('#profile-condition-groups');
|
||||
this._addConditionGroupButton = document.querySelector('#profile-add-condition-group');
|
||||
|
||||
for (let i = 0, ii = conditionGroups.length; i < ii; ++i) {
|
||||
this._addConditionGroup(conditionGroups[i], i);
|
||||
}
|
||||
|
||||
this._eventListeners.addEventListener(this._addConditionGroupButton, 'click', this._onAddConditionGroupButtonClick.bind(this), false);
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this._eventListeners.removeAllEventListeners();
|
||||
|
||||
for (const child of this._children) {
|
||||
child.cleanup();
|
||||
}
|
||||
this._children = [];
|
||||
|
||||
this._conditionGroupsContainer = null;
|
||||
this._addConditionGroupButton = null;
|
||||
}
|
||||
|
||||
instantiateTemplate(templateSelector) {
|
||||
const template = document.querySelector(templateSelector);
|
||||
const content = document.importNode(template.content, true);
|
||||
return content.firstChild;
|
||||
}
|
||||
|
||||
getDescriptorTypes() {
|
||||
const results = [];
|
||||
for (const [name, {displayName}] of this._descriptors.entries()) {
|
||||
results.push({name, displayName});
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
getDescriptorOperators(type) {
|
||||
const info = this._descriptors.get(type);
|
||||
const results = [];
|
||||
if (typeof info !== 'undefined') {
|
||||
for (const [name, {displayName}] of info.operators.entries()) {
|
||||
results.push({name, displayName});
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
getDefaultType() {
|
||||
return this._defaultType;
|
||||
}
|
||||
|
||||
getDefaultOperator(type) {
|
||||
const info = this._descriptors.get(type);
|
||||
return (typeof info !== 'undefined' ? info.defaultOperator : '');
|
||||
}
|
||||
|
||||
getOperatorDetails(type, operator) {
|
||||
const info = this._getOperatorDetails(type, operator);
|
||||
|
||||
const {
|
||||
displayName=operator,
|
||||
type: type2='string',
|
||||
defaultValue='',
|
||||
resetDefaultOnChange=false,
|
||||
validate=null,
|
||||
normalize=null
|
||||
} = (typeof info === 'undefined' ? {} : info);
|
||||
|
||||
return {
|
||||
displayName,
|
||||
type: type2,
|
||||
defaultValue,
|
||||
resetDefaultOnChange,
|
||||
validate,
|
||||
normalize
|
||||
};
|
||||
}
|
||||
|
||||
getDefaultCondition() {
|
||||
const type = this.getDefaultType();
|
||||
const operator = this.getDefaultOperator(type);
|
||||
const {defaultValue: value} = this.getOperatorDetails(type, operator);
|
||||
return {type, operator, value};
|
||||
}
|
||||
|
||||
removeConditionGroup(child) {
|
||||
const index = child.index;
|
||||
if (index < 0 || index >= this._children.length) { return false; }
|
||||
|
||||
const child2 = this._children[index];
|
||||
if (child !== child2) { return false; }
|
||||
|
||||
this._children.splice(index, 1);
|
||||
child.cleanup();
|
||||
|
||||
for (let i = index, ii = this._children.length; i < ii; ++i) {
|
||||
this._children[i].index = i;
|
||||
}
|
||||
|
||||
this.settingsController.modifyGlobalSettings([{
|
||||
action: 'splice',
|
||||
path: this.getPath('conditionGroups'),
|
||||
start: index,
|
||||
deleteCount: 1,
|
||||
items: []
|
||||
}]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
splitValue(value) {
|
||||
return value.split(/[,;\s]+/).map((v) => v.trim().toLowerCase()).filter((v) => v.length > 0);
|
||||
}
|
||||
|
||||
getModifierKeyStrings(modifiers) {
|
||||
let value = '';
|
||||
let displayValue = '';
|
||||
let first = true;
|
||||
for (const modifier of modifiers) {
|
||||
let keyName = this._keyNames.get(modifier);
|
||||
if (typeof keyName === 'undefined') { keyName = modifier; }
|
||||
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
value += ', ';
|
||||
displayValue += this._keySeparator;
|
||||
}
|
||||
value += modifier;
|
||||
displayValue += keyName;
|
||||
}
|
||||
return {value, displayValue};
|
||||
}
|
||||
|
||||
sortModifiers(modifiers) {
|
||||
return modifiers.sort();
|
||||
}
|
||||
|
||||
getPath(property) {
|
||||
property = (typeof property === 'string' ? `.${property}` : '');
|
||||
return `profiles[${this.index}]${property}`;
|
||||
}
|
||||
|
||||
// Private
|
||||
|
||||
_onAddConditionGroupButtonClick() {
|
||||
const conditionGroup = {
|
||||
conditions: [this.getDefaultCondition()]
|
||||
};
|
||||
const index = this._children.length;
|
||||
|
||||
this._addConditionGroup(conditionGroup, index);
|
||||
|
||||
this.settingsController.modifyGlobalSettings([{
|
||||
action: 'splice',
|
||||
path: this.getPath('conditionGroups'),
|
||||
start: index,
|
||||
deleteCount: 0,
|
||||
items: [conditionGroup]
|
||||
}]);
|
||||
}
|
||||
|
||||
_addConditionGroup(conditionGroup, index) {
|
||||
const child = new ProfileConditionGroupUI(this, index);
|
||||
child.prepare(conditionGroup);
|
||||
this._children.push(child);
|
||||
this._conditionGroupsContainer.appendChild(child.node);
|
||||
return child;
|
||||
}
|
||||
|
||||
_getOperatorDetails(type, operator) {
|
||||
const info = this._descriptors.get(type);
|
||||
return (typeof info !== 'undefined' ? info.operators.get(operator) : void 0);
|
||||
}
|
||||
|
||||
_validateInteger(value) {
|
||||
const number = Number.parseFloat(value);
|
||||
return Number.isFinite(number) && Math.floor(number) === number;
|
||||
}
|
||||
|
||||
_validateDomains(value) {
|
||||
return this.splitValue(value).length > 0;
|
||||
}
|
||||
|
||||
_validateRegExp(value) {
|
||||
try {
|
||||
new RegExp(value, 'i');
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
_normalizeInteger(value) {
|
||||
const number = Number.parseFloat(value);
|
||||
return `${number}`;
|
||||
}
|
||||
|
||||
_normalizeDomains(value) {
|
||||
return this.splitValue(value).join(', ');
|
||||
}
|
||||
}
|
||||
|
||||
class ProfileConditionGroupUI {
|
||||
constructor(parent, index) {
|
||||
this._parent = parent;
|
||||
this._index = index;
|
||||
this._node = null;
|
||||
this._conditionContainer = null;
|
||||
this._addConditionButton = null;
|
||||
this._children = [];
|
||||
this._eventListeners = new EventListenerCollection();
|
||||
}
|
||||
|
||||
get settingsController() {
|
||||
return this._parent.settingsController;
|
||||
}
|
||||
|
||||
get parent() {
|
||||
return this._parent;
|
||||
}
|
||||
|
||||
get index() {
|
||||
return this._index;
|
||||
}
|
||||
|
||||
set index(value) {
|
||||
this._index = value;
|
||||
}
|
||||
|
||||
get node() {
|
||||
return this._node;
|
||||
}
|
||||
|
||||
prepare(conditionGroup) {
|
||||
this._node = this._parent.instantiateTemplate('#condition-group-template');
|
||||
this._conditionContainer = this._node.querySelector('.condition-list');
|
||||
this._addConditionButton = this._node.querySelector('.condition-add');
|
||||
|
||||
const conditions = conditionGroup.conditions;
|
||||
for (let i = 0, ii = conditions.length; i < ii; ++i) {
|
||||
this._addCondition(conditions[i], i);
|
||||
}
|
||||
|
||||
this._eventListeners.addEventListener(this._addConditionButton, 'click', this._onAddConditionButtonClick.bind(this), false);
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this._eventListeners.removeAllEventListeners();
|
||||
|
||||
for (const child of this._children) {
|
||||
child.cleanup();
|
||||
}
|
||||
this._children = [];
|
||||
|
||||
if (this._node === null) { return; }
|
||||
|
||||
const node = this._node;
|
||||
this._node = null;
|
||||
this._conditionContainer = null;
|
||||
this._addConditionButton = null;
|
||||
|
||||
if (node.parentNode !== null) {
|
||||
node.parentNode.removeChild(node);
|
||||
}
|
||||
}
|
||||
|
||||
removeCondition(child) {
|
||||
const index = child.index;
|
||||
if (index < 0 || index >= this._children.length) { return false; }
|
||||
|
||||
const child2 = this._children[index];
|
||||
if (child !== child2) { return false; }
|
||||
|
||||
this._children.splice(index, 1);
|
||||
child.cleanup();
|
||||
|
||||
for (let i = index, ii = this._children.length; i < ii; ++i) {
|
||||
this._children[i].index = i;
|
||||
}
|
||||
|
||||
this.settingsController.modifyGlobalSettings([{
|
||||
action: 'splice',
|
||||
path: this.getPath('conditions'),
|
||||
start: index,
|
||||
deleteCount: 1,
|
||||
items: []
|
||||
}]);
|
||||
|
||||
if (this._children.length === 0) {
|
||||
this._parent.removeConditionGroup(this);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getPath(property) {
|
||||
property = (typeof property === 'string' ? `.${property}` : '');
|
||||
return this._parent.getPath(`conditionGroups[${this._index}]${property}`);
|
||||
}
|
||||
|
||||
// Private
|
||||
|
||||
_onAddConditionButtonClick() {
|
||||
const condition = this._parent.getDefaultCondition();
|
||||
const index = this._children.length;
|
||||
|
||||
this._addCondition(condition, index);
|
||||
|
||||
this.settingsController.modifyGlobalSettings([{
|
||||
action: 'splice',
|
||||
path: this.getPath('conditions'),
|
||||
start: index,
|
||||
deleteCount: 0,
|
||||
items: [condition]
|
||||
}]);
|
||||
}
|
||||
|
||||
_addCondition(condition, index) {
|
||||
const child = new ProfileConditionUI(this, index);
|
||||
child.prepare(condition);
|
||||
this._children.push(child);
|
||||
this._conditionContainer.appendChild(child.node);
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
||||
class ProfileConditionUI {
|
||||
constructor(parent, index) {
|
||||
this._parent = parent;
|
||||
this._index = index;
|
||||
this._node = null;
|
||||
this._typeInput = null;
|
||||
this._operatorInput = null;
|
||||
this._valueInputContainer = null;
|
||||
this._removeButton = null;
|
||||
this._value = '';
|
||||
this._eventListeners = new EventListenerCollection();
|
||||
this._inputEventListeners = new EventListenerCollection();
|
||||
}
|
||||
|
||||
get settingsController() {
|
||||
return this._parent.parent.settingsController;
|
||||
}
|
||||
|
||||
get parent() {
|
||||
return this._parent;
|
||||
}
|
||||
|
||||
get index() {
|
||||
return this._index;
|
||||
}
|
||||
|
||||
set index(value) {
|
||||
this._index = value;
|
||||
}
|
||||
|
||||
get node() {
|
||||
return this._node;
|
||||
}
|
||||
|
||||
prepare(condition) {
|
||||
const {type, operator, value} = condition;
|
||||
|
||||
this._node = this._parent.parent.instantiateTemplate('#condition-template');
|
||||
this._typeInput = this._node.querySelector('.condition-type');
|
||||
this._typeOptionContainer = this._typeInput.querySelector('optgroup');
|
||||
this._operatorInput = this._node.querySelector('.condition-operator');
|
||||
this._operatorOptionContainer = this._operatorInput.querySelector('optgroup');
|
||||
this._valueInput = this._node.querySelector('.condition-input-inner');
|
||||
this._removeButton = this._node.querySelector('.condition-remove');
|
||||
|
||||
const operatorDetails = this._getOperatorDetails(type, operator);
|
||||
this._updateTypes(type);
|
||||
this._updateOperators(type, operator);
|
||||
this._updateValueInput(value, operatorDetails);
|
||||
|
||||
this._eventListeners.addEventListener(this._typeInput, 'change', this._onTypeChange.bind(this), false);
|
||||
this._eventListeners.addEventListener(this._operatorInput, 'change', this._onOperatorChange.bind(this), false);
|
||||
this._eventListeners.addEventListener(this._removeButton, 'click', this._onRemoveButtonClick.bind(this), false);
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this._eventListeners.removeAllEventListeners();
|
||||
this._value = '';
|
||||
|
||||
if (this._node === null) { return; }
|
||||
|
||||
const node = this._node;
|
||||
this._node = null;
|
||||
this._typeInput = null;
|
||||
this._operatorInput = null;
|
||||
this._valueInputContainer = null;
|
||||
this._removeButton = null;
|
||||
|
||||
if (node.parentNode !== null) {
|
||||
node.parentNode.removeChild(node);
|
||||
}
|
||||
}
|
||||
|
||||
getPath(property) {
|
||||
property = (typeof property === 'string' ? `.${property}` : '');
|
||||
return this._parent.getPath(`conditions[${this._index}]${property}`);
|
||||
}
|
||||
|
||||
// Private
|
||||
|
||||
_onTypeChange(e) {
|
||||
const type = e.currentTarget.value;
|
||||
const operators = this._getDescriptorOperators(type);
|
||||
const operator = operators.length > 0 ? operators[0].name : '';
|
||||
const operatorDetails = this._getOperatorDetails(type, operator);
|
||||
this._updateSelect(this._operatorInput, this._operatorOptionContainer, operators, operator);
|
||||
this._updateValueInput(operatorDetails.defaultValue, operatorDetails);
|
||||
this.settingsController.setGlobalSetting(this.getPath('type'), type);
|
||||
}
|
||||
|
||||
_onOperatorChange(e) {
|
||||
const type = this._typeInput.value;
|
||||
const operator = e.currentTarget.value;
|
||||
const operatorDetails = this._getOperatorDetails(type, operator);
|
||||
if (operatorDetails.resetDefaultOnChange) {
|
||||
const okay = this._updateValueInput(operatorDetails.defaultValue, operatorDetails);
|
||||
if (okay) {
|
||||
this.settingsController.setGlobalSetting(this.getPath('operator'), operator);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onValueInputChange({validate, normalize}, e) {
|
||||
const node = e.currentTarget;
|
||||
const value = node.value;
|
||||
const okay = this._validateValue(value, validate);
|
||||
this._value = value;
|
||||
if (okay) {
|
||||
const normalizedValue = this._normalizeValue(value, normalize);
|
||||
node.value = normalizedValue;
|
||||
this.settingsController.setGlobalSetting(this.getPath('value'), normalizedValue);
|
||||
}
|
||||
}
|
||||
|
||||
_onModifierKeyDown({validate, normalize}, e) {
|
||||
e.preventDefault();
|
||||
const node = e.currentTarget;
|
||||
|
||||
let modifiers;
|
||||
const key = DocumentUtil.getKeyFromEvent(e);
|
||||
switch (key) {
|
||||
case 'Escape':
|
||||
case 'Backspace':
|
||||
modifiers = [];
|
||||
break;
|
||||
default:
|
||||
{
|
||||
modifiers = this._getModifiers(e);
|
||||
const currentModifier = this._splitValue(this._value);
|
||||
for (const modifier of currentModifier) {
|
||||
modifiers.add(modifier);
|
||||
}
|
||||
modifiers = [...modifiers];
|
||||
modifiers = this._sortModifiers(modifiers);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
const {value, displayValue} = this._getModifierKeyStrings(modifiers);
|
||||
node.value = displayValue;
|
||||
const okay = this._validateValue(value, validate);
|
||||
this._value = value;
|
||||
if (okay) {
|
||||
const normalizedValue = this._normalizeValue(value, normalize);
|
||||
node.value = normalizedValue;
|
||||
this.settingsController.setGlobalSetting(this.getPath('value'), normalizedValue);
|
||||
}
|
||||
}
|
||||
|
||||
_onRemoveButtonClick() {
|
||||
this._parent.removeCondition(this);
|
||||
}
|
||||
|
||||
_getDescriptorTypes() {
|
||||
return this._parent.parent.getDescriptorTypes();
|
||||
}
|
||||
|
||||
_getDescriptorOperators(type) {
|
||||
return this._parent.parent.getDescriptorOperators(type);
|
||||
}
|
||||
|
||||
_getOperatorDetails(type, operator) {
|
||||
return this._parent.parent.getOperatorDetails(type, operator);
|
||||
}
|
||||
|
||||
_getModifierKeyStrings(modifiers) {
|
||||
return this._parent.parent.getModifierKeyStrings(modifiers);
|
||||
}
|
||||
|
||||
_sortModifiers(modifiers) {
|
||||
return this._parent.parent.sortModifiers(modifiers);
|
||||
}
|
||||
|
||||
_splitValue(value) {
|
||||
return this._parent.parent.splitValue(value);
|
||||
}
|
||||
|
||||
_updateTypes(type) {
|
||||
const types = this._getDescriptorTypes();
|
||||
this._updateSelect(this._typeInput, this._typeOptionContainer, types, type);
|
||||
}
|
||||
|
||||
_updateOperators(type, operator) {
|
||||
const operators = this._getDescriptorOperators(type);
|
||||
this._updateSelect(this._operatorInput, this._operatorOptionContainer, operators, operator);
|
||||
}
|
||||
|
||||
_updateSelect(select, optionContainer, values, value) {
|
||||
optionContainer.textContent = '';
|
||||
for (const {name, displayName} of values) {
|
||||
const option = document.createElement('option');
|
||||
option.value = name;
|
||||
option.textContent = displayName;
|
||||
optionContainer.appendChild(option);
|
||||
}
|
||||
select.value = value;
|
||||
}
|
||||
|
||||
_updateValueInput(value, {type, validate, normalize}) {
|
||||
this._inputEventListeners.removeAllEventListeners();
|
||||
|
||||
const inputData = {validate, normalize};
|
||||
const node = this._valueInput;
|
||||
node.classList.remove('is-invalid');
|
||||
this._value = value;
|
||||
|
||||
switch (type) {
|
||||
case 'integer':
|
||||
{
|
||||
node.type = 'number';
|
||||
node.step = '1';
|
||||
node.value = value;
|
||||
this._inputEventListeners.addEventListener(node, 'change', this._onValueInputChange.bind(this, inputData), false);
|
||||
}
|
||||
break;
|
||||
case 'modifierKeys':
|
||||
{
|
||||
const modifiers = this._splitValue(value);
|
||||
const {displayValue} = this._getModifierKeyStrings(modifiers);
|
||||
node.type = 'text';
|
||||
node.removeAttribute('step');
|
||||
node.value = displayValue;
|
||||
this._inputEventListeners.addEventListener(node, 'keydown', this._onModifierKeyDown.bind(this, inputData), false);
|
||||
}
|
||||
break;
|
||||
default: // 'string'
|
||||
{
|
||||
node.type = 'text';
|
||||
node.removeAttribute('step');
|
||||
node.value = value;
|
||||
this._inputEventListeners.addEventListener(node, 'change', this._onValueInputChange.bind(this, inputData), false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
this._validateValue(value, validate);
|
||||
}
|
||||
|
||||
_validateValue(value, validate) {
|
||||
const okay = (validate === null || validate(value));
|
||||
this._valueInput.classList.toggle('is-invalid', !okay);
|
||||
return okay;
|
||||
}
|
||||
|
||||
_normalizeValue(value, normalize) {
|
||||
return (normalize !== null ? normalize(value) : value);
|
||||
}
|
||||
|
||||
_getModifiers(e) {
|
||||
const modifiers = DocumentUtil.getActiveModifiers(e);
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/metaKey
|
||||
// https://askubuntu.com/questions/567731/why-is-shift-alt-being-mapped-to-meta
|
||||
// It works with mouse events on some platforms, so try to determine if metaKey is pressed.
|
||||
// This is a hack and only works when both Shift and Alt are not pressed.
|
||||
if (
|
||||
!modifiers.has('meta') &&
|
||||
DocumentUtil.getKeyFromEvent(e) === 'Meta' &&
|
||||
!(
|
||||
modifiers.size === 2 &&
|
||||
modifiers.has('shift') &&
|
||||
modifiers.has('alt')
|
||||
)
|
||||
) {
|
||||
modifiers.add('meta');
|
||||
}
|
||||
return modifiers;
|
||||
}
|
||||
}
|
@ -16,17 +16,15 @@
|
||||
*/
|
||||
|
||||
/* global
|
||||
* ConditionsUI
|
||||
* conditionsClearCaches
|
||||
* profileConditionsDescriptor
|
||||
* profileConditionsDescriptorPromise
|
||||
* ProfileConditionsUI
|
||||
* api
|
||||
* utilBackgroundIsolate
|
||||
*/
|
||||
|
||||
class ProfileController {
|
||||
constructor(settingsController) {
|
||||
this._settingsController = settingsController;
|
||||
this._conditionsContainer = null;
|
||||
this._profileConditionsUI = new ProfileConditionsUI(settingsController);
|
||||
}
|
||||
|
||||
async prepare() {
|
||||
@ -49,8 +47,11 @@ class ProfileController {
|
||||
// Private
|
||||
|
||||
async _onOptionsChanged() {
|
||||
const {modifiers} = await api.getEnvironmentInfo();
|
||||
this._profileConditionsUI.setKeyInfo(modifiers.separator, modifiers.keys);
|
||||
|
||||
const optionsFull = await this._settingsController.getOptionsFullMutable();
|
||||
await this._formWrite(optionsFull);
|
||||
this._formWrite(optionsFull);
|
||||
}
|
||||
|
||||
_tryGetIntegerValue(selector, min, max) {
|
||||
@ -78,7 +79,7 @@ class ProfileController {
|
||||
profile.name = $('#profile-name').val();
|
||||
}
|
||||
|
||||
async _formWrite(optionsFull) {
|
||||
_formWrite(optionsFull) {
|
||||
const currentProfileIndex = this._settingsController.profileIndex;
|
||||
const profile = optionsFull.profiles[currentProfileIndex];
|
||||
|
||||
@ -91,23 +92,17 @@ class ProfileController {
|
||||
|
||||
$('#profile-name').val(profile.name);
|
||||
|
||||
if (this._conditionsContainer !== null) {
|
||||
this._conditionsContainer.cleanup();
|
||||
this._refreshProfileConditions(optionsFull);
|
||||
}
|
||||
|
||||
await profileConditionsDescriptorPromise;
|
||||
this._conditionsContainer = new ConditionsUI.Container(
|
||||
profileConditionsDescriptor,
|
||||
'popupLevel',
|
||||
profile.conditionGroups,
|
||||
$('#profile-condition-groups'),
|
||||
$('#profile-add-condition-group')
|
||||
);
|
||||
this._conditionsContainer.save = () => {
|
||||
this._settingsController.save();
|
||||
conditionsClearCaches(profileConditionsDescriptor);
|
||||
};
|
||||
this._conditionsContainer.isolate = utilBackgroundIsolate;
|
||||
_refreshProfileConditions(optionsFull) {
|
||||
this._profileConditionsUI.cleanup();
|
||||
|
||||
const profileIndex = this._settingsController.profileIndex;
|
||||
if (profileIndex < 0 || profileIndex >= optionsFull.profiles.length) { return; }
|
||||
|
||||
const {conditionGroups} = optionsFull.profiles[profileIndex];
|
||||
this._profileConditionsUI.prepare(conditionGroups);
|
||||
}
|
||||
|
||||
_populateSelect(select, profiles, currentValue, ignoreIndices) {
|
||||
|
@ -112,23 +112,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template id="condition-group-template"><div class="condition-group">
|
||||
<div class="condition-list"></div>
|
||||
<div class="condition-group-options">
|
||||
<button class="btn btn-default condition-add"><span class="glyphicon glyphicon-plus"></span></button>
|
||||
</div>
|
||||
<div class="condition-group-separator-label">OR</div>
|
||||
</div></template>
|
||||
<template id="condition-template"><div class="input-group condition">
|
||||
<div class="input-group-addon condition-prefix"></div>
|
||||
<div class="input-group-btn"><select class="form-control btn btn-default condition-type"><optgroup label="Type"></optgroup></select></div>
|
||||
<div class="input-group-btn"><select class="form-control btn btn-default condition-operator"><optgroup label="Operator"></optgroup></select></div>
|
||||
<div class="condition-line-break"></div>
|
||||
<div class="condition-input"></div>
|
||||
<div class="condition-input"><input type="text" class="form-control condition-input-inner"></div>
|
||||
<div class="input-group-btn"><button class="btn btn-danger condition-remove" title="Remove"><span class="glyphicon glyphicon-remove"></span></button></div>
|
||||
</div></template>
|
||||
<template id="condition-group-separator-template"><div class="input-group">
|
||||
<div class="condition-group-separator-label">OR</div>
|
||||
</div></template>
|
||||
<template id="condition-group-options-template"><div class="condition-group-options">
|
||||
<button class="btn btn-default condition-add"><span class="glyphicon glyphicon-plus"></span></button>
|
||||
</div></template>
|
||||
<template id="condition-input-text-template"><input type="text" class="form-control condition-input-inner" /></template>
|
||||
<template id="condition-input-select-template"><select class="form-control condition-input-inner"></select></template>
|
||||
<template id="condition-input-option-template"><option></option></template>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@ -1143,7 +1141,6 @@
|
||||
<script src="/bg/js/anki-note-builder.js"></script>
|
||||
<script src="/bg/js/conditions.js"></script>
|
||||
<script src="/bg/js/options.js"></script>
|
||||
<script src="/bg/js/profile-conditions.js"></script>
|
||||
<script src="/bg/js/util.js"></script>
|
||||
<script src="/mixed/js/audio-system.js"></script>
|
||||
<script src="/mixed/js/document-util.js"></script>
|
||||
@ -1153,11 +1150,11 @@
|
||||
<script src="/bg/js/settings/audio.js"></script>
|
||||
<script src="/bg/js/settings/backup.js"></script>
|
||||
<script src="/bg/js/settings/clipboard-popups-controller.js"></script>
|
||||
<script src="/bg/js/settings/conditions-ui.js"></script>
|
||||
<script src="/bg/js/settings/dictionaries.js"></script>
|
||||
<script src="/bg/js/settings/generic-setting-controller.js"></script>
|
||||
<script src="/bg/js/settings/popup-preview.js"></script>
|
||||
<script src="/bg/js/settings/profiles.js"></script>
|
||||
<script src="/bg/js/settings/profile-conditions-ui.js"></script>
|
||||
<script src="/bg/js/settings/settings-controller.js"></script>
|
||||
<script src="/bg/js/settings/storage.js"></script>
|
||||
<script src="/mixed/js/dictionary-data-util.js"></script>
|
||||
|
Loading…
Reference in New Issue
Block a user