From 879b5afe66ee79424c3ffee3951ef1c0b8c86eaa Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 10 Sep 2017 21:28:00 +0900 Subject: [PATCH] key input sequence as action/reducer --- src/actions/index.js | 4 ++ src/actions/input.js | 15 ++++++ src/background/index.js | 56 +++++++++++++------- src/background/{key-queue.js => keys.js} | 67 ++++++++---------------- src/background/tabs.js | 4 +- src/content/index.js | 10 +--- src/reducers/input.js | 23 ++++++++ test/background/key-queue.test.js | 50 ------------------ test/background/keys.test.js | 31 +++++++++++ 9 files changed, 134 insertions(+), 126 deletions(-) create mode 100644 src/actions/input.js rename src/background/{key-queue.js => keys.js} (57%) create mode 100644 src/reducers/input.js delete mode 100644 test/background/key-queue.test.js create mode 100644 test/background/keys.test.js diff --git a/src/actions/index.js b/src/actions/index.js index de3ab42..135dd4a 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -29,4 +29,8 @@ export default { FOLLOW_START: 'follow.start', HISTORY_PREV: 'history.prev', HISTORY_NEXT: 'history.next', + + // User input + INPUT_KEY_PRESS: 'input.key,press', + INPUT_CLEAR_KEYS: 'input.clear.keys', }; diff --git a/src/actions/input.js b/src/actions/input.js new file mode 100644 index 0000000..c72b9e0 --- /dev/null +++ b/src/actions/input.js @@ -0,0 +1,15 @@ +import actions from '../actions'; + +export function keyPress(code, ctrl) { + return { + type: actions.INPUT_KEY_PRESS, + code, + ctrl + }; +} + +export function clearKeys() { + return { + type: actions.INPUT_CLEAR_KEYS + } +} diff --git a/src/background/index.js b/src/background/index.js index 3fa8553..4d75b33 100644 --- a/src/background/index.js +++ b/src/background/index.js @@ -1,22 +1,10 @@ import * as tabs from './tabs'; -import KeyQueue from './key-queue'; +import * as keys from './keys'; +import * as inputActions from '../actions/input'; import backgroundReducers from '../reducers/background'; +import inputReducers from '../reducers/input'; -const queue = new KeyQueue(); - -const keyPressHandle = (request, sender) => { - let action = queue.push({ - code: request.code, - ctrl: request.ctrl - }); - if (!action) { - return Promise.resolve(); - } - - return backgroundReducers(undefined, action, sender).then(() => { - return browser.tabs.sendMessage(sender.tab.id, action); - }); -}; +let inputState = inputReducers(undefined, {}); const normalizeUrl = (string) => { try { @@ -51,8 +39,6 @@ const cmdEnterHandle = (request, sender) => { browser.runtime.onMessage.addListener((request, sender) => { switch (request.type) { - case 'event.keypress': - return keyPressHandle(request, sender); case 'event.cmd.enter': return cmdEnterHandle(request, sender); default: @@ -60,6 +46,36 @@ browser.runtime.onMessage.addListener((request, sender) => { } }); -browser.runtime.onMessage.addListener((action, sender) => { +const keyQueueChanged = (sender, prevState, state) => { + if (state.keys.length === 0) { + return Promise.resolve(); + } + + let prefix = keys.asKeymapChars(state.keys); + let matched = Object.keys(keys.defaultKeymap).filter((keys) => { + return keys.startsWith(prefix); + }); + if (matched.length == 0) { + return handleMessage(inputActions.clearKeys(), sender); + } else if (matched.length > 1 || matched.length === 1 && prefix !== matched[0]) { + return Promise.resolve(); + } + let action = keys.defaultKeymap[matched]; + return handleMessage(inputActions.clearKeys(), sender).then(() => { + return backgroundReducers(undefined, action, sender).then(() => { + return browser.tabs.sendMessage(sender.tab.id, action); + }); + }); +}; + +const handleMessage = (action, sender) => { + let nextInputState = inputReducers(inputState, action); + if (JSON.stringify(nextInputState) !== JSON.stringify(inputState)) { + let prevState = inputState; + inputState = nextInputState; + return keyQueueChanged(sender, prevState, inputState); + } return backgroundReducers(undefined, action, sender); -}); +}; + +browser.runtime.onMessage.addListener(handleMessage); diff --git a/src/background/key-queue.js b/src/background/keys.js similarity index 57% rename from src/background/key-queue.js rename to src/background/keys.js index 924bf77..0ce53fa 100644 --- a/src/background/key-queue.js +++ b/src/background/keys.js @@ -1,6 +1,6 @@ import actions from '../actions'; -const DEFAULT_KEYMAP = { +const defaultKeymap = { ':': { type: actions.CMD_OPEN }, 'o': { type: actions.CMD_TABS_OPEN, alter: false }, 'O': { type: actions.CMD_TABS_OPEN, alter: true }, @@ -32,51 +32,26 @@ const DEFAULT_KEYMAP = { 'L': { type: actions.HISTORY_NEXT }, } -export default class KeyQueue { - - constructor(keymap = DEFAULT_KEYMAP) { - this.data = []; - this.keymap = keymap; - } - - push(key) { - this.data.push(key); - - let current = this.asKeymapChars(); - let filtered = Object.keys(this.keymap).filter((keys) => { - return keys.startsWith(current); - }); - - if (filtered.length == 0) { - this.data = []; - return null; - } else if (filtered.length === 1 && current === filtered[0]) { - let action = this.keymap[filtered[0]]; - this.data = []; - return action; +const asKeymapChars = (keys) => { + return keys.map((k) => { + let c = String.fromCharCode(k.code); + if (k.ctrl) { + return ''; + } else { + return c } - return null; - } - - asKeymapChars() { - return this.data.map((k) => { - let c = String.fromCharCode(k.code); - if (k.ctrl) { - return ''; - } else { - return c - } - }).join(''); - } + }).join(''); +} - asCaretChars() { - return this.data.map((k) => { - let c = String.fromCharCode(k.code); - if (k.ctrl) { - return '^' + c.toUpperCase(); - } else { - return c; - } - }).join(''); - } +const asCaretChars = (keys) => { + return keys.map((k) => { + let c = String.fromCharCode(k.code); + if (k.ctrl) { + return '^' + c.toUpperCase(); + } else { + return c; + } + }).join(''); } + +export { defaultKeymap, asKeymapChars, asCaretChars }; diff --git a/src/background/tabs.js b/src/background/tabs.js index 111bbd9..bd69b4b 100644 --- a/src/background/tabs.js +++ b/src/background/tabs.js @@ -59,7 +59,7 @@ const getCompletions = (keyword) => { }; const selectPrevTab = (current, count) => { - return browser.tabs.query({ currentWindow: true }, (tabs) => { + return browser.tabs.query({ currentWindow: true }).then((tabs) => { if (tabs.length < 2) { return; } @@ -70,7 +70,7 @@ const selectPrevTab = (current, count) => { }; const selectNextTab = (current, count) => { - return browser.tabs.query({ currentWindow: true }, (tabs) => { + return browser.tabs.query({ currentWindow: true }).then((tabs) => { if (tabs.length < 2) { return; } diff --git a/src/content/index.js b/src/content/index.js index 314dfea..12d079f 100644 --- a/src/content/index.js +++ b/src/content/index.js @@ -1,4 +1,5 @@ import '../console/console-frame.scss'; +import * as inputActions from '../actions/input'; import * as consoleFrames from '../console/frames'; import actions from '../actions'; import contentReducer from '../reducers/content'; @@ -14,14 +15,7 @@ window.addEventListener("keypress", (e) => { if (e.target instanceof HTMLInputElement) { return; } - - let request = { - type: 'event.keypress', - code: e.which, - ctrl: e.ctrlKey, - } - - browser.runtime.sendMessage(request) + browser.runtime.sendMessage(inputActions.keyPress(e.which, e.ctrlKey)) .catch((err) => { console.error("Vim Vixen:", err); return consoleFrames.showError(err.message); diff --git a/src/reducers/input.js b/src/reducers/input.js new file mode 100644 index 0000000..25ff1a3 --- /dev/null +++ b/src/reducers/input.js @@ -0,0 +1,23 @@ +import actions from '../actions'; + +const defaultState = { + keys: [], +}; + +export default function reducer(state = defaultState, action = {}) { + switch (action.type) { + case actions.INPUT_KEY_PRESS: + return Object.assign({}, state, { + keys: state.keys.concat([{ + code: action.code, + ctrl: action.ctrl + }]) + }); + case actions.INPUT_CLEAR_KEYS: + return Object.assign({}, state, { + keys: [], + }); + default: + return state; + } +} diff --git a/test/background/key-queue.test.js b/test/background/key-queue.test.js deleted file mode 100644 index ac43228..0000000 --- a/test/background/key-queue.test.js +++ /dev/null @@ -1,50 +0,0 @@ -import { expect } from "chai"; -import KeyQueue from '../../src/background/key-queue'; - -describe("keyQueue class", () => { - const KEYMAP = { - 'gGG': [], - 'gg': [ 'scroll.top' ], - }; - - const g = 'g'.charCodeAt(0); - const G = 'G'.charCodeAt(0); - const x = 'x'.charCodeAt(0); - - describe("#push", () => { - it("returns matched action", () => { - let queue = new KeyQueue(KEYMAP); - queue.push({ code: g }); - let action = queue.push({ code: g }); - - expect(action).to.deep.equal([ 'scroll.top' ]); - }); - - it("returns null on no actions matched", () => { - let queue = new KeyQueue(KEYMAP); - queue.push({ code: g }); - let action = queue.push({ code: G }); - - expect(action).to.be.null; - expect(queue.asKeymapChars()).to.be.empty; - }); - }); - - describe('#asKeymapChars', () => { - let queue = new KeyQueue(KEYMAP); - queue.push({ code: g }); - queue.push({ code: x, ctrl: true }); - queue.push({ code: G }); - - expect(queue.asKeymapChars()).to.equal('gG'); - }); - - describe('#asCaretChars', () => { - let queue = new KeyQueue(KEYMAP); - queue.push({ code: g }); - queue.push({ code: x, ctrl: true }); - queue.push({ code: G }); - - expect(queue.asCaretChars()).to.equal('g^XG'); - }); -}); diff --git a/test/background/keys.test.js b/test/background/keys.test.js new file mode 100644 index 0000000..2cb9a3a --- /dev/null +++ b/test/background/keys.test.js @@ -0,0 +1,31 @@ +import { expect } from "chai"; +import * as keys from '../../src/background/keys'; + +describe("keys", () => { + const KEYMAP = { + 'gGG': [], + 'gg': { type: 'scroll.top' }, + }; + + const g = 'g'.charCodeAt(0); + const G = 'G'.charCodeAt(0); + const x = 'x'.charCodeAt(0); + + describe('#asKeymapChars', () => { + let keySequence = [ + { code: g }, + { code: x, ctrl: true }, + { code: G } + ]; + expect(keys.asKeymapChars(keySequence)).to.equal('gG'); + }); + + describe('#asCaretChars', () => { + let keySequence = [ + { code: g }, + { code: x, ctrl: true }, + { code: G } + ]; + expect(keys.asCaretChars(keySequence)).to.equal('g^XG'); + }); +});