diff --git a/.eslintrc.json b/.eslintrc.json index a2de6671..3186a491 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -97,6 +97,7 @@ "parseUrl": "readonly", "areSetsEqual": "readonly", "getSetIntersection": "readonly", + "getSetDifference": "readonly", "EventDispatcher": "readonly", "EventListenerCollection": "readonly", "EXTENSION_IS_BROWSER_EDGE": "readonly" 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 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(); } 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 @@
-
+