From 5b96559df819f496b39acb75c679f6b3d8c8e65d Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 26 Apr 2020 16:55:25 -0400 Subject: Error logging refactoring (#454) * Create new logging methods on yomichan object * Use new yomichan.logError instead of global logError * Remove old logError * Handle unhandledrejection events * Add addEventListener stub * Update log function * Update error conversion to support more types * Add log event * Add API log function * Log errors to the backend * Make error/warning logs update the badge * Clear log error indicator on extension button click * Log correct URL on the background page * Fix incorrect error conversion * Remove unhandledrejection handling Firefox doesn't support it properly. * Remove unused argument type from log function * Improve function name * Change console.warn to yomichan.logWarning * Move log forwarding initialization into main scripts --- ext/mixed/js/text-scanner.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ext/mixed/js/text-scanner.js') diff --git a/ext/mixed/js/text-scanner.js b/ext/mixed/js/text-scanner.js index 0cd12cd7..1c32714b 100644 --- a/ext/mixed/js/text-scanner.js +++ b/ext/mixed/js/text-scanner.js @@ -201,7 +201,7 @@ class TextScanner { } onError(error) { - logError(error, false); + yomichan.logError(error); } async scanTimerWait() { -- cgit v1.2.3 From 48c7010f4ea8daafd30e5650625c377affa0cecd Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Mon, 27 Apr 2020 18:10:37 -0400 Subject: Frontend refactor (part 1) (#484) * Remove _getVisualViewportScale * Use super's mouse event listener definitions * Remove redundant override * Remove getTouchEventListeners override * Rename Display.onSearchClear to onEscape * Change onSearchClear to clearSelection and use an event * Update how text is marked for selection and deselection * Replace onError with yomichan.logError * Update setEnabled to refresh all event listeners --- ext/bg/js/search-query-parser.js | 25 ++--------------- ext/bg/js/search.js | 2 +- ext/bg/js/settings/popup-preview-frame.js | 2 +- ext/fg/js/float.js | 2 +- ext/fg/js/frontend.js | 26 ++++++++--------- ext/mixed/js/display.js | 4 +-- ext/mixed/js/text-scanner.js | 46 +++++++++++++++---------------- 7 files changed, 42 insertions(+), 65 deletions(-) (limited to 'ext/mixed/js/text-scanner.js') diff --git a/ext/bg/js/search-query-parser.js b/ext/bg/js/search-query-parser.js index 0001c9ff..3215f8e4 100644 --- a/ext/bg/js/search-query-parser.js +++ b/ext/bg/js/search-query-parser.js @@ -44,12 +44,7 @@ class QueryParser extends TextScanner { await this.queryParserGenerator.prepare(); } - onError(error) { - yomichan.logError(error); - } - - onClick(e) { - super.onClick(e); + onClick2(e) { this.searchAt(e.clientX, e.clientY, 'click'); } @@ -84,22 +79,8 @@ class QueryParser extends TextScanner { getMouseEventListeners() { return [ - [this.node, 'click', this.onClick.bind(this)], - [this.node, 'mousedown', this.onMouseDown.bind(this)], - [this.node, 'mousemove', this.onMouseMove.bind(this)], - [this.node, 'mouseover', this.onMouseOver.bind(this)], - [this.node, 'mouseout', this.onMouseOut.bind(this)] - ]; - } - - getTouchEventListeners() { - return [ - [this.node, 'auxclick', this.onAuxClick.bind(this)], - [this.node, 'touchstart', this.onTouchStart.bind(this)], - [this.node, 'touchend', this.onTouchEnd.bind(this)], - [this.node, 'touchcancel', this.onTouchCancel.bind(this)], - [this.node, 'touchmove', this.onTouchMove.bind(this), {passive: false}], - [this.node, 'contextmenu', this.onContextMenu.bind(this)] + ...super.getMouseEventListeners(), + [this.node, 'click', this.onClick2.bind(this)] ]; } diff --git a/ext/bg/js/search.js b/ext/bg/js/search.js index cbd7b562..b7d2eed8 100644 --- a/ext/bg/js/search.js +++ b/ext/bg/js/search.js @@ -125,7 +125,7 @@ class DisplaySearch extends Display { yomichan.logError(error); } - onSearchClear() { + onEscape() { if (this.query === null) { return; } diff --git a/ext/bg/js/settings/popup-preview-frame.js b/ext/bg/js/settings/popup-preview-frame.js index 05a2a41b..e73c04a0 100644 --- a/ext/bg/js/settings/popup-preview-frame.js +++ b/ext/bg/js/settings/popup-preview-frame.js @@ -69,7 +69,7 @@ class SettingsPopupPreview { this.frontend.getOptionsContext = async () => this.optionsContext; this.frontend.setEnabled = () => {}; - this.frontend.onSearchClear = () => {}; + this.frontend.clearSelection = () => {}; await this.frontend.prepare(); diff --git a/ext/fg/js/float.js b/ext/fg/js/float.js index fd3b92cc..294093cd 100644 --- a/ext/fg/js/float.js +++ b/ext/fg/js/float.js @@ -92,7 +92,7 @@ class DisplayFloat extends Display { this._orphaned = true; } - onSearchClear() { + onEscape() { window.parent.postMessage('popupClose', '*'); } diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index 46921d36..50f52724 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -49,7 +49,7 @@ class Frontend extends TextScanner { this._lastShowPromise = Promise.resolve(); this._windowMessageHandlers = new Map([ - ['popupClose', () => this.onSearchClear(true)], + ['popupClose', () => this.clearSelection(false)], ['selectionCopy', () => document.execCommand('copy')] ]); @@ -79,10 +79,12 @@ class Frontend extends TextScanner { yomichan.on('zoomChanged', this.onZoomChanged.bind(this)); chrome.runtime.onMessage.addListener(this.onRuntimeMessage.bind(this)); + this.on('clearSelection', this.onClearSelection.bind(this)); + this._updateContentScale(); this._broadcastRootPopupInformation(); } catch (e) { - this.onError(e); + yomichan.logError(e); } } @@ -140,7 +142,7 @@ class Frontend extends TextScanner { } async setPopup(popup) { - this.onSearchClear(false); + this.clearSelection(true); this.popup = popup; await popup.setOptionsContext(await this.getOptionsContext(), this._id); } @@ -186,11 +188,11 @@ class Frontend extends TextScanner { this._showPopupContent(textSource, await this.getOptionsContext(), 'orphaned'); } } else { - this.onError(e); + yomichan.logError(e); } } finally { if (results === null && this.options.scanning.autoHideResults) { - this.onSearchClear(true); + this.clearSelection(false); } } @@ -238,10 +240,9 @@ class Frontend extends TextScanner { return {definitions, type: 'kanji'}; } - onSearchClear(changeFocus) { - this.popup.hide(changeFocus); + onClearSelection({passive}) { + this.popup.hide(!passive); this.popup.clearAutoPlayTimer(); - super.onSearchClear(changeFocus); } async getOptionsContext() { @@ -269,7 +270,9 @@ class Frontend extends TextScanner { contentScale /= this._pageZoomFactor; } if (popupScaleRelativeToVisualViewport) { - contentScale /= Frontend._getVisualViewportScale(); + const visualViewport = window.visualViewport; + const visualViewportScale = (visualViewport !== null && typeof visualViewport === 'object' ? visualViewport.scale : 1.0); + contentScale /= visualViewportScale; } if (contentScale === this._contentScale) { return; } @@ -302,9 +305,4 @@ class Frontend extends TextScanner { this._showPopupContent(textSource, await this.getOptionsContext()); } } - - static _getVisualViewportScale() { - const visualViewport = window.visualViewport; - return visualViewport !== null && typeof visualViewport === 'object' ? visualViewport.scale : 1.0; - } } diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index 70b7fcd3..32081c70 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -69,7 +69,7 @@ class Display { this._onKeyDownHandlers = new Map([ ['Escape', () => { - this.onSearchClear(); + this.onEscape(); return true; }], ['PageUp', (e) => { @@ -183,7 +183,7 @@ class Display { throw new Error('Override me'); } - onSearchClear() { + onEscape() { throw new Error('Override me'); } diff --git a/ext/mixed/js/text-scanner.js b/ext/mixed/js/text-scanner.js index 1c32714b..c582ccd8 100644 --- a/ext/mixed/js/text-scanner.js +++ b/ext/mixed/js/text-scanner.js @@ -21,8 +21,9 @@ * docRangeFromPoint */ -class TextScanner { +class TextScanner extends EventDispatcher { constructor(node, ignoreElements, ignorePoints) { + super(); this.node = node; this.ignoreElements = ignoreElements; this.ignorePoints = ignorePoints; @@ -32,6 +33,7 @@ class TextScanner { this.scanTimerPromise = null; this.causeCurrent = null; this.textSourceCurrent = null; + this.textSourceCurrentSelected = false; this.pendingLookup = false; this.options = null; @@ -92,7 +94,7 @@ class TextScanner { if (DOM.isMouseButtonDown(e, 'primary')) { this.scanTimerClear(); - this.onSearchClear(true); + this.clearSelection(false); } } @@ -200,10 +202,6 @@ class TextScanner { throw new Error('Override me'); } - onError(error) { - yomichan.logError(error); - } - async scanTimerWait() { const delay = this.options.scanning.delay; const promise = promiseTimeout(delay, true); @@ -225,17 +223,12 @@ class TextScanner { } setEnabled(enabled, canEnable) { - if (enabled && canEnable) { - if (!this.enabled) { - this.hookEvents(); - this.enabled = true; - } + this.eventListeners.removeAllEventListeners(); + this.enabled = enabled && canEnable; + if (this.enabled) { + this.hookEvents(); } else { - if (this.enabled) { - this.eventListeners.removeAllEventListeners(); - this.enabled = false; - } - this.onSearchClear(false); + this.clearSelection(true); } } @@ -300,10 +293,7 @@ class TextScanner { const result = await this.onSearchSource(textSource, cause); if (result !== null) { this.causeCurrent = cause; - this.textSourceCurrent = textSource; - if (this.options.scanning.selectText) { - textSource.select(); - } + this.setCurrentTextSource(textSource); } this.pendingLookup = false; } finally { @@ -312,7 +302,7 @@ class TextScanner { } } } catch (e) { - this.onError(e); + yomichan.logError(e); } } @@ -333,13 +323,15 @@ class TextScanner { } } - onSearchClear(_) { + clearSelection(passive) { if (this.textSourceCurrent !== null) { - if (this.options.scanning.selectText) { + if (this.textSourceCurrentSelected) { this.textSourceCurrent.deselect(); } this.textSourceCurrent = null; + this.textSourceCurrentSelected = false; } + this.trigger('clearSelection', {passive}); } getCurrentTextSource() { @@ -347,7 +339,13 @@ class TextScanner { } setCurrentTextSource(textSource) { - return this.textSourceCurrent = textSource; + this.textSourceCurrent = textSource; + if (this.options.scanning.selectText) { + this.textSourceCurrent.select(); + this.textSourceCurrentSelected = true; + } else { + this.textSourceCurrentSelected = false; + } } static isScanningModifierPressed(scanningModifier, mouseEvent) { -- cgit v1.2.3 From 08ada6844af424e8ff28e592fc6b9dbc1a9a97eb Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 2 May 2020 12:47:15 -0400 Subject: Remove Frontend inheritance (#486) * Make Frontend use composition instead of inheritance for TextScanner * Use push instead of concat * Update setOptions and setEnabled APIs * Update how onWindowMessage event listener is added/removed * Rename options to _options * Use bind instead of arrow function * Fix selection being cleared due to settings changes --- ext/bg/js/search-query-parser.js | 1 + ext/bg/js/settings/popup-preview-frame.js | 9 +-- ext/fg/js/frontend.js | 94 +++++++++++++++++++------------ ext/mixed/js/text-scanner.js | 22 ++++++-- 4 files changed, 77 insertions(+), 49 deletions(-) (limited to 'ext/mixed/js/text-scanner.js') diff --git a/ext/bg/js/search-query-parser.js b/ext/bg/js/search-query-parser.js index 3215f8e4..137234e8 100644 --- a/ext/bg/js/search-query-parser.js +++ b/ext/bg/js/search-query-parser.js @@ -86,6 +86,7 @@ class QueryParser extends TextScanner { setOptions(options) { super.setOptions(options); + super.setEnabled(true); this.queryParser.dataset.termSpacing = `${options.parsing.termSpacing}`; } diff --git a/ext/bg/js/settings/popup-preview-frame.js b/ext/bg/js/settings/popup-preview-frame.js index e73c04a0..cb548ed7 100644 --- a/ext/bg/js/settings/popup-preview-frame.js +++ b/ext/bg/js/settings/popup-preview-frame.js @@ -66,12 +66,10 @@ class SettingsPopupPreview { this.popup.setCustomOuterCss = this.popupSetCustomOuterCss.bind(this); this.frontend = new Frontend(this.popup); - this.frontend.getOptionsContext = async () => this.optionsContext; - this.frontend.setEnabled = () => {}; - this.frontend.clearSelection = () => {}; - await this.frontend.prepare(); + this.frontend.setDisabledOverride(true); + this.frontend.canClearSelection = false; // Update search this.updateSearch(); @@ -169,8 +167,7 @@ class SettingsPopupPreview { const source = new TextSourceRange(range, range.toString(), null, null); try { - await this.frontend.onSearchSource(source, 'script'); - this.frontend.setCurrentTextSource(source); + await this.frontend.setTextSource(source); } finally { source.cleanup(); } diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index 50f52724..76ad27e0 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -25,14 +25,8 @@ * docSentenceExtract */ -class Frontend extends TextScanner { +class Frontend { constructor(popup, getUrl=null) { - super( - window, - () => this.popup.isProxy() ? [] : [this.popup.getContainer()], - [(x, y) => this.popup.containsPoint(x, y)] - ); - this._id = yomichan.generateId(16); this.popup = popup; @@ -41,15 +35,23 @@ class Frontend extends TextScanner { this._disabledOverride = false; - this.options = null; + this._options = null; this._pageZoomFactor = 1.0; this._contentScale = 1.0; this._orphaned = false; this._lastShowPromise = Promise.resolve(); + this._enabledEventListeners = new EventListenerCollection(); + this._textScanner = new TextScanner( + window, + () => this.popup.isProxy() ? [] : [this.popup.getContainer()], + [(x, y) => this.popup.containsPoint(x, y)] + ); + this._textScanner.onSearchSource = this.onSearchSource.bind(this); + this._windowMessageHandlers = new Map([ - ['popupClose', () => this.clearSelection(false)], + ['popupClose', () => this._textScanner.clearSelection(false)], ['selectionCopy', () => document.execCommand('copy')] ]); @@ -60,6 +62,14 @@ class Frontend extends TextScanner { ]); } + get canClearSelection() { + return this._textScanner.canClearSelection; + } + + set canClearSelection(value) { + this._textScanner.canClearSelection = value; + } + async prepare() { try { await this.updateOptions(); @@ -79,7 +89,7 @@ class Frontend extends TextScanner { yomichan.on('zoomChanged', this.onZoomChanged.bind(this)); chrome.runtime.onMessage.addListener(this.onRuntimeMessage.bind(this)); - this.on('clearSelection', this.onClearSelection.bind(this)); + this._textScanner.on('clearSelection', this.onClearSelection.bind(this)); this._updateContentScale(); this._broadcastRootPopupInformation(); @@ -129,44 +139,45 @@ class Frontend extends TextScanner { this._updateContentScale(); } - getMouseEventListeners() { - return [ - ...super.getMouseEventListeners(), - [window, 'message', this.onWindowMessage.bind(this)] - ]; - } - setDisabledOverride(disabled) { this._disabledOverride = disabled; - this.setEnabled(this.options.general.enable, this._canEnable()); + this._updateTextScannerEnabled(); } async setPopup(popup) { - this.clearSelection(true); + this._textScanner.clearSelection(true); this.popup = popup; await popup.setOptionsContext(await this.getOptionsContext(), this._id); } async updateOptions() { const optionsContext = await this.getOptionsContext(); - this.options = await apiOptionsGet(optionsContext); - this.setOptions(this.options, this._canEnable()); + this._options = await apiOptionsGet(optionsContext); + this._textScanner.setOptions(this._options); + this._updateTextScannerEnabled(); const ignoreNodes = ['.scan-disable', '.scan-disable *']; - if (!this.options.scanning.enableOnPopupExpressions) { + if (!this._options.scanning.enableOnPopupExpressions) { ignoreNodes.push('.source-text', '.source-text *'); } - this.ignoreNodes = ignoreNodes.join(','); + this._textScanner.ignoreNodes = ignoreNodes.join(','); await this.popup.setOptionsContext(optionsContext, this._id); this._updateContentScale(); - if (this.textSourceCurrent !== null && this.causeCurrent !== null) { - await this.onSearchSource(this.textSourceCurrent, this.causeCurrent); + const textSourceCurrent = this._textScanner.getCurrentTextSource(); + const causeCurrent = this._textScanner.causeCurrent; + if (textSourceCurrent !== null && causeCurrent !== null) { + await this.onSearchSource(textSourceCurrent, causeCurrent); } } + async setTextSource(textSource) { + await this.onSearchSource(textSource, 'script'); + this._textScanner.setCurrentTextSource(textSource); + } + async onSearchSource(textSource, cause) { let results = null; @@ -184,15 +195,15 @@ class Frontend extends TextScanner { } } catch (e) { if (this._orphaned) { - if (textSource !== null && this.options.scanning.modifier !== 'none') { + if (textSource !== null && this._options.scanning.modifier !== 'none') { this._showPopupContent(textSource, await this.getOptionsContext(), 'orphaned'); } } else { yomichan.logError(e); } } finally { - if (results === null && this.options.scanning.autoHideResults) { - this.clearSelection(false); + if (results === null && this._options.scanning.autoHideResults) { + this._textScanner.clearSelection(false); } } @@ -201,7 +212,7 @@ class Frontend extends TextScanner { showContent(textSource, focus, definitions, type, optionsContext) { const {url} = optionsContext; - const sentence = docSentenceExtract(textSource, this.options.anki.sentenceExt); + const sentence = docSentenceExtract(textSource, this._options.anki.sentenceExt); this._showPopupContent( textSource, optionsContext, @@ -215,7 +226,7 @@ class Frontend extends TextScanner { } async findTerms(textSource, optionsContext) { - this.setTextSourceScanLength(textSource, this.options.scanning.length); + this._textScanner.setTextSourceScanLength(textSource, this._options.scanning.length); const searchText = textSource.text(); if (searchText.length === 0) { return null; } @@ -229,7 +240,7 @@ class Frontend extends TextScanner { } async findKanji(textSource, optionsContext) { - this.setTextSourceScanLength(textSource, 1); + this._textScanner.setTextSourceScanLength(textSource, 1); const searchText = textSource.text(); if (searchText.length === 0) { return null; } @@ -263,8 +274,21 @@ class Frontend extends TextScanner { return this._lastShowPromise; } + _updateTextScannerEnabled() { + const enabled = ( + this._options.general.enable && + this.popup.depth <= this._options.scanning.popupNestingMaxDepth && + !this._disabledOverride + ); + this._enabledEventListeners.removeAllEventListeners(); + this._textScanner.setEnabled(enabled); + if (enabled) { + this._enabledEventListeners.addEventListener(window, 'message', this.onWindowMessage.bind(this)); + } + } + _updateContentScale() { - const {popupScalingFactor, popupScaleRelativeToPageZoom, popupScaleRelativeToVisualViewport} = this.options.general; + const {popupScalingFactor, popupScaleRelativeToPageZoom, popupScaleRelativeToVisualViewport} = this._options.general; let contentScale = popupScalingFactor; if (popupScaleRelativeToPageZoom) { contentScale /= this._pageZoomFactor; @@ -295,12 +319,8 @@ class Frontend extends TextScanner { }); } - _canEnable() { - return this.popup.depth <= this.options.scanning.popupNestingMaxDepth && !this._disabledOverride; - } - async _updatePopupPosition() { - const textSource = this.getCurrentTextSource(); + const textSource = this._textScanner.getCurrentTextSource(); if (textSource !== null && await this.popup.isVisible()) { this._showPopupContent(textSource, await this.getOptionsContext()); } diff --git a/ext/mixed/js/text-scanner.js b/ext/mixed/js/text-scanner.js index c582ccd8..774eef44 100644 --- a/ext/mixed/js/text-scanner.js +++ b/ext/mixed/js/text-scanner.js @@ -45,6 +45,16 @@ class TextScanner extends EventDispatcher { this.preventNextMouseDown = false; this.preventNextClick = false; this.preventScroll = false; + + this._canClearSelection = true; + } + + get canClearSelection() { + return this._canClearSelection; + } + + set canClearSelection(value) { + this._canClearSelection = value; } onMouseOver(e) { @@ -222,9 +232,9 @@ class TextScanner extends EventDispatcher { } } - setEnabled(enabled, canEnable) { + setEnabled(enabled) { this.eventListeners.removeAllEventListeners(); - this.enabled = enabled && canEnable; + this.enabled = enabled; if (this.enabled) { this.hookEvents(); } else { @@ -233,9 +243,9 @@ class TextScanner extends EventDispatcher { } hookEvents() { - let eventListenerInfos = this.getMouseEventListeners(); + const eventListenerInfos = this.getMouseEventListeners(); if (this.options.scanning.touchInputEnabled) { - eventListenerInfos = eventListenerInfos.concat(this.getTouchEventListeners()); + eventListenerInfos.push(...this.getTouchEventListeners()); } for (const [node, type, listener, options] of eventListenerInfos) { @@ -264,9 +274,8 @@ class TextScanner extends EventDispatcher { ]; } - setOptions(options, canEnable=true) { + setOptions(options) { this.options = options; - this.setEnabled(this.options.general.enable, canEnable); } async searchAt(x, y, cause) { @@ -324,6 +333,7 @@ class TextScanner extends EventDispatcher { } clearSelection(passive) { + if (!this._canClearSelection) { return; } if (this.textSourceCurrent !== null) { if (this.textSourceCurrentSelected) { this.textSourceCurrent.deselect(); -- cgit v1.2.3 From 77b744e675f8abf17ff5e8433f4f1717e0c9ffb5 Mon Sep 17 00:00:00 2001 From: siikamiika Date: Sun, 3 May 2020 04:39:24 +0300 Subject: 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 --- .eslintrc.json | 1 + ext/bg/js/profile-conditions.js | 66 +++++++++++++++++ ext/bg/js/search.js | 3 +- ext/bg/js/settings/conditions-ui.js | 139 ++++++++++++++++++++++++++++++++---- ext/bg/settings.html | 5 +- ext/fg/js/frontend.js | 27 ++++++- ext/mixed/js/core.js | 6 ++ ext/mixed/js/display.js | 7 +- ext/mixed/js/dom.js | 14 ++++ ext/mixed/js/text-scanner.js | 3 + 10 files changed, 248 insertions(+), 23 deletions(-) (limited to 'ext/mixed/js/text-scanner.js') 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 @@
-
+