diff --git a/ext/bg/background.html b/ext/bg/background.html index 90a56024..3b37db87 100644 --- a/ext/bg/background.html +++ b/ext/bg/background.html @@ -16,11 +16,13 @@ + + diff --git a/ext/bg/js/api.js b/ext/bg/js/api.js index f32b984f..474fe604 100644 --- a/ext/bg/js/api.js +++ b/ext/bg/js/api.js @@ -140,7 +140,10 @@ async function apiCommandExec(command) { }, toggle: async () => { - const optionsContext = {depth: 0}; + const optionsContext = { + depth: 0, + url: window.location.href + }; const options = await apiOptionsGet(optionsContext); options.general.enable = !options.general.enable; await apiOptionsSave('popup'); diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 3839da39..4068b760 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -23,7 +23,8 @@ class Backend { this.anki = new AnkiNull(); this.options = null; this.optionsContext = { - depth: 0 + depth: 0, + url: window.location.href }; this.isPreparedResolve = null; @@ -173,7 +174,40 @@ class Backend { if (typeof optionsContext.index === 'number') { return profiles[optionsContext.index]; } - return this.options.profiles[this.options.profileCurrent]; + const profile = this.getProfileFromContext(optionsContext); + return profile !== null ? profile : this.options.profiles[this.options.profileCurrent]; + } + + getProfileFromContext(optionsContext) { + for (const profile of this.options.profiles) { + const conditionGroups = profile.conditionGroups; + if (conditionGroups.length > 0 && Backend.testConditionGroups(conditionGroups, optionsContext)) { + return profile; + } + } + return null; + } + + static testConditionGroups(conditionGroups, data) { + if (conditionGroups.length === 0) { return false; } + + for (const conditionGroup of conditionGroups) { + const conditions = conditionGroup.conditions; + if (conditions.length > 0 && Backend.testConditions(conditions, data)) { + return true; + } + } + + return false; + } + + static testConditions(conditions, data) { + for (const condition of conditions) { + if (!conditionsTestValue(profileConditionsDescriptor, condition.type, condition.operator, condition.value, data)) { + return false; + } + } + return true; } setExtensionBadgeBackgroundColor(color) { diff --git a/ext/bg/js/conditions-ui.js b/ext/bg/js/conditions-ui.js new file mode 100644 index 00000000..a6f54a1c --- /dev/null +++ b/ext/bg/js/conditions-ui.js @@ -0,0 +1,326 @@ +/* + * Copyright (C) 2019 Alex Yatskov + * Author: Alex Yatskov + * + * 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 . + */ + + +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 conditionGroups) { + this.children.push(new ConditionsUI.ConditionGroup(this, conditionGroup)); + } + + this.addButton.on('click', () => this.onAddConditionGroup()); + } + + 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 (this.conditionDescriptors.hasOwnProperty(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 (this.conditionDescriptors.hasOwnProperty(type)) { + const conditionDescriptor = this.conditionDescriptors[type]; + if (conditionDescriptor.operators.hasOwnProperty(operator)) { + const operatorDescriptor = conditionDescriptor.operators[operator]; + if (operatorDescriptor.hasOwnProperty('defaultValue')) { + return {value: operatorDescriptor.defaultValue, fromOperator: true}; + } + } + if (conditionDescriptor.hasOwnProperty('defaultValue')) { + return {value: conditionDescriptor.defaultValue, fromOperator: false}; + } + } + return {fromOperator: false}; + } +}; + +ConditionsUI.ConditionGroup = class ConditionGroup { + constructor(parent, conditionGroup) { + this.parent = parent; + this.children = []; + this.conditionGroup = conditionGroup; + this.container = $('
').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 conditionGroup.conditions) { + this.children.push(new ConditionsUI.Condition(this, condition)); + } + + this.addButton.on('click', () => this.onAddCondition()); + } + + 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('input'); + 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.input.on('change', () => this.onInputChanged()); + this.typeSelect.on('change', () => this.onConditionTypeChanged()); + this.operatorSelect.on('change', () => this.onConditionOperatorChanged()); + this.removeButton.on('click', () => this.onRemoveClicked()); + } + + cleanup() { + this.input.off('change'); + this.typeSelect.off('change'); + this.operatorSelect.off('change'); + this.removeButton.off('click'); + this.container.remove(); + } + + save() { + this.parent.save(); + } + + 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]; + $('
@@ -563,9 +647,12 @@ + + + diff --git a/ext/fg/js/float.js b/ext/fg/js/float.js index 348c114e..fd7986b8 100644 --- a/ext/fg/js/float.js +++ b/ext/fg/js/float.js @@ -24,7 +24,8 @@ class DisplayFloat extends Display { this.styleNode = null; this.optionsContext = { - depth: 0 + depth: 0, + url: window.location.href }; this.dependencies = Object.assign({}, this.dependencies, {docRangeFromPoint, docSentenceExtract}); @@ -78,9 +79,10 @@ class DisplayFloat extends Display { } }, - popupNestedInitialize: ({id, depth, parentFrameId}) => { + popupNestedInitialize: ({id, depth, parentFrameId, url}) => { this.optionsContext.depth = depth; - popupNestedInitialize(id, depth, parentFrameId); + this.optionsContext.url = url; + popupNestedInitialize(id, depth, parentFrameId, url); } }; diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index 564df343..167e82c0 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -27,7 +27,8 @@ class Frontend { this.ignoreNodes = (Array.isArray(ignoreNodes) && ignoreNodes.length > 0 ? ignoreNodes.join(',') : null); this.optionsContext = { - depth: popup.depth + depth: popup.depth, + url: popup.url }; this.primaryTouchIdentifier = null; @@ -42,9 +43,9 @@ class Frontend { static create() { const initializationData = window.frontendInitializationData; const isNested = (initializationData !== null && typeof initializationData === 'object'); - const {id, depth, parentFrameId, ignoreNodes} = isNested ? initializationData : {}; + const {id, depth, parentFrameId, ignoreNodes, url} = isNested ? initializationData : {}; - const popup = isNested ? new PopupProxy(depth + 1, id, parentFrameId) : PopupProxyHost.instance.createPopup(null); + const popup = isNested ? new PopupProxy(depth + 1, id, parentFrameId, url) : PopupProxyHost.instance.createPopup(null); const frontend = new Frontend(popup, ignoreNodes); frontend.prepare(); return frontend; @@ -52,7 +53,7 @@ class Frontend { async prepare() { try { - this.options = await apiOptionsGet(this.optionsContext); + this.options = await apiOptionsGet(this.getOptionsContext()); window.addEventListener('message', this.onFrameMessage.bind(this)); window.addEventListener('mousedown', this.onMouseDown.bind(this)); @@ -262,7 +263,7 @@ class Frontend { } async updateOptions() { - this.options = await apiOptionsGet(this.optionsContext); + this.options = await apiOptionsGet(this.getOptionsContext()); if (!this.options.enable) { this.searchClear(); } @@ -335,7 +336,7 @@ class Frontend { return; } - const {definitions, length} = await apiTermsFind(searchText, this.optionsContext); + const {definitions, length} = await apiTermsFind(searchText, this.getOptionsContext()); if (definitions.length === 0) { return false; } @@ -368,7 +369,7 @@ class Frontend { return; } - const definitions = await apiKanjiFind(searchText, this.optionsContext); + const definitions = await apiKanjiFind(searchText, this.getOptionsContext()); if (definitions.length === 0) { return false; } @@ -512,6 +513,11 @@ class Frontend { } } + getOptionsContext() { + this.optionsContext.url = this.popup.url; + return this.optionsContext; + } + static isScanningModifierPressed(scanningModifier, mouseEvent) { switch (scanningModifier) { case 'alt': return mouseEvent.altKey; diff --git a/ext/fg/js/popup-nested.js b/ext/fg/js/popup-nested.js index de2acccc..b36de2ec 100644 --- a/ext/fg/js/popup-nested.js +++ b/ext/fg/js/popup-nested.js @@ -19,13 +19,13 @@ let popupNestedInitialized = false; -async function popupNestedInitialize(id, depth, parentFrameId) { +async function popupNestedInitialize(id, depth, parentFrameId, url) { if (popupNestedInitialized) { return; } popupNestedInitialized = true; - const optionsContext = {depth}; + const optionsContext = {depth, url}; const options = await apiOptionsGet(optionsContext); const popupNestingMaxDepth = options.scanning.popupNestingMaxDepth; @@ -35,7 +35,7 @@ async function popupNestedInitialize(id, depth, parentFrameId) { const ignoreNodes = options.scanning.enableOnPopupExpressions ? [] : [ '.expression', '.expression *' ]; - window.frontendInitializationData = {id, depth, parentFrameId, ignoreNodes}; + window.frontendInitializationData = {id, depth, parentFrameId, ignoreNodes, url}; const scriptSrcs = [ '/fg/js/frontend-api-sender.js', diff --git a/ext/fg/js/popup-proxy.js b/ext/fg/js/popup-proxy.js index f04e24e0..235e1730 100644 --- a/ext/fg/js/popup-proxy.js +++ b/ext/fg/js/popup-proxy.js @@ -18,7 +18,7 @@ class PopupProxy { - constructor(depth, parentId, parentFrameId) { + constructor(depth, parentId, parentFrameId, url) { this.parentId = parentId; this.parentFrameId = parentFrameId; this.id = null; @@ -26,6 +26,7 @@ class PopupProxy { this.parent = null; this.child = null; this.depth = depth; + this.url = url; this.container = null; diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index 8953cf30..08965084 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -59,7 +59,8 @@ class Popup { this.invokeApi('popupNestedInitialize', { id: this.id, depth: this.depth, - parentFrameId + parentFrameId, + url: this.url }); this.invokeApi('setOptions', { general: { @@ -311,4 +312,8 @@ class Popup { parent.appendChild(this.container); } } + + get url() { + return window.location.href; + } }