diff options
| -rw-r--r-- | .eslintrc.json | 1 | ||||
| -rw-r--r-- | ext/bg/js/profile-conditions.js | 66 | ||||
| -rw-r--r-- | ext/bg/js/search.js | 3 | ||||
| -rw-r--r-- | ext/bg/js/settings/conditions-ui.js | 139 | ||||
| -rw-r--r-- | ext/bg/settings.html | 5 | ||||
| -rw-r--r-- | ext/fg/js/frontend.js | 27 | ||||
| -rw-r--r-- | ext/mixed/js/core.js | 6 | ||||
| -rw-r--r-- | ext/mixed/js/display.js | 7 | ||||
| -rw-r--r-- | ext/mixed/js/dom.js | 14 | ||||
| -rw-r--r-- | ext/mixed/js/text-scanner.js | 3 | 
10 files changed, 248 insertions, 23 deletions
| 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 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 (!( |