From a6b197ca73a6be50c5c5bf391391c9971ff8c5e2 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sat, 7 Oct 2017 14:14:02 +0900 Subject: [PATCH 1/9] reload settings on content --- src/components/background.js | 20 ++++++++++++++++++++ src/content/index.js | 4 ++++ src/content/messages.js | 1 + 3 files changed, 25 insertions(+) diff --git a/src/components/background.js b/src/components/background.js index 487e3af..de44dae 100644 --- a/src/components/background.js +++ b/src/components/background.js @@ -19,6 +19,8 @@ export default class BackgroundComponent { }); } }); + browser.tabs.onUpdated.addListener(this.onTabUpdated.bind(this)); + browser.tabs.onActivated.addListener(this.onTabActivated.bind(this)); } update() { @@ -62,4 +64,22 @@ export default class BackgroundComponent { this.store.dispatch(settingsActions.load()); } } + + onTabActivated(info) { + this.syncSettings(info.tabId); + } + + onTabUpdated(id, info) { + if (info.url) { + this.syncSettings(id); + } + } + + syncSettings(id) { + let { settings } = this.store.getState().setting; + return browser.tabs.sendMessage(id, { + type: messages.CONTENT_SET_SETTINGS, + settings, + }); + } } diff --git a/src/content/index.js b/src/content/index.js index b29118d..cd1a0af 100644 --- a/src/content/index.js +++ b/src/content/index.js @@ -2,6 +2,7 @@ import './console-frame.scss'; import * as consoleFrames from './console-frames'; import * as scrolls from 'content/scrolls'; import * as navigates from 'content/navigates'; +import * as settingActions from 'actions/setting'; import * as followActions from 'actions/follow'; import { createStore } from 'store'; import ContentInputComponent from 'components/content-input'; @@ -64,6 +65,9 @@ browser.runtime.onMessage.addListener((action) => { case messages.CONTENT_OPERATION: execOperation(action.operation); return Promise.resolve(); + case messages.CONTENT_SET_SETTINGS: + store.dispatch(settingActions.set(action.settings)); + return Promise.resolve(); default: return Promise.resolve(); } diff --git a/src/content/messages.js b/src/content/messages.js index 0e66fa0..8d416c7 100644 --- a/src/content/messages.js +++ b/src/content/messages.js @@ -1,5 +1,6 @@ export default { CONTENT_OPERATION: 'content.operation', + CONTENT_SET_SETTINGS: 'content.set.settings', CONSOLE_BLURRED: 'console.blured', CONSOLE_ENTERED: 'console.entered', From 8ff302a1f2870994cddc36fd461879eac951203d Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sat, 7 Oct 2017 18:38:57 +0900 Subject: [PATCH 2/9] store input keys in content script --- src/actions/operation.js | 48 +++++++++++++++++++++++---- src/background/index.js | 3 -- src/components/background-input.js | 53 ------------------------------ src/components/background.js | 9 ++--- src/components/content-input.js | 52 +++++++++++++++++++++++------ src/content/index.js | 42 ++--------------------- src/content/messages.js | 4 +-- 7 files changed, 92 insertions(+), 119 deletions(-) delete mode 100644 src/components/background-input.js diff --git a/src/actions/operation.js b/src/actions/operation.js index 295fd4f..0bb8310 100644 --- a/src/actions/operation.js +++ b/src/actions/operation.js @@ -2,6 +2,9 @@ import operations from 'shared/operations'; import messages from 'content/messages'; import * as tabs from 'background/tabs'; import * as zooms from 'background/zooms'; +import * as scrolls from 'content/scrolls'; +import * as navigates from 'content/navigates'; +import * as followActions from 'actions/follow'; const sendConsoleShowCommand = (tab, command) => { return browser.tabs.sendMessage(tab.id, { @@ -10,7 +13,43 @@ const sendConsoleShowCommand = (tab, command) => { }); }; -const exec = (operation, tab) => { +const exec = (operation) => { + switch (operation.type) { + case operations.SCROLL_LINES: + return scrolls.scrollLines(window, operation.count); + case operations.SCROLL_PAGES: + return scrolls.scrollPages(window, operation.count); + case operations.SCROLL_TOP: + return scrolls.scrollTop(window); + case operations.SCROLL_BOTTOM: + return scrolls.scrollBottom(window); + case operations.SCROLL_HOME: + return scrolls.scrollLeft(window); + case operations.SCROLL_END: + return scrolls.scrollRight(window); + case operations.FOLLOW_START: + return followActions.enable(false); + case operations.NAVIGATE_HISTORY_PREV: + return navigates.historyPrev(window); + case operations.NAVIGATE_HISTORY_NEXT: + return navigates.historyNext(window); + case operations.NAVIGATE_LINK_PREV: + return navigates.linkPrev(window); + case operations.NAVIGATE_LINK_NEXT: + return navigates.linkNext(window); + case operations.NAVIGATE_PARENT: + return navigates.parent(window); + case operations.NAVIGATE_ROOT: + return navigates.root(window); + default: + browser.runtime.sendMessage({ + type: messages.BACKGROUND_OPERATION, + operation, + }); + } +}; + +const execBackground = (operation, tab) => { switch (operation.type) { case operations.TAB_CLOSE: return tabs.closeTab(tab.id); @@ -45,11 +84,8 @@ const exec = (operation, tab) => { case operations.COMMAND_SHOW_BUFFER: return sendConsoleShowCommand(tab, 'buffer '); default: - return browser.tabs.sendMessage(tab.id, { - type: messages.CONTENT_OPERATION, - operation - }); + return Promise.resolve(); } }; -export { exec }; +export { exec, execBackground }; diff --git a/src/background/index.js b/src/background/index.js index b966c13..dbc10fb 100644 --- a/src/background/index.js +++ b/src/background/index.js @@ -1,7 +1,6 @@ import * as settingsActions from 'actions/setting'; import messages from 'content/messages'; import BackgroundComponent from 'components/background'; -import BackgroundInputComponent from 'components/background-input'; import reducers from 'reducers'; import { createStore } from 'store'; @@ -15,10 +14,8 @@ const store = createStore(reducers, (e, sender) => { } }); const backgroundComponent = new BackgroundComponent(store); -const backgroundInputComponent = new BackgroundInputComponent(store); store.subscribe((sender) => { backgroundComponent.update(sender); - backgroundInputComponent.update(sender); }); store.dispatch(settingsActions.load()); diff --git a/src/components/background-input.js b/src/components/background-input.js deleted file mode 100644 index bd6ecf9..0000000 --- a/src/components/background-input.js +++ /dev/null @@ -1,53 +0,0 @@ -import * as inputActions from 'actions/input'; -import * as operationActions from 'actions/operation'; - -export default class BackgroundInputComponent { - constructor(store) { - this.store = store; - this.keymaps = {}; - this.prevInputs = []; - } - - update(sender) { - let state = this.store.getState(); - this.reloadSettings(state.setting); - this.handleKeyInputs(sender, state.input); - } - - reloadSettings(setting) { - if (!setting.settings.json) { - return; - } - this.keymaps = JSON.parse(setting.settings.json).keymaps; - } - - handleKeyInputs(sender, input) { - if (JSON.stringify(this.prevInputs) === JSON.stringify(input)) { - return; - } - this.prevInputs = input; - - if (input.keys.length === 0) { - return; - } - if (sender) { - return this.handleKeysChanged(sender, input); - } - } - - handleKeysChanged(sender, input) { - let matched = Object.keys(this.keymaps).filter((keyStr) => { - return keyStr.startsWith(input.keys); - }); - if (matched.length === 0) { - this.store.dispatch(inputActions.clearKeys(), sender); - return Promise.resolve(); - } else if (matched.length > 1 || - matched.length === 1 && input.keys !== matched[0]) { - return Promise.resolve(); - } - let operation = this.keymaps[matched]; - this.store.dispatch(operationActions.exec(operation, sender.tab), sender); - this.store.dispatch(inputActions.clearKeys(), sender); - } -} diff --git a/src/components/background.js b/src/components/background.js index de44dae..79a7a6a 100644 --- a/src/components/background.js +++ b/src/components/background.js @@ -1,5 +1,5 @@ import messages from 'content/messages'; -import * as inputActions from 'actions/input'; +import * as operationActions from 'actions/operation'; import * as settingsActions from 'actions/setting'; import * as tabActions from 'actions/tab'; import * as commands from 'shared/commands'; @@ -37,9 +37,10 @@ export default class BackgroundComponent { onMessage(message, sender) { switch (message.type) { - case messages.KEYDOWN: + case messages.BACKGROUND_OPERATION: return this.store.dispatch( - inputActions.keyPress(message.key, message.ctrl), sender); + operationActions.execBackground(message.operation, sender.tab), + sender); case messages.OPEN_URL: if (message.newTab) { return this.store.dispatch( @@ -70,7 +71,7 @@ export default class BackgroundComponent { } onTabUpdated(id, info) { - if (info.url) { + if (info.status === 'complete') { this.syncSettings(id); } } diff --git a/src/components/content-input.js b/src/components/content-input.js index 38d57fd..1dbf48f 100644 --- a/src/components/content-input.js +++ b/src/components/content-input.js @@ -1,14 +1,43 @@ -import messages from 'content/messages'; +import * as inputActions from 'actions/input'; +import * as operationActions from 'actions/operation'; export default class ContentInputComponent { - constructor(target) { + constructor(target, store) { this.pressed = {}; + this.store = store; target.addEventListener('keypress', this.onKeyPress.bind(this)); target.addEventListener('keydown', this.onKeyDown.bind(this)); target.addEventListener('keyup', this.onKeyUp.bind(this)); } + update() { + let settings = this.store.getState().setting.settings; + if (!settings) { + return; + } + let input = this.store.getState().input; + let keymaps = JSON.parse(settings.json).keymaps; + + let matched = Object.keys(keymaps).filter((keyStr) => { + return keyStr.startsWith(input.keys); + }); + if (matched.length === 0) { + this.store.dispatch(inputActions.clearKeys()); + return Promise.resolve(); + } else if (matched.length > 1 || + matched.length === 1 && input.keys !== matched[0]) { + return Promise.resolve(); + } + let operation = keymaps[matched]; + try { + this.store.dispatch(operationActions.exec(operation)); + } catch (e) { + console.error(e); + } + this.store.dispatch(inputActions.clearKeys()); + } + onKeyPress(e) { if (this.pressed[e.key] && this.pressed[e.key] !== 'keypress') { return; @@ -30,18 +59,21 @@ export default class ContentInputComponent { } capture(e) { - if (e.target instanceof HTMLInputElement || - e.target instanceof HTMLTextAreaElement || - e.target instanceof HTMLSelectElement) { + if (this.fromInput(e)) { if (e.key === 'Escape' && e.target.blur) { e.target.blur(); } return; } - browser.runtime.sendMessage({ - type: messages.KEYDOWN, - key: e.key, - ctrl: e.ctrlKey - }); + if (e.key === 'OS') { + return; + } + this.store.dispatch(inputActions.keyPress(e.key, e.ctrlKey)); + } + + fromInput(e) { + return e.target instanceof HTMLInputElement || + e.target instanceof HTMLTextAreaElement || + e.target instanceof HTMLSelectElement; } } diff --git a/src/content/index.js b/src/content/index.js index cd1a0af..09143b0 100644 --- a/src/content/index.js +++ b/src/content/index.js @@ -1,70 +1,32 @@ import './console-frame.scss'; import * as consoleFrames from './console-frames'; -import * as scrolls from 'content/scrolls'; -import * as navigates from 'content/navigates'; import * as settingActions from 'actions/setting'; -import * as followActions from 'actions/follow'; import { createStore } from 'store'; import ContentInputComponent from 'components/content-input'; import FollowComponent from 'components/follow'; import reducers from 'reducers'; -import operations from 'shared/operations'; import messages from './messages'; const store = createStore(reducers); const followComponent = new FollowComponent(window.document.body, store); +const contentInputComponent = new ContentInputComponent(window, store); store.subscribe(() => { try { followComponent.update(); + contentInputComponent.update(); } catch (e) { console.error(e); } }); -// eslint-disable-next-line no-unused-vars -const contentInputComponent = new ContentInputComponent(window); consoleFrames.initialize(window.document); -const execOperation = (operation) => { - switch (operation.type) { - case operations.SCROLL_LINES: - return scrolls.scrollLines(window, operation.count); - case operations.SCROLL_PAGES: - return scrolls.scrollPages(window, operation.count); - case operations.SCROLL_TOP: - return scrolls.scrollTop(window); - case operations.SCROLL_BOTTOM: - return scrolls.scrollBottom(window); - case operations.SCROLL_HOME: - return scrolls.scrollLeft(window); - case operations.SCROLL_END: - return scrolls.scrollRight(window); - case operations.FOLLOW_START: - return store.dispatch(followActions.enable(false)); - case operations.NAVIGATE_HISTORY_PREV: - return navigates.historyPrev(window); - case operations.NAVIGATE_HISTORY_NEXT: - return navigates.historyNext(window); - case operations.NAVIGATE_LINK_PREV: - return navigates.linkPrev(window); - case operations.NAVIGATE_LINK_NEXT: - return navigates.linkNext(window); - case operations.NAVIGATE_PARENT: - return navigates.parent(window); - case operations.NAVIGATE_ROOT: - return navigates.root(window); - } -}; - browser.runtime.onMessage.addListener((action) => { switch (action.type) { case messages.CONSOLE_HIDE: window.focus(); consoleFrames.blur(window.document); return Promise.resolve(); - case messages.CONTENT_OPERATION: - execOperation(action.operation); - return Promise.resolve(); case messages.CONTENT_SET_SETTINGS: store.dispatch(settingActions.set(action.settings)); return Promise.resolve(); diff --git a/src/content/messages.js b/src/content/messages.js index 8d416c7..eb056a7 100644 --- a/src/content/messages.js +++ b/src/content/messages.js @@ -1,6 +1,6 @@ export default { - CONTENT_OPERATION: 'content.operation', CONTENT_SET_SETTINGS: 'content.set.settings', + BACKGROUND_OPERATION: 'background.operation', CONSOLE_BLURRED: 'console.blured', CONSOLE_ENTERED: 'console.entered', @@ -9,8 +9,6 @@ export default { CONSOLE_SHOW_ERROR: 'console.show.error', CONSOLE_HIDE: 'console.hide', - KEYDOWN: 'keydown', - OPEN_URL: 'open.url', SETTINGS_RELOAD: 'settings.reload', From 30641f1b75a2a21b3bc3b3605bae634be7e809c9 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sat, 7 Oct 2017 22:31:37 +0900 Subject: [PATCH 3/9] load settings from content --- src/components/background.js | 27 ++++++++++----------------- src/components/content-input.js | 2 +- src/content/index.js | 15 ++++++++++++--- src/content/messages.js | 3 ++- 4 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/components/background.js b/src/components/background.js index 79a7a6a..4961d85 100644 --- a/src/components/background.js +++ b/src/components/background.js @@ -19,8 +19,6 @@ export default class BackgroundComponent { }); } }); - browser.tabs.onUpdated.addListener(this.onTabUpdated.bind(this)); - browser.tabs.onActivated.addListener(this.onTabActivated.bind(this)); } update() { @@ -59,28 +57,23 @@ export default class BackgroundComponent { text: e.message, }); }); + case messages.SETTINGS_QUERY: + return Promise.resolve(this.store.getState().setting.settings); case messages.CONSOLE_QUERY_COMPLETIONS: return commands.complete(message.text, this.settings); case messages.SETTINGS_RELOAD: this.store.dispatch(settingsActions.load()); + return this.broadcastSettingsChanged(); } } - onTabActivated(info) { - this.syncSettings(info.tabId); - } - - onTabUpdated(id, info) { - if (info.status === 'complete') { - this.syncSettings(id); - } - } - - syncSettings(id) { - let { settings } = this.store.getState().setting; - return browser.tabs.sendMessage(id, { - type: messages.CONTENT_SET_SETTINGS, - settings, + broadcastSettingsChanged() { + return browser.tabs.query({}).then((tabs) => { + for (let tab of tabs) { + browser.tabs.sendMessage(tab.id, { + type: messages.SETTINGS_CHANGED, + }); + } }); } } diff --git a/src/components/content-input.js b/src/components/content-input.js index 1dbf48f..504835c 100644 --- a/src/components/content-input.js +++ b/src/components/content-input.js @@ -13,7 +13,7 @@ export default class ContentInputComponent { update() { let settings = this.store.getState().setting.settings; - if (!settings) { + if (!settings || !settings.json) { return; } let input = this.store.getState().input; diff --git a/src/content/index.js b/src/content/index.js index 09143b0..a2e3e3d 100644 --- a/src/content/index.js +++ b/src/content/index.js @@ -21,16 +21,25 @@ store.subscribe(() => { consoleFrames.initialize(window.document); +const reloadSettings = () => { + return browser.runtime.sendMessage({ + type: messages.SETTINGS_QUERY, + }).then((settings) => { + store.dispatch(settingActions.set(settings)); + }); +}; + browser.runtime.onMessage.addListener((action) => { switch (action.type) { case messages.CONSOLE_HIDE: window.focus(); consoleFrames.blur(window.document); return Promise.resolve(); - case messages.CONTENT_SET_SETTINGS: - store.dispatch(settingActions.set(action.settings)); - return Promise.resolve(); + case messages.SETTINGS_CHANGED: + return reloadSettings(); default: return Promise.resolve(); } }); + +reloadSettings(); diff --git a/src/content/messages.js b/src/content/messages.js index eb056a7..138f0e0 100644 --- a/src/content/messages.js +++ b/src/content/messages.js @@ -1,5 +1,6 @@ export default { - CONTENT_SET_SETTINGS: 'content.set.settings', + SETTINGS_QUERY: 'settings.query', + BACKGROUND_OPERATION: 'background.operation', CONSOLE_BLURRED: 'console.blured', From ea455059bd0af542f3e8c723fc7774e74b8abffe Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 8 Oct 2017 07:41:55 +0900 Subject: [PATCH 4/9] prevent keymaps in the page --- src/components/content-input.js | 59 +++++++++++++++++++-------------- src/content/index.js | 3 +- 2 files changed, 37 insertions(+), 25 deletions(-) diff --git a/src/components/content-input.js b/src/components/content-input.js index 504835c..488a51d 100644 --- a/src/components/content-input.js +++ b/src/components/content-input.js @@ -12,30 +12,6 @@ export default class ContentInputComponent { } update() { - let settings = this.store.getState().setting.settings; - if (!settings || !settings.json) { - return; - } - let input = this.store.getState().input; - let keymaps = JSON.parse(settings.json).keymaps; - - let matched = Object.keys(keymaps).filter((keyStr) => { - return keyStr.startsWith(input.keys); - }); - if (matched.length === 0) { - this.store.dispatch(inputActions.clearKeys()); - return Promise.resolve(); - } else if (matched.length > 1 || - matched.length === 1 && input.keys !== matched[0]) { - return Promise.resolve(); - } - let operation = keymaps[matched]; - try { - this.store.dispatch(operationActions.exec(operation)); - } catch (e) { - console.error(e); - } - this.store.dispatch(inputActions.clearKeys()); } onKeyPress(e) { @@ -68,7 +44,34 @@ export default class ContentInputComponent { if (e.key === 'OS') { return; } + let keymaps = this.keymaps(); + if (!keymaps) { + return; + } this.store.dispatch(inputActions.keyPress(e.key, e.ctrlKey)); + + if (this.mapKeys(keymaps)) { + e.preventDefault(); + e.stopPropagation(); + } + } + + mapKeys(keymaps) { + let input = this.store.getState().input; + let matched = Object.keys(keymaps).filter((keyStr) => { + return keyStr.startsWith(input.keys); + }); + if (matched.length === 0) { + this.store.dispatch(inputActions.clearKeys()); + return false; + } else if (matched.length > 1 || + matched.length === 1 && input.keys !== matched[0]) { + return true; + } + let operation = keymaps[matched]; + this.store.dispatch(operationActions.exec(operation)); + this.store.dispatch(inputActions.clearKeys()); + return true; } fromInput(e) { @@ -76,4 +79,12 @@ export default class ContentInputComponent { e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLSelectElement; } + + keymaps() { + let settings = this.store.getState().setting.settings; + if (!settings || !settings.json) { + return null; + } + return JSON.parse(settings.json).keymaps; + } } diff --git a/src/content/index.js b/src/content/index.js index a2e3e3d..2c13c70 100644 --- a/src/content/index.js +++ b/src/content/index.js @@ -9,7 +9,8 @@ import messages from './messages'; const store = createStore(reducers); const followComponent = new FollowComponent(window.document.body, store); -const contentInputComponent = new ContentInputComponent(window, store); +const contentInputComponent = + new ContentInputComponent(window.document.body, store); store.subscribe(() => { try { followComponent.update(); From 944683a2d8f9a165c490d05c350fdc689bdb6c85 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 8 Oct 2017 08:58:11 +0900 Subject: [PATCH 5/9] use key instead of keyCode in follow --- src/components/follow.js | 64 ++++++++-------------------------- src/reducers/follow.js | 4 +-- test/components/follow.test.js | 10 ------ test/reducers/follow.test.js | 20 +++++------ 4 files changed, 26 insertions(+), 72 deletions(-) diff --git a/src/components/follow.js b/src/components/follow.js index 9221759..365bb20 100644 --- a/src/components/follow.js +++ b/src/components/follow.js @@ -5,21 +5,6 @@ import HintKeyProducer from 'content/hint-key-producer'; const DEFAULT_HINT_CHARSET = 'abcdefghijklmnopqrstuvwxyz'; -const availableKey = (keyCode) => { - return ( - KeyboardEvent.DOM_VK_0 <= keyCode && keyCode <= KeyboardEvent.DOM_VK_9 || - KeyboardEvent.DOM_VK_A <= keyCode && keyCode <= KeyboardEvent.DOM_VK_Z - ); -}; - -const isNumericKey = (code) => { - return KeyboardEvent.DOM_VK_0 <= code && code <= KeyboardEvent.DOM_VK_9; -}; - -const isAlphabeticKey = (code) => { - return KeyboardEvent.DOM_VK_A <= code && code <= KeyboardEvent.DOM_VK_Z; -}; - const inWindow = (window, element) => { let { top, left, bottom, right @@ -49,8 +34,7 @@ export default class FollowComponent { this.create(); } else if (prevState.enabled && !this.state.enabled) { this.remove(); - } else if (JSON.stringify(prevState.keys) !== - JSON.stringify(this.state.keys)) { + } else if (prevState.keys !== this.state.keys) { this.updateHints(); } } @@ -60,23 +44,21 @@ export default class FollowComponent { return; } - let { keyCode } = e; - switch (keyCode) { - case KeyboardEvent.DOM_VK_ENTER: - case KeyboardEvent.DOM_VK_RETURN: - this.activate(this.hintElements[ - FollowComponent.codeChars(this.state.keys)].target); + let { key } = e; + switch (key) { + case 'Enter': + this.activate(this.hintElements[this.state.keys].target); return; - case KeyboardEvent.DOM_VK_ESCAPE: + case 'Escape': this.store.dispatch(followActions.disable()); return; - case KeyboardEvent.DOM_VK_BACK_SPACE: - case KeyboardEvent.DOM_VK_DELETE: + case 'Backspace': + case 'Delete': this.store.dispatch(followActions.backspace()); break; default: - if (availableKey(keyCode)) { - this.store.dispatch(followActions.keyPress(keyCode)); + if (DEFAULT_HINT_CHARSET.includes(key)) { + this.store.dispatch(followActions.keyPress(key)); } break; } @@ -86,18 +68,18 @@ export default class FollowComponent { } updateHints() { - let chars = FollowComponent.codeChars(this.state.keys); + let keys = this.state.keys; let shown = Object.keys(this.hintElements).filter((key) => { - return key.startsWith(chars); + return key.startsWith(keys); }); let hidden = Object.keys(this.hintElements).filter((key) => { - return !key.startsWith(chars); + return !key.startsWith(keys); }); if (shown.length === 0) { this.remove(); return; } else if (shown.length === 1) { - this.activate(this.hintElements[chars].target); + this.activate(this.hintElements[keys].target); this.remove(); } @@ -177,24 +159,6 @@ export default class FollowComponent { }); } - static codeChars(codes) { - const CHARCODE_ZERO = '0'.charCodeAt(0); - const CHARCODE_A = 'a'.charCodeAt(0); - - let chars = ''; - - for (let code of codes) { - if (isNumericKey(code)) { - chars += String.fromCharCode( - code - KeyboardEvent.DOM_VK_0 + CHARCODE_ZERO); - } else if (isAlphabeticKey(code)) { - chars += String.fromCharCode( - code - KeyboardEvent.DOM_VK_A + CHARCODE_A); - } - } - return chars; - } - static getTargetElements(doc) { let all = doc.querySelectorAll('a,button,input,textarea'); let filtered = Array.prototype.filter.call(all, (element) => { diff --git a/src/reducers/follow.js b/src/reducers/follow.js index a2397b4..27543ec 100644 --- a/src/reducers/follow.js +++ b/src/reducers/follow.js @@ -3,7 +3,7 @@ import actions from 'actions'; const defaultState = { enabled: false, newTab: false, - keys: [], + keys: '', }; export default function reducer(state = defaultState, action = {}) { @@ -19,7 +19,7 @@ export default function reducer(state = defaultState, action = {}) { }); case actions.FOLLOW_KEY_PRESS: return Object.assign({}, state, { - keys: state.keys.concat([action.key]), + keys: state.keys + action.key, }); case actions.FOLLOW_BACKSPACE: return Object.assign({}, state, { diff --git a/test/components/follow.test.js b/test/components/follow.test.js index c83e211..294bfc9 100644 --- a/test/components/follow.test.js +++ b/test/components/follow.test.js @@ -2,16 +2,6 @@ import { expect } from "chai"; import FollowComponent from 'components/follow'; describe('FollowComponent', () => { - describe('#codeChars', () => { - it('returns a string for key codes', () => { - let chars = [ - KeyboardEvent.DOM_VK_0, KeyboardEvent.DOM_VK_1, - KeyboardEvent.DOM_VK_A, KeyboardEvent.DOM_VK_B]; - expect(FollowComponent.codeChars(chars)).to.equal('01ab'); - expect(FollowComponent.codeChars([])).to.be.equal(''); - }); - }); - describe('#getTargetElements', () => { beforeEach(() => { document.body.innerHTML = __html__['test/components/follow.html']; diff --git a/test/reducers/follow.test.js b/test/reducers/follow.test.js index 79e75d4..0b5e3bd 100644 --- a/test/reducers/follow.test.js +++ b/test/reducers/follow.test.js @@ -7,7 +7,7 @@ describe('follow reducer', () => { let state = followReducer(undefined, {}); expect(state).to.have.property('enabled', false); expect(state).to.have.property('newTab'); - expect(state).to.have.deep.property('keys', []); + expect(state).to.have.deep.property('keys', ''); }); it ('returns next state for FOLLOW_ENABLE', () => { @@ -24,24 +24,24 @@ describe('follow reducer', () => { }); it ('returns next state for FOLLOW_KEY_PRESS', () => { - let action = { type: actions.FOLLOW_KEY_PRESS, key: 100}; - let state = followReducer({ keys: [] }, action); - expect(state).to.have.deep.property('keys', [100]); + let action = { type: actions.FOLLOW_KEY_PRESS, key: 'a'}; + let state = followReducer({ keys: '' }, action); + expect(state).to.have.deep.property('keys', 'a'); - action = { type: actions.FOLLOW_KEY_PRESS, key: 200}; + action = { type: actions.FOLLOW_KEY_PRESS, key: 'b'}; state = followReducer(state, action); - expect(state).to.have.deep.property('keys', [100, 200]); + expect(state).to.have.deep.property('keys', 'ab'); }); it ('returns next state for FOLLOW_BACKSPACE', () => { let action = { type: actions.FOLLOW_BACKSPACE }; - let state = followReducer({ keys: [100, 200] }, action); - expect(state).to.have.deep.property('keys', [100]); + let state = followReducer({ keys: 'ab' }, action); + expect(state).to.have.deep.property('keys', 'a'); state = followReducer(state, action); - expect(state).to.have.deep.property('keys', []); + expect(state).to.have.deep.property('keys', ''); state = followReducer(state, action); - expect(state).to.have.deep.property('keys', []); + expect(state).to.have.deep.property('keys', ''); }); }); From 6f9b217df86c72ba5c8187cc192f90639488cee3 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 8 Oct 2017 09:24:09 +0900 Subject: [PATCH 6/9] make content-input more simple --- src/components/content-input.js | 48 ++++++++------------------------- src/components/keymapper.js | 43 +++++++++++++++++++++++++++++ src/content/index.js | 5 ++++ 3 files changed, 59 insertions(+), 37 deletions(-) create mode 100644 src/components/keymapper.js diff --git a/src/components/content-input.js b/src/components/content-input.js index 488a51d..0f15937 100644 --- a/src/components/content-input.js +++ b/src/components/content-input.js @@ -1,10 +1,7 @@ -import * as inputActions from 'actions/input'; -import * as operationActions from 'actions/operation'; - export default class ContentInputComponent { - constructor(target, store) { + constructor(target) { this.pressed = {}; - this.store = store; + this.onKeyListeners = []; target.addEventListener('keypress', this.onKeyPress.bind(this)); target.addEventListener('keydown', this.onKeyDown.bind(this)); @@ -14,6 +11,10 @@ export default class ContentInputComponent { update() { } + onKey(cb) { + this.onKeyListeners.push(cb); + } + onKeyPress(e) { if (this.pressed[e.key] && this.pressed[e.key] !== 'keypress') { return; @@ -44,47 +45,20 @@ export default class ContentInputComponent { if (e.key === 'OS') { return; } - let keymaps = this.keymaps(); - if (!keymaps) { - return; - } - this.store.dispatch(inputActions.keyPress(e.key, e.ctrlKey)); - if (this.mapKeys(keymaps)) { + let stop = false; + for (let listener of this.onKeyListeners) { + stop = stop || listener(e.key, e.ctrlKey); + } + if (stop) { e.preventDefault(); e.stopPropagation(); } } - mapKeys(keymaps) { - let input = this.store.getState().input; - let matched = Object.keys(keymaps).filter((keyStr) => { - return keyStr.startsWith(input.keys); - }); - if (matched.length === 0) { - this.store.dispatch(inputActions.clearKeys()); - return false; - } else if (matched.length > 1 || - matched.length === 1 && input.keys !== matched[0]) { - return true; - } - let operation = keymaps[matched]; - this.store.dispatch(operationActions.exec(operation)); - this.store.dispatch(inputActions.clearKeys()); - return true; - } - fromInput(e) { return e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLSelectElement; } - - keymaps() { - let settings = this.store.getState().setting.settings; - if (!settings || !settings.json) { - return null; - } - return JSON.parse(settings.json).keymaps; - } } diff --git a/src/components/keymapper.js b/src/components/keymapper.js new file mode 100644 index 0000000..3685a4f --- /dev/null +++ b/src/components/keymapper.js @@ -0,0 +1,43 @@ +import * as inputActions from 'actions/input'; +import * as operationActions from 'actions/operation'; + +export default class KeymapperComponent { + constructor(store) { + this.store = store; + } + + update() { + } + + key(key, ctrl) { + let keymaps = this.keymaps(); + if (!keymaps) { + return; + } + this.store.dispatch(inputActions.keyPress(key, ctrl)); + + let input = this.store.getState().input; + let matched = Object.keys(keymaps).filter((keyStr) => { + return keyStr.startsWith(input.keys); + }); + if (matched.length === 0) { + this.store.dispatch(inputActions.clearKeys()); + return false; + } else if (matched.length > 1 || + matched.length === 1 && input.keys !== matched[0]) { + return true; + } + let operation = keymaps[matched]; + this.store.dispatch(operationActions.exec(operation)); + this.store.dispatch(inputActions.clearKeys()); + return true; + } + + keymaps() { + let settings = this.store.getState().setting.settings; + if (!settings || !settings.json) { + return null; + } + return JSON.parse(settings.json).keymaps; + } +} diff --git a/src/content/index.js b/src/content/index.js index 2c13c70..ad891ca 100644 --- a/src/content/index.js +++ b/src/content/index.js @@ -3,6 +3,7 @@ import * as consoleFrames from './console-frames'; import * as settingActions from 'actions/setting'; import { createStore } from 'store'; import ContentInputComponent from 'components/content-input'; +import KeymapperComponent from 'components/keymapper'; import FollowComponent from 'components/follow'; import reducers from 'reducers'; import messages from './messages'; @@ -11,6 +12,10 @@ const store = createStore(reducers); const followComponent = new FollowComponent(window.document.body, store); const contentInputComponent = new ContentInputComponent(window.document.body, store); +const keymapperComponent = new KeymapperComponent(store); +contentInputComponent.onKey((key, ctrl) => { + return keymapperComponent.key(key, ctrl); +}); store.subscribe(() => { try { followComponent.update(); From e2aae9cff249db0617f082a34a8a3a4f5dd1f4f1 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 8 Oct 2017 09:56:54 +0900 Subject: [PATCH 7/9] follow from content-input --- src/components/content-input.js | 3 +++ src/components/follow.js | 12 +++--------- src/content/index.js | 3 +++ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/content-input.js b/src/components/content-input.js index 0f15937..9568caf 100644 --- a/src/components/content-input.js +++ b/src/components/content-input.js @@ -49,6 +49,9 @@ export default class ContentInputComponent { let stop = false; for (let listener of this.onKeyListeners) { stop = stop || listener(e.key, e.ctrlKey); + if (stop) { + break; + } } if (stop) { e.preventDefault(); diff --git a/src/components/follow.js b/src/components/follow.js index 365bb20..25db0c4 100644 --- a/src/components/follow.js +++ b/src/components/follow.js @@ -22,9 +22,6 @@ export default class FollowComponent { this.store = store; this.hintElements = {}; this.state = {}; - - let doc = wrapper.ownerDocument; - doc.addEventListener('keydown', this.onKeyDown.bind(this)); } update() { @@ -39,12 +36,11 @@ export default class FollowComponent { } } - onKeyDown(e) { + key(key) { if (!this.state.enabled) { - return; + return false; } - let { key } = e; switch (key) { case 'Enter': this.activate(this.hintElements[this.state.keys].target); @@ -62,9 +58,7 @@ export default class FollowComponent { } break; } - - e.stopPropagation(); - e.preventDefault(); + return true; } updateHints() { diff --git a/src/content/index.js b/src/content/index.js index ad891ca..d380291 100644 --- a/src/content/index.js +++ b/src/content/index.js @@ -13,6 +13,9 @@ const followComponent = new FollowComponent(window.document.body, store); const contentInputComponent = new ContentInputComponent(window.document.body, store); const keymapperComponent = new KeymapperComponent(store); +contentInputComponent.onKey((key, ctrl) => { + return followComponent.key(key, ctrl); +}); contentInputComponent.onKey((key, ctrl) => { return keymapperComponent.key(key, ctrl); }); From 370e20d76ad52daad0f3fcae33372434b45e3bac Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 8 Oct 2017 10:00:06 +0900 Subject: [PATCH 8/9] disable follow after activate --- src/components/follow.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/follow.js b/src/components/follow.js index 25db0c4..eedbd4d 100644 --- a/src/components/follow.js +++ b/src/components/follow.js @@ -74,7 +74,7 @@ export default class FollowComponent { return; } else if (shown.length === 1) { this.activate(this.hintElements[keys].target); - this.remove(); + this.store.dispatch(followActions.disable()); } shown.forEach((key) => { From 38fee747603d37a99f1a8d156f41ea3d7c400b78 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 8 Oct 2017 10:12:52 +0900 Subject: [PATCH 9/9] reset follow on enabled --- src/reducers/follow.js | 1 + test/reducers/follow.test.js | 1 + 2 files changed, 2 insertions(+) diff --git a/src/reducers/follow.js b/src/reducers/follow.js index 27543ec..ed875e8 100644 --- a/src/reducers/follow.js +++ b/src/reducers/follow.js @@ -12,6 +12,7 @@ export default function reducer(state = defaultState, action = {}) { return Object.assign({}, state, { enabled: true, newTab: action.newTab, + keys: '', }); case actions.FOLLOW_DISABLE: return Object.assign({}, state, { diff --git a/test/reducers/follow.test.js b/test/reducers/follow.test.js index 0b5e3bd..e1db680 100644 --- a/test/reducers/follow.test.js +++ b/test/reducers/follow.test.js @@ -15,6 +15,7 @@ describe('follow reducer', () => { let state = followReducer({ enabled: false, newTab: false }, action); expect(state).to.have.property('enabled', true); expect(state).to.have.property('newTab', true); + expect(state).to.have.property('keys', ''); }); it ('returns next state for FOLLOW_DISABLE', () => {