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
This commit is contained in:
parent
acfdaa4f48
commit
77b744e675
@ -97,6 +97,7 @@
|
||||
"parseUrl": "readonly",
|
||||
"areSetsEqual": "readonly",
|
||||
"getSetIntersection": "readonly",
|
||||
"getSetDifference": "readonly",
|
||||
"EventDispatcher": "readonly",
|
||||
"EventListenerCollection": "readonly",
|
||||
"EXTENSION_IS_BROWSER_EDGE": "readonly"
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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([
|
||||
|
@ -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 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']
|
||||
]);
|
||||
|
||||
const objects = [];
|
||||
if (hasOwn(conditionDescriptors, type)) {
|
||||
const conditionDescriptor = conditionDescriptors[type];
|
||||
objects.push(conditionDescriptor);
|
||||
if (hasOwn(conditionDescriptor.operators, operator)) {
|
||||
const operatorDescriptor = conditionDescriptor.operators[operator];
|
||||
objects.push(operatorDescriptor);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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 ||
|
||||
|
@ -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 (!(
|
||||
|
Loading…
Reference in New Issue
Block a user