diff options
| -rw-r--r-- | ext/bg/background.html | 2 | ||||
| -rw-r--r-- | ext/bg/css/settings.css | 22 | ||||
| -rw-r--r-- | ext/bg/js/backend.js | 46 | ||||
| -rw-r--r-- | ext/bg/js/json-schema.js | 4 | ||||
| -rw-r--r-- | ext/bg/js/options.js | 22 | ||||
| -rw-r--r-- | ext/bg/js/profile-conditions.js | 178 | ||||
| -rw-r--r-- | ext/bg/js/settings/conditions-ui.js | 449 | ||||
| -rw-r--r-- | ext/bg/js/settings/profile-conditions-ui.js | 686 | ||||
| -rw-r--r-- | ext/bg/js/settings/profiles.js | 41 | ||||
| -rw-r--r-- | ext/bg/settings.html | 21 | 
10 files changed, 772 insertions, 699 deletions
| diff --git a/ext/bg/background.html b/ext/bg/background.html index 218e9925..b3067b1e 100644 --- a/ext/bg/background.html +++ b/ext/bg/background.html @@ -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> diff --git a/ext/bg/css/settings.css b/ext/bg/css/settings.css index 1b945310..2abcc1b1 100644 --- a/ext/bg/css/settings.css +++ b/ext/bg/css/settings.css @@ -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;  } diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 810370c4..7f85d9a5 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -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)) { -                return profile; -            } -        } -        return null; -    } -    _testConditionGroups(conditionGroups, data) { -        if (conditionGroups.length === 0) { return false; } +            let schema; +            if (index < this._profileConditionsSchemaCache.length) { +                schema = this._profileConditionsSchemaCache[index]; +            } else { +                schema = this._profileConditionsUtil.createSchema(conditionGroups); +                this._profileConditionsSchemaCache.push(schema); +            } -        for (const conditionGroup of conditionGroups) { -            const conditions = conditionGroup.conditions; -            if (conditions.length > 0 && this._testConditions(conditions, data)) { -                return true; +            if (conditionGroups.length > 0 && this._optionsSchemaValidator.isValid(optionsContext, schema)) { +                return profile;              } +            ++index;          } -        return false; +        return null;      } -    _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() { diff --git a/ext/bg/js/json-schema.js b/ext/bg/js/json-schema.js index 30446559..1be78fd2 100644 --- a/ext/bg/js/json-schema.js +++ b/ext/bg/js/json-schema.js @@ -181,6 +181,10 @@ class JsonSchemaValidator {          return this._getPropertySchema(schema, property, value, null);      } +    clearCache() { +        this._regexCache.clear(); +    } +      // Private      _getPropertySchema(schema, property, value, path) { diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js index 0d83f428..c513f572 100644 --- a/ext/bg/js/options.js +++ b/ext/bg/js/options.js @@ -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; +    }  } diff --git a/ext/bg/js/profile-conditions.js b/ext/bg/js/profile-conditions.js deleted file mode 100644 index f3a85cb1..00000000 --- a/ext/bg/js/profile-conditions.js +++ /dev/null @@ -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) -                } -            } -        } -    }; -})(); diff --git a/ext/bg/js/settings/conditions-ui.js b/ext/bg/js/settings/conditions-ui.js deleted file mode 100644 index 98b3d432..00000000 --- a/ext/bg/js/settings/conditions-ui.js +++ /dev/null @@ -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(); -    } -}; diff --git a/ext/bg/js/settings/profile-conditions-ui.js b/ext/bg/js/settings/profile-conditions-ui.js new file mode 100644 index 00000000..4f206cc1 --- /dev/null +++ b/ext/bg/js/settings/profile-conditions-ui.js @@ -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; +    } +} diff --git a/ext/bg/js/settings/profiles.js b/ext/bg/js/settings/profiles.js index 2449ab44..c1961e20 100644 --- a/ext/bg/js/settings/profiles.js +++ b/ext/bg/js/settings/profiles.js @@ -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); +    } + +    _refreshProfileConditions(optionsFull) { +        this._profileConditionsUI.cleanup(); + +        const profileIndex = this._settingsController.profileIndex; +        if (profileIndex < 0 || profileIndex >= optionsFull.profiles.length) { return; } -        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; +        const {conditionGroups} = optionsFull.profiles[profileIndex]; +        this._profileConditionsUI.prepare(conditionGroups);      }      _populateSelect(select, profiles, currentValue, ignoreIndices) { diff --git a/ext/bg/settings.html b/ext/bg/settings.html index abe3b6aa..f7877dd2 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -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> |