Merge pull request #218 from toasted-nutbread/settings-profile-conditions

Settings profile conditions
This commit is contained in:
Alex Yatskov 2019-09-23 17:03:00 -07:00 committed by GitHub
commit ba2858309e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 736 additions and 23 deletions

View File

@ -16,11 +16,13 @@
<script src="/bg/js/api.js"></script> <script src="/bg/js/api.js"></script>
<script src="/bg/js/audio.js"></script> <script src="/bg/js/audio.js"></script>
<script src="/bg/js/backend-api-forwarder.js"></script> <script src="/bg/js/backend-api-forwarder.js"></script>
<script src="/bg/js/conditions.js"></script>
<script src="/bg/js/database.js"></script> <script src="/bg/js/database.js"></script>
<script src="/bg/js/deinflector.js"></script> <script src="/bg/js/deinflector.js"></script>
<script src="/bg/js/dictionary.js"></script> <script src="/bg/js/dictionary.js"></script>
<script src="/bg/js/handlebars.js"></script> <script src="/bg/js/handlebars.js"></script>
<script src="/bg/js/options.js"></script> <script src="/bg/js/options.js"></script>
<script src="/bg/js/profile-conditions.js"></script>
<script src="/bg/js/request.js"></script> <script src="/bg/js/request.js"></script>
<script src="/bg/js/templates.js"></script> <script src="/bg/js/templates.js"></script>
<script src="/bg/js/translator.js"></script> <script src="/bg/js/translator.js"></script>

View File

@ -140,7 +140,10 @@ async function apiCommandExec(command) {
}, },
toggle: async () => { toggle: async () => {
const optionsContext = {depth: 0}; const optionsContext = {
depth: 0,
url: window.location.href
};
const options = await apiOptionsGet(optionsContext); const options = await apiOptionsGet(optionsContext);
options.general.enable = !options.general.enable; options.general.enable = !options.general.enable;
await apiOptionsSave('popup'); await apiOptionsSave('popup');

View File

@ -23,7 +23,8 @@ class Backend {
this.anki = new AnkiNull(); this.anki = new AnkiNull();
this.options = null; this.options = null;
this.optionsContext = { this.optionsContext = {
depth: 0 depth: 0,
url: window.location.href
}; };
this.isPreparedResolve = null; this.isPreparedResolve = null;
@ -173,7 +174,40 @@ class Backend {
if (typeof optionsContext.index === 'number') { if (typeof optionsContext.index === 'number') {
return profiles[optionsContext.index]; 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) { setExtensionBadgeBackgroundColor(color) {

326
ext/bg/js/conditions-ui.js Normal file
View File

@ -0,0 +1,326 @@
/*
* Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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 = $('<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 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];
$('<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 (conditionDescriptors.hasOwnProperty(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 props = {
placeholder: '',
type: 'text'
};
const objects = [];
if (conditionDescriptors.hasOwnProperty(type)) {
const conditionDescriptor = conditionDescriptors[type];
objects.push(conditionDescriptor);
if (conditionDescriptor.operators.hasOwnProperty(operator)) {
const operatorDescriptor = conditionDescriptor.operators[operator];
objects.push(operatorDescriptor);
}
}
for (const object of objects) {
if (object.hasOwnProperty('placeholder')) {
props.placeholder = object.placeholder;
}
if (object.type === 'number') {
props.type = 'number';
for (const prop of ['step', 'min', 'max']) {
if (object.hasOwnProperty(prop)) {
props[prop] = object[prop];
}
}
}
}
for (const prop in props) {
this.input.prop(prop, props[prop]);
}
const {valid} = this.validateValue(this.condition.value);
this.input.toggleClass('is-invalid', !valid);
this.input.val(this.condition.value);
}
validateValue(value) {
const conditionDescriptors = this.parent.parent.conditionDescriptors;
let valid = true;
try {
value = conditionsNormalizeOptionValue(
conditionDescriptors,
this.condition.type,
this.condition.operator,
value
);
} catch (e) {
valid = false;
}
return {valid, value};
}
onInputChanged() {
const {valid, value} = this.validateValue(this.input.val());
this.input.toggleClass('is-invalid', !valid);
this.input.val(value);
this.condition.value = 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();
}
};

117
ext/bg/js/conditions.js Normal file
View File

@ -0,0 +1,117 @@
/*
* Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* 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 <http://www.gnu.org/licenses/>.
*/
function conditionsValidateOptionValue(object, value) {
if (object.hasOwnProperty('validate') && !object.validate(value)) {
throw new Error('Invalid value for condition');
}
if (object.hasOwnProperty('transform')) {
value = object.transform(value);
if (object.hasOwnProperty('validateTransformed') && !object.validateTransformed(value)) {
throw new Error('Invalid value for condition');
}
}
return value;
}
function conditionsNormalizeOptionValue(descriptors, type, operator, optionValue) {
if (!descriptors.hasOwnProperty(type)) {
throw new Error('Invalid type');
}
const conditionDescriptor = descriptors[type];
if (!conditionDescriptor.operators.hasOwnProperty(operator)) {
throw new Error('Invalid operator');
}
const operatorDescriptor = conditionDescriptor.operators[operator];
let transformedValue = conditionsValidateOptionValue(conditionDescriptor, optionValue);
transformedValue = conditionsValidateOptionValue(operatorDescriptor, transformedValue);
if (operatorDescriptor.hasOwnProperty('transformReverse')) {
transformedValue = operatorDescriptor.transformReverse(transformedValue);
}
return transformedValue;
}
function conditionsTestValueThrowing(descriptors, type, operator, optionValue, value) {
if (!descriptors.hasOwnProperty(type)) {
throw new Error('Invalid type');
}
const conditionDescriptor = descriptors[type];
if (!conditionDescriptor.operators.hasOwnProperty(operator)) {
throw new Error('Invalid operator');
}
const operatorDescriptor = conditionDescriptor.operators[operator];
if (operatorDescriptor.hasOwnProperty('transform')) {
if (operatorDescriptor.hasOwnProperty('transformCache')) {
const key = `${optionValue}`;
const transformCache = operatorDescriptor.transformCache;
if (transformCache.hasOwnProperty(key)) {
optionValue = transformCache[key];
} else {
optionValue = operatorDescriptor.transform(optionValue);
transformCache[key] = optionValue;
}
} else {
optionValue = operatorDescriptor.transform(optionValue);
}
}
return operatorDescriptor.test(value, optionValue);
}
function conditionsTestValue(descriptors, type, operator, optionValue, value) {
try {
return conditionsTestValueThrowing(descriptors, type, operator, optionValue, value);
} catch (e) {
return false;
}
}
function conditionsClearCaches(descriptors) {
for (const type in descriptors) {
if (!descriptors.hasOwnProperty(type)) {
continue;
}
const conditionDescriptor = descriptors[type];
if (conditionDescriptor.hasOwnProperty('transformCache')) {
conditionDescriptor.transformCache = {};
}
const operatorDescriptors = conditionDescriptor.operators;
for (const operator in operatorDescriptors) {
if (!operatorDescriptors.hasOwnProperty(operator)) {
continue;
}
const operatorDescriptor = operatorDescriptors[operator];
if (operatorDescriptor.hasOwnProperty('transformCache')) {
operatorDescriptor.transformCache = {};
}
}
}
}

View File

@ -22,7 +22,10 @@ $(document).ready(utilAsync(() => {
$('#open-options').click(() => apiCommandExec('options')); $('#open-options').click(() => apiCommandExec('options'));
$('#open-help').click(() => apiCommandExec('help')); $('#open-help').click(() => apiCommandExec('help'));
const optionsContext = {depth: 0}; const optionsContext = {
depth: 0,
url: window.location.href
};
apiOptionsGet(optionsContext).then(options => { apiOptionsGet(optionsContext).then(options => {
const toggle = $('#enable-search'); const toggle = $('#enable-search');
toggle.prop('checked', options.general.enable).change(); toggle.prop('checked', options.general.enable).change();

View File

@ -329,6 +329,22 @@ function profileOptionsUpdateVersion(options) {
/* /*
* Global options * Global options
*
* Each profile has an array named "conditionGroups", which is an array of condition groups
* which enable the contextual selection of profiles. The structure of the array is as follows:
* [
* {
* conditions: [
* {
* type: "string",
* operator: "string",
* value: "string"
* },
* // ...
* ]
* },
* // ...
* ]
*/ */
const optionsVersionUpdates = []; const optionsVersionUpdates = [];
@ -351,7 +367,8 @@ function optionsUpdateVersion(options, defaultProfileOptions) {
if (profiles.length === 0) { if (profiles.length === 0) {
profiles.push({ profiles.push({
name: 'Default', name: 'Default',
options: defaultProfileOptions options: defaultProfileOptions,
conditionGroups: []
}); });
} }
@ -369,6 +386,9 @@ function optionsUpdateVersion(options, defaultProfileOptions) {
// Update profile options // Update profile options
for (const profile of profiles) { for (const profile of profiles) {
if (!Array.isArray(profile.conditionGroups)) {
profile.conditionGroups = [];
}
profile.options = profileOptionsUpdateVersion(profile.options); profile.options = profileOptionsUpdateVersion(profile.options);
} }

View File

@ -0,0 +1,85 @@
/*
* Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* 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 <http://www.gnu.org/licenses/>.
*/
const 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) => (transformedOptionValue.indexOf(new URL(url).hostname.toLowerCase()) >= 0)
},
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))
}
}
}
};

View File

@ -18,7 +18,10 @@
async function searchFrontendSetup() { async function searchFrontendSetup() {
const optionsContext = {depth: 0}; const optionsContext = {
depth: 0,
url: window.location.href
};
const options = await apiOptionsGet(optionsContext); const options = await apiOptionsGet(optionsContext);
if (!options.scanning.enableOnSearchPage) { return; } if (!options.scanning.enableOnSearchPage) { return; }

View File

@ -22,7 +22,8 @@ class DisplaySearch extends Display {
super($('#spinner'), $('#content')); super($('#spinner'), $('#content'));
this.optionsContext = { this.optionsContext = {
depth: 0 depth: 0,
url: window.location.href
}; };
this.search = $('#search').click(this.onSearch.bind(this)); this.search = $('#search').click(this.onSearch.bind(this));

View File

@ -17,6 +17,7 @@
*/ */
let currentProfileIndex = 0; let currentProfileIndex = 0;
let profileConditionsContainer = null;
function getOptionsContext() { function getOptionsContext() {
return { return {
@ -81,6 +82,23 @@ async function profileFormWrite(optionsFull) {
$('#profile-move-down').prop('disabled', currentProfileIndex >= optionsFull.profiles.length - 1); $('#profile-move-down').prop('disabled', currentProfileIndex >= optionsFull.profiles.length - 1);
$('#profile-name').val(profile.name); $('#profile-name').val(profile.name);
if (profileConditionsContainer !== null) {
profileConditionsContainer.cleanup();
}
profileConditionsContainer = new ConditionsUI.Container(
profileConditionsDescriptor,
'popupLevel',
profile.conditionGroups,
$('#profile-condition-groups'),
$('#profile-add-condition-group')
);
profileConditionsContainer.save = () => {
apiOptionsSave();
conditionsClearCaches(profileConditionsDescriptor);
};
profileConditionsContainer.isolate = utilBackgroundIsolate;
} }
function profileOptionsPopulateSelect(select, profiles, currentValue, ignoreIndices) { function profileOptionsPopulateSelect(select, profiles, currentValue, ignoreIndices) {

View File

@ -34,6 +34,55 @@
font-weight: normal; font-weight: normal;
} }
.form-control.is-invalid {
border-color: #f00000;
}
.condition>.condition-prefix:after {
content: "IF";
}
.condition:nth-child(n+2)>.condition-prefix:after {
content: "AND";
}
.input-group .condition-prefix,
.input-group .condition-group-separator-label {
width: 60px;
text-align: center;
}
.input-group .condition-group-separator-label {
padding: 6px 12px;
font-weight: bold;
display: inline-block;
}
.input-group .condition-type,
.input-group .condition-operator {
width: auto;
text-align: center;
text-align-last: center;
}
.condition-group>.condition>div:first-child {
border-bottom-left-radius: 0;
}
.condition-group>.condition:nth-child(n+2)>div:first-child {
border-top-left-radius: 0;
}
.condition-group>.condition:nth-child(n+2)>div:last-child>button {
border-top-right-radius: 0;
}
.condition-group>.condition:nth-last-child(n+2)>div:last-child>button {
border-bottom-right-radius: 0;
}
.condition-group-options>.condition-add {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.condition-groups>*:last-of-type {
display: none;
}
#custom-popup-css { #custom-popup-css {
width: 100%; width: 100%;
min-height: 34px; min-height: 34px;
@ -71,7 +120,7 @@
<h3>Profiles</h3> <h3>Profiles</h3>
<p class="help-block"> <p class="help-block">
Profiles allow you to create multiple configurations and quickly switch between them. Profiles allow you to create multiple configurations and quickly switch between them or use them in different contexts.
</p> </p>
<div class="form-group"> <div class="form-group">
@ -100,6 +149,27 @@
<input type="text" id="profile-name" class="form-control"> <input type="text" id="profile-name" class="form-control">
</div> </div>
<div class="form-group">
<label>Usage conditions</label>
<p class="help-block">
Usage conditions can be assigned such that certain profiles are automatically used in different contexts.
For example, when <a href="#popup-content-scanning">Popup Content Scanning</a> is enabled, different profiles can be used
depending on the level of the popup.
</p>
<p class="help-block">
Conditions are organized into groups which represent how the conditions are checked.
If all of the conditions in any group are met, then the profile will automatically be used for that context.
If no conditions are specified, the profile will only be used if it is selected as the <strong>Active profile</strong>.
</p>
<div class="condition-groups" id="profile-condition-groups"></div>
</div>
<div class="form-group">
<button class="btn btn-default" id="profile-add-condition-group">Add Condition Group</button>
</div>
<div class="modal fade" tabindex="-1" role="dialog" id="profile-copy-modal"> <div class="modal fade" tabindex="-1" role="dialog" id="profile-copy-modal">
<div class="modal-dialog modal-dialog-centered"> <div class="modal-dialog modal-dialog-centered">
<div class="modal-content"> <div class="modal-content">
@ -136,6 +206,20 @@
</div> </div>
</div> </div>
</div> </div>
<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>
<input type="text" class="form-control" />
<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>
</div> </div>
<div> <div>
@ -563,9 +647,12 @@
<script src="/bg/js/anki.js"></script> <script src="/bg/js/anki.js"></script>
<script src="/bg/js/api.js"></script> <script src="/bg/js/api.js"></script>
<script src="/bg/js/conditions.js"></script>
<script src="/bg/js/conditions-ui.js"></script>
<script src="/bg/js/dictionary.js"></script> <script src="/bg/js/dictionary.js"></script>
<script src="/bg/js/handlebars.js"></script> <script src="/bg/js/handlebars.js"></script>
<script src="/bg/js/options.js"></script> <script src="/bg/js/options.js"></script>
<script src="/bg/js/profile-conditions.js"></script>
<script src="/bg/js/templates.js"></script> <script src="/bg/js/templates.js"></script>
<script src="/bg/js/util.js"></script> <script src="/bg/js/util.js"></script>

View File

@ -24,7 +24,8 @@ class DisplayFloat extends Display {
this.styleNode = null; this.styleNode = null;
this.optionsContext = { this.optionsContext = {
depth: 0 depth: 0,
url: window.location.href
}; };
this.dependencies = Object.assign({}, this.dependencies, {docRangeFromPoint, docSentenceExtract}); 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; this.optionsContext.depth = depth;
popupNestedInitialize(id, depth, parentFrameId); this.optionsContext.url = url;
popupNestedInitialize(id, depth, parentFrameId, url);
} }
}; };

View File

@ -27,7 +27,8 @@ class Frontend {
this.ignoreNodes = (Array.isArray(ignoreNodes) && ignoreNodes.length > 0 ? ignoreNodes.join(',') : null); this.ignoreNodes = (Array.isArray(ignoreNodes) && ignoreNodes.length > 0 ? ignoreNodes.join(',') : null);
this.optionsContext = { this.optionsContext = {
depth: popup.depth depth: popup.depth,
url: popup.url
}; };
this.primaryTouchIdentifier = null; this.primaryTouchIdentifier = null;
@ -42,9 +43,9 @@ class Frontend {
static create() { static create() {
const initializationData = window.frontendInitializationData; const initializationData = window.frontendInitializationData;
const isNested = (initializationData !== null && typeof initializationData === 'object'); 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); const frontend = new Frontend(popup, ignoreNodes);
frontend.prepare(); frontend.prepare();
return frontend; return frontend;
@ -52,7 +53,7 @@ class Frontend {
async prepare() { async prepare() {
try { try {
this.options = await apiOptionsGet(this.optionsContext); this.options = await apiOptionsGet(this.getOptionsContext());
window.addEventListener('message', this.onFrameMessage.bind(this)); window.addEventListener('message', this.onFrameMessage.bind(this));
window.addEventListener('mousedown', this.onMouseDown.bind(this)); window.addEventListener('mousedown', this.onMouseDown.bind(this));
@ -262,7 +263,7 @@ class Frontend {
} }
async updateOptions() { async updateOptions() {
this.options = await apiOptionsGet(this.optionsContext); this.options = await apiOptionsGet(this.getOptionsContext());
if (!this.options.enable) { if (!this.options.enable) {
this.searchClear(); this.searchClear();
} }
@ -335,7 +336,7 @@ class Frontend {
return; return;
} }
const {definitions, length} = await apiTermsFind(searchText, this.optionsContext); const {definitions, length} = await apiTermsFind(searchText, this.getOptionsContext());
if (definitions.length === 0) { if (definitions.length === 0) {
return false; return false;
} }
@ -368,7 +369,7 @@ class Frontend {
return; return;
} }
const definitions = await apiKanjiFind(searchText, this.optionsContext); const definitions = await apiKanjiFind(searchText, this.getOptionsContext());
if (definitions.length === 0) { if (definitions.length === 0) {
return false; return false;
} }
@ -512,6 +513,11 @@ class Frontend {
} }
} }
getOptionsContext() {
this.optionsContext.url = this.popup.url;
return this.optionsContext;
}
static isScanningModifierPressed(scanningModifier, mouseEvent) { static isScanningModifierPressed(scanningModifier, mouseEvent) {
switch (scanningModifier) { switch (scanningModifier) {
case 'alt': return mouseEvent.altKey; case 'alt': return mouseEvent.altKey;

View File

@ -19,13 +19,13 @@
let popupNestedInitialized = false; let popupNestedInitialized = false;
async function popupNestedInitialize(id, depth, parentFrameId) { async function popupNestedInitialize(id, depth, parentFrameId, url) {
if (popupNestedInitialized) { if (popupNestedInitialized) {
return; return;
} }
popupNestedInitialized = true; popupNestedInitialized = true;
const optionsContext = {depth}; const optionsContext = {depth, url};
const options = await apiOptionsGet(optionsContext); const options = await apiOptionsGet(optionsContext);
const popupNestingMaxDepth = options.scanning.popupNestingMaxDepth; const popupNestingMaxDepth = options.scanning.popupNestingMaxDepth;
@ -35,7 +35,7 @@ async function popupNestedInitialize(id, depth, parentFrameId) {
const ignoreNodes = options.scanning.enableOnPopupExpressions ? [] : [ '.expression', '.expression *' ]; const ignoreNodes = options.scanning.enableOnPopupExpressions ? [] : [ '.expression', '.expression *' ];
window.frontendInitializationData = {id, depth, parentFrameId, ignoreNodes}; window.frontendInitializationData = {id, depth, parentFrameId, ignoreNodes, url};
const scriptSrcs = [ const scriptSrcs = [
'/fg/js/frontend-api-sender.js', '/fg/js/frontend-api-sender.js',

View File

@ -18,7 +18,7 @@
class PopupProxy { class PopupProxy {
constructor(depth, parentId, parentFrameId) { constructor(depth, parentId, parentFrameId, url) {
this.parentId = parentId; this.parentId = parentId;
this.parentFrameId = parentFrameId; this.parentFrameId = parentFrameId;
this.id = null; this.id = null;
@ -26,6 +26,7 @@ class PopupProxy {
this.parent = null; this.parent = null;
this.child = null; this.child = null;
this.depth = depth; this.depth = depth;
this.url = url;
this.container = null; this.container = null;

View File

@ -59,7 +59,8 @@ class Popup {
this.invokeApi('popupNestedInitialize', { this.invokeApi('popupNestedInitialize', {
id: this.id, id: this.id,
depth: this.depth, depth: this.depth,
parentFrameId parentFrameId,
url: this.url
}); });
this.invokeApi('setOptions', { this.invokeApi('setOptions', {
general: { general: {
@ -311,4 +312,8 @@ class Popup {
parent.appendChild(this.container); parent.appendChild(this.container);
} }
} }
get url() {
return window.location.href;
}
} }