aboutsummaryrefslogtreecommitdiff
path: root/ext
diff options
context:
space:
mode:
authorsiikamiika <siikamiika@users.noreply.github.com>2020-05-03 04:39:24 +0300
committerGitHub <noreply@github.com>2020-05-03 04:39:24 +0300
commit77b744e675f8abf17ff5e8433f4f1717e0c9ffb5 (patch)
tree037cb5c45dc1f9041130ea913d120e7f1526b1e1 /ext
parentacfdaa4f483790cf3d70a2c1a59d82a422ebed1f (diff)
Modifier key profile condition (#487)
* update Frontend options on modifier change * add modifier key profile condition * use select element for modifier condition value * support "is" and "is not" modifier key conditions * use plural * remove dead null check it's never null in that function * pass element on rather than assigning to this * rename event * remove Firefox OS key to Meta detection * hide Meta from dropdown on Firefox * move input type
Diffstat (limited to 'ext')
-rw-r--r--ext/bg/js/profile-conditions.js66
-rw-r--r--ext/bg/js/search.js3
-rw-r--r--ext/bg/js/settings/conditions-ui.js139
-rw-r--r--ext/bg/settings.html5
-rw-r--r--ext/fg/js/frontend.js27
-rw-r--r--ext/mixed/js/core.js6
-rw-r--r--ext/mixed/js/display.js7
-rw-r--r--ext/mixed/js/dom.js14
-rw-r--r--ext/mixed/js/text-scanner.js3
9 files changed, 247 insertions, 23 deletions
diff --git a/ext/bg/js/profile-conditions.js b/ext/bg/js/profile-conditions.js
index a0710bd1..c0f5d3f5 100644
--- a/ext/bg/js/profile-conditions.js
+++ b/ext/bg/js/profile-conditions.js
@@ -36,6 +36,24 @@ function _profileConditionTestDomainList(url, domainList) {
return false;
}
+const _profileModifierKeys = [
+ {optionValue: 'alt', name: 'Alt'},
+ {optionValue: 'ctrl', name: 'Ctrl'},
+ {optionValue: 'shift', name: 'Shift'}
+];
+
+if (!hasOwn(window, 'netscape')) {
+ _profileModifierKeys.push({optionValue: 'meta', name: 'Meta'});
+}
+
+const _profileModifierValueToName = new Map(
+ _profileModifierKeys.map(({optionValue, name}) => [optionValue, name])
+);
+
+const _profileModifierNameToValue = new Map(
+ _profileModifierKeys.map(({optionValue, name}) => [name, optionValue])
+);
+
const profileConditionsDescriptor = {
popupLevel: {
name: 'Popup Level',
@@ -100,5 +118,53 @@ const profileConditionsDescriptor = {
test: ({url}, transformedOptionValue) => (transformedOptionValue !== null && transformedOptionValue.test(url))
}
}
+ },
+ modifierKeys: {
+ name: 'Modifier Keys',
+ description: 'Use profile depending on the active modifier keys.',
+ values: _profileModifierKeys,
+ defaultOperator: 'are',
+ operators: {
+ are: {
+ name: 'are',
+ placeholder: 'Press one or more modifier keys here',
+ defaultValue: '',
+ type: 'keyMulti',
+ transform: (optionValue) => optionValue
+ .split(' + ')
+ .filter((v) => v.length > 0)
+ .map((v) => _profileModifierNameToValue.get(v)),
+ transformReverse: (transformedOptionValue) => transformedOptionValue
+ .map((v) => _profileModifierValueToName.get(v))
+ .join(' + '),
+ 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',
+ transform: (optionValue) => optionValue
+ .split(' + ')
+ .filter((v) => v.length > 0)
+ .map((v) => _profileModifierNameToValue.get(v)),
+ transformReverse: (transformedOptionValue) => transformedOptionValue
+ .map((v) => _profileModifierValueToName.get(v))
+ .join(' + '),
+ 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/search.js b/ext/bg/js/search.js
index b7d2eed8..47d495e6 100644
--- a/ext/bg/js/search.js
+++ b/ext/bg/js/search.js
@@ -17,6 +17,7 @@
/* global
* ClipboardMonitor
+ * DOM
* Display
* QueryParser
* apiClipboardGet
@@ -178,7 +179,7 @@ class DisplaySearch extends Display {
}
onKeyDown(e) {
- const key = Display.getKeyFromEvent(e);
+ const key = DOM.getKeyFromEvent(e);
const ignoreKeys = this._onKeyDownIgnoreKeys;
const activeModifierMap = new Map([
diff --git a/ext/bg/js/settings/conditions-ui.js b/ext/bg/js/settings/conditions-ui.js
index 84498b42..5b356101 100644
--- a/ext/bg/js/settings/conditions-ui.js
+++ b/ext/bg/js/settings/conditions-ui.js
@@ -16,6 +16,7 @@
*/
/* global
+ * DOM
* conditionsNormalizeOptionValue
*/
@@ -177,7 +178,8 @@ ConditionsUI.Condition = class Condition {
this.parent = parent;
this.condition = condition;
this.container = ConditionsUI.instantiateTemplate('#condition-template').appendTo(parent.container);
- this.input = this.container.find('input');
+ 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');
@@ -186,14 +188,13 @@ ConditionsUI.Condition = class Condition {
this.updateOperators();
this.updateInput();
- this.input.on('change', this.onInputChanged.bind(this));
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.input.off('change');
+ this.inputInner.off('change');
this.typeSelect.off('change');
this.operatorSelect.off('change');
this.removeButton.off('click');
@@ -236,21 +237,48 @@ ConditionsUI.Condition = class Condition {
updateInput() {
const conditionDescriptors = this.parent.parent.conditionDescriptors;
const {type, operator} = this.condition;
- const props = new Map([
- ['placeholder', ''],
- ['type', 'text']
- ]);
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} = this.validateValue(this.condition.value);
+ this.inputInner.toggleClass('is-invalid', !valid);
+ this.inputInner.val(this.condition.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);
@@ -266,12 +294,95 @@ ConditionsUI.Condition = class Condition {
}
for (const [prop, value] of props.entries()) {
- this.input.prop(prop, value);
+ inputInner.prop(prop, value);
}
- const {valid} = this.validateValue(this.condition.value);
- this.input.toggleClass('is-invalid', !valid);
- this.input.val(this.condition.value);
+ return inputInner;
+ }
+
+ createInputKeyMultiElement(objects) {
+ const inputInner = this.createInputElement(objects);
+
+ inputInner.prop('readonly', true);
+
+ let values = [];
+ for (const object of objects) {
+ if (hasOwn(object, 'values')) {
+ values = object.values;
+ }
+ }
+
+ const pressedKeyIndices = new Set();
+
+ const onKeyDown = ({originalEvent}) => {
+ const pressedKeyEventName = DOM.getKeyFromEvent(originalEvent);
+ if (pressedKeyEventName === 'Escape' || pressedKeyEventName === 'Backspace') {
+ pressedKeyIndices.clear();
+ inputInner.val('');
+ inputInner.change();
+ return;
+ }
+
+ const pressedModifiers = DOM.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(' + ');
+ 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', 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(defaultValue);
+ }
+
+ return inputInner;
}
validateValue(value) {
@@ -291,9 +402,9 @@ ConditionsUI.Condition = class Condition {
}
onInputChanged() {
- const {valid, value} = this.validateValue(this.input.val());
- this.input.toggleClass('is-invalid', !valid);
- this.input.val(value);
+ const {valid, value} = this.validateValue(this.inputInner.val());
+ this.inputInner.toggleClass('is-invalid', !valid);
+ this.inputInner.val(value);
this.condition.value = value;
this.save();
}
diff --git a/ext/bg/settings.html b/ext/bg/settings.html
index a0220e96..fc9221f8 100644
--- a/ext/bg/settings.html
+++ b/ext/bg/settings.html
@@ -117,7 +117,7 @@
<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"><input type="text" class="form-control" /></div>
+ <div class="condition-input"></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">
@@ -126,6 +126,9 @@
<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>
diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js
index 76ad27e0..d979246d 100644
--- a/ext/fg/js/frontend.js
+++ b/ext/fg/js/frontend.js
@@ -50,6 +50,9 @@ class Frontend {
);
this._textScanner.onSearchSource = this.onSearchSource.bind(this);
+ this._activeModifiers = new Set();
+ this._optionsUpdatePending = false;
+
this._windowMessageHandlers = new Map([
['popupClose', () => this._textScanner.clearSelection(false)],
['selectionCopy', () => document.execCommand('copy')]
@@ -90,6 +93,7 @@ class Frontend {
chrome.runtime.onMessage.addListener(this.onRuntimeMessage.bind(this));
this._textScanner.on('clearSelection', this.onClearSelection.bind(this));
+ this._textScanner.on('activeModifiersChanged', this.onActiveModifiersChanged.bind(this));
this._updateContentScale();
this._broadcastRootPopupInformation();
@@ -173,12 +177,21 @@ class Frontend {
}
}
+ async updatePendingOptions() {
+ if (this._optionsUpdatePending) {
+ this._optionsUpdatePending = false;
+ await this.updateOptions();
+ }
+ }
+
async setTextSource(textSource) {
await this.onSearchSource(textSource, 'script');
this._textScanner.setCurrentTextSource(textSource);
}
async onSearchSource(textSource, cause) {
+ await this.updatePendingOptions();
+
let results = null;
try {
@@ -254,12 +267,24 @@ class Frontend {
onClearSelection({passive}) {
this.popup.hide(!passive);
this.popup.clearAutoPlayTimer();
+ this.updatePendingOptions();
+ }
+
+ async onActiveModifiersChanged({modifiers}) {
+ if (areSetsEqual(modifiers, this._activeModifiers)) { return; }
+ this._activeModifiers = modifiers;
+ if (await this.popup.isVisible()) {
+ this._optionsUpdatePending = true;
+ return;
+ }
+ await this.updateOptions();
}
async getOptionsContext() {
const url = this._getUrl !== null ? await this._getUrl() : window.location.href;
const depth = this.popup.depth;
- return {depth, url};
+ const modifierKeys = [...this._activeModifiers];
+ return {depth, url, modifierKeys};
}
_showPopupContent(textSource, optionsContext, type=null, details=null) {
diff --git a/ext/mixed/js/core.js b/ext/mixed/js/core.js
index fbe9943a..835d9cea 100644
--- a/ext/mixed/js/core.js
+++ b/ext/mixed/js/core.js
@@ -146,6 +146,12 @@ function getSetIntersection(set1, set2) {
return result;
}
+function getSetDifference(set1, set2) {
+ return new Set(
+ [...set1].filter((value) => !set2.has(value))
+ );
+}
+
/*
* Async utilities
diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js
index 32081c70..783af7d8 100644
--- a/ext/mixed/js/display.js
+++ b/ext/mixed/js/display.js
@@ -338,7 +338,7 @@ class Display {
}
onKeyDown(e) {
- const key = Display.getKeyFromEvent(e);
+ const key = DOM.getKeyFromEvent(e);
const handler = this._onKeyDownHandlers.get(key);
if (typeof handler === 'function') {
if (handler(e)) {
@@ -964,11 +964,6 @@ class Display {
return elementRect.top - documentRect.top;
}
- static getKeyFromEvent(event) {
- const key = event.key;
- return (typeof key === 'string' ? (key.length === 1 ? key.toUpperCase() : key) : '');
- }
-
async _getNoteContext() {
const documentTitle = await this.getDocumentTitle();
return {
diff --git a/ext/mixed/js/dom.js b/ext/mixed/js/dom.js
index 31ba33d6..0e8f4462 100644
--- a/ext/mixed/js/dom.js
+++ b/ext/mixed/js/dom.js
@@ -63,6 +63,20 @@ class DOM {
}
}
+ static getActiveModifiers(event) {
+ const modifiers = new Set();
+ if (event.altKey) { modifiers.add('alt'); }
+ if (event.ctrlKey) { modifiers.add('ctrl'); }
+ if (event.metaKey) { modifiers.add('meta'); }
+ if (event.shiftKey) { modifiers.add('shift'); }
+ return modifiers;
+ }
+
+ static getKeyFromEvent(event) {
+ const key = event.key;
+ return (typeof key === 'string' ? (key.length === 1 ? key.toUpperCase() : key) : '');
+ }
+
static getFullscreenElement() {
return (
document.fullscreenElement ||
diff --git a/ext/mixed/js/text-scanner.js b/ext/mixed/js/text-scanner.js
index 774eef44..d74a04f8 100644
--- a/ext/mixed/js/text-scanner.js
+++ b/ext/mixed/js/text-scanner.js
@@ -70,6 +70,9 @@ class TextScanner extends EventDispatcher {
return;
}
+ const modifiers = DOM.getActiveModifiers(e);
+ this.trigger('activeModifiersChanged', {modifiers});
+
const scanningOptions = this.options.scanning;
const scanningModifier = scanningOptions.modifier;
if (!(