aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md1
-rw-r--r--src/content/actions/addon.js15
-rw-r--r--src/content/actions/index.js4
-rw-r--r--src/content/actions/input.js11
-rw-r--r--src/content/actions/operation.js7
-rw-r--r--src/content/components/common/index.js4
-rw-r--r--src/content/components/common/input.js29
-rw-r--r--src/content/components/common/keymapper.js16
-rw-r--r--src/content/reducers/addon.js24
-rw-r--r--src/content/reducers/index.js3
-rw-r--r--src/shared/default-settings.js3
-rw-r--r--src/shared/operations.js5
-rw-r--r--test/content/actions/addon.test.js26
-rw-r--r--test/content/actions/input.test.js8
-rw-r--r--test/content/components/common/input.test.js102
-rw-r--r--test/content/reducers/addon.test.js38
16 files changed, 269 insertions, 27 deletions
diff --git a/README.md b/README.md
index 066a706..8514e0c 100644
--- a/README.md
+++ b/README.md
@@ -47,6 +47,7 @@ The default mapping are shown in the following.
- <kbd>z</kbd><kbd>i</kbd>, <kbd>z</kbd><kbd>o</kbd>: zoom-in/zoom-out
- <kbd>z</kbd><kbd>z</kbd>: Set default zoom level
- <kbd>y</kbd>: copy URL in current tab
+- <kbd>Shift</kbd>+<kbd>Esc</kbd>: enable of disable the add-on in current tab.
### Console commands
diff --git a/src/content/actions/addon.js b/src/content/actions/addon.js
new file mode 100644
index 0000000..8d38025
--- /dev/null
+++ b/src/content/actions/addon.js
@@ -0,0 +1,15 @@
+import actions from 'content/actions';
+
+const enable = () => {
+ return { type: actions.ADDON_ENABLE };
+};
+
+const disable = () => {
+ return { type: actions.ADDON_DISABLE };
+};
+
+const toggleEnabled = () => {
+ return { type: actions.ADDON_TOGGLE_ENABLED };
+};
+
+export { enable, disable, toggleEnabled };
diff --git a/src/content/actions/index.js b/src/content/actions/index.js
index f8db948..085d510 100644
--- a/src/content/actions/index.js
+++ b/src/content/actions/index.js
@@ -1,5 +1,9 @@
export default {
// User input
+ ADDON_ENABLE: 'addon.enable',
+ ADDON_DISABLE: 'addon.disable',
+ ADDON_TOGGLE_ENABLED: 'addon.toggle.enabled',
+
INPUT_KEY_PRESS: 'input.key,press',
INPUT_CLEAR_KEYS: 'input.clear.keys',
INPUT_SET_KEYMAPS: 'input.set.keymaps',
diff --git a/src/content/actions/input.js b/src/content/actions/input.js
index 10ff835..31cfee3 100644
--- a/src/content/actions/input.js
+++ b/src/content/actions/input.js
@@ -1,16 +1,9 @@
import actions from 'content/actions';
-const asKeymapChars = (key, ctrl) => {
- if (ctrl) {
- return '<C-' + key.toUpperCase() + '>';
- }
- return key;
-};
-
-const keyPress = (key, ctrl) => {
+const keyPress = (key) => {
return {
type: actions.INPUT_KEY_PRESS,
- key: asKeymapChars(key, ctrl),
+ key,
};
};
diff --git a/src/content/actions/operation.js b/src/content/actions/operation.js
index 81bcc2f..897f361 100644
--- a/src/content/actions/operation.js
+++ b/src/content/actions/operation.js
@@ -4,9 +4,16 @@ import * as scrolls from 'content/scrolls';
import * as navigates from 'content/navigates';
import * as urls from 'content/urls';
import * as consoleFrames from 'content/console-frames';
+import * as addonActions from './addon';
const exec = (operation) => {
switch (operation.type) {
+ case operations.ADDON_ENABLE:
+ return addonActions.enable();
+ case operations.ADDON_DISABLE:
+ return addonActions.disable();
+ case operations.ADDON_TOGGLE_ENABLED:
+ return addonActions.toggleEnabled();
case operations.SCROLL_VERTICALLY:
return scrolls.scrollVertically(window, operation.count);
case operations.SCROLL_HORIZONALLY:
diff --git a/src/content/components/common/index.js b/src/content/components/common/index.js
index 98a2337..db0ac43 100644
--- a/src/content/components/common/index.js
+++ b/src/content/components/common/index.js
@@ -10,8 +10,8 @@ export default class Common {
const input = new InputComponent(win.document.body, store);
const keymapper = new KeymapperComponent(store);
- input.onKey((key, ctrl) => follow.key(key, ctrl));
- input.onKey((key, ctrl) => keymapper.key(key, ctrl));
+ input.onKey(key => follow.key(key));
+ input.onKey(key => keymapper.key(key));
this.store = store;
this.children = [
diff --git a/src/content/components/common/input.js b/src/content/components/common/input.js
index df09894..f285b0c 100644
--- a/src/content/components/common/input.js
+++ b/src/content/components/common/input.js
@@ -1,3 +1,21 @@
+const modifierdKeyName = (name) => {
+ if (name.length === 1) {
+ return name.toUpperCase();
+ } else if (name === 'Escape') {
+ return 'Esc';
+ }
+ return name;
+};
+
+const mapKey = (e) => {
+ if (e.ctrlKey) {
+ return '<C-' + modifierdKeyName(e.key) + '>';
+ } else if (e.shiftKey && e.key.length !== 1) {
+ return '<S-' + modifierdKeyName(e.key) + '>';
+ }
+ return e.key;
+};
+
export default class InputComponent {
constructor(target) {
this.pressed = {};
@@ -47,17 +65,16 @@ export default class InputComponent {
return;
}
- let stop = false;
+ let key = mapKey(e);
+
for (let listener of this.onKeyListeners) {
- stop = stop || listener(e.key, e.ctrlKey);
+ let stop = listener(key);
if (stop) {
+ e.preventDefault();
+ e.stopPropagation();
break;
}
}
- if (stop) {
- e.preventDefault();
- e.stopPropagation();
- }
}
fromInput(e) {
diff --git a/src/content/components/common/keymapper.js b/src/content/components/common/keymapper.js
index 655c3f2..5070cd8 100644
--- a/src/content/components/common/keymapper.js
+++ b/src/content/components/common/keymapper.js
@@ -1,5 +1,6 @@
import * as inputActions from 'content/actions/input';
import * as operationActions from 'content/actions/operation';
+import operations from 'shared/operations';
export default class KeymapperComponent {
constructor(store) {
@@ -9,13 +10,24 @@ export default class KeymapperComponent {
update() {
}
- key(key, ctrl) {
- this.store.dispatch(inputActions.keyPress(key, ctrl));
+ key(key) {
+ let enabled = this.store.getState().addon.enabled;
+
+ this.store.dispatch(inputActions.keyPress(key));
let input = this.store.getState().input;
let matched = Object.keys(input.keymaps).filter((keyStr) => {
return keyStr.startsWith(input.keys);
});
+ if (!enabled) {
+ // available keymaps are only ADDON_ENABLE and ADDON_TOGGLE_ENABLED if
+ // the addon disabled
+ matched = matched.filter((keys) => {
+ let type = input.keymaps[keys].type;
+ return type === operations.ADDON_ENABLE ||
+ type === operations.ADDON_TOGGLE_ENABLED;
+ });
+ }
if (matched.length === 0) {
this.store.dispatch(inputActions.clearKeys());
return false;
diff --git a/src/content/reducers/addon.js b/src/content/reducers/addon.js
new file mode 100644
index 0000000..8cc5ef1
--- /dev/null
+++ b/src/content/reducers/addon.js
@@ -0,0 +1,24 @@
+import actions from 'content/actions';
+
+const defaultState = {
+ enabled: true,
+};
+
+export default function reducer(state = defaultState, action = {}) {
+ switch (action.type) {
+ case actions.ADDON_ENABLE:
+ return Object.assign({}, state, {
+ enabled: true,
+ });
+ case actions.ADDON_DISABLE:
+ return Object.assign({}, state, {
+ enabled: false,
+ });
+ case actions.ADDON_TOGGLE_ENABLED:
+ return Object.assign({}, state, {
+ enabled: !state.enabled,
+ });
+ default:
+ return state;
+ }
+}
diff --git a/src/content/reducers/index.js b/src/content/reducers/index.js
index c026a19..7cf318e 100644
--- a/src/content/reducers/index.js
+++ b/src/content/reducers/index.js
@@ -1,14 +1,17 @@
+import addonReducer from './addon';
import inputReducer from './input';
import followReducer from './follow';
// Make setting reducer instead of re-use
const defaultState = {
+ addon: addonReducer(undefined, {}),
input: inputReducer(undefined, {}),
follow: followReducer(undefined, {}),
};
export default function reducer(state = defaultState, action = {}) {
return Object.assign({}, state, {
+ addon: addonReducer(state.addon, action),
input: inputReducer(state.input, action),
follow: followReducer(state.follow, action),
});
diff --git a/src/shared/default-settings.js b/src/shared/default-settings.js
index d280185..60e7876 100644
--- a/src/shared/default-settings.js
+++ b/src/shared/default-settings.js
@@ -41,7 +41,8 @@ export default {
"]]": { "type": "navigate.link.next" },
"gu": { "type": "navigate.parent" },
"gU": { "type": "navigate.root" },
- "y": { "type": "urls.yank" }
+ "y": { "type": "urls.yank" },
+ "<S-Esc>": { "type": "addon.toggle.enabled" }
},
"search": {
"default": "google",
diff --git a/src/shared/operations.js b/src/shared/operations.js
index 0d2a381..d5c2985 100644
--- a/src/shared/operations.js
+++ b/src/shared/operations.js
@@ -1,4 +1,9 @@
export default {
+ // Addons
+ ADDON_ENABLE: 'addon.enable',
+ ADDON_DISABLE: 'addon.disable',
+ ADDON_TOGGLE_ENABLED: 'addon.toggle.enabled',
+
// Command
COMMAND_SHOW: 'command.show',
COMMAND_SHOW_OPEN: 'command.show.open',
diff --git a/test/content/actions/addon.test.js b/test/content/actions/addon.test.js
new file mode 100644
index 0000000..7f244dc
--- /dev/null
+++ b/test/content/actions/addon.test.js
@@ -0,0 +1,26 @@
+import { expect } from "chai";
+import actions from 'content/actions';
+import * as addonActions from 'content/actions/addon';
+
+describe("addon actions", () => {
+ describe("enable", () => {
+ it('create ADDON_ENABLE action', () => {
+ let action = addonActions.enable();
+ expect(action.type).to.equal(actions.ADDON_ENABLE);
+ });
+ });
+
+ describe("disable", () => {
+ it('create ADDON_DISABLE action', () => {
+ let action = addonActions.disable();
+ expect(action.type).to.equal(actions.ADDON_DISABLE);
+ });
+ });
+
+ describe("toggle", () => {
+ it('create ADDON_TOGGLE_ENABLED action', () => {
+ let action = addonActions.toggleEnabled();
+ expect(action.type).to.equal(actions.ADDON_TOGGLE_ENABLED);
+ });
+ });
+});
diff --git a/test/content/actions/input.test.js b/test/content/actions/input.test.js
index 6031829..30705d2 100644
--- a/test/content/actions/input.test.js
+++ b/test/content/actions/input.test.js
@@ -5,16 +5,10 @@ import * as inputActions from 'content/actions/input';
describe("input actions", () => {
describe("keyPress", () => {
it('create INPUT_KEY_PRESS action', () => {
- let action = inputActions.keyPress('a', false);
+ let action = inputActions.keyPress('a');
expect(action.type).to.equal(actions.INPUT_KEY_PRESS);
expect(action.key).to.equal('a');
});
-
- it('create INPUT_KEY_PRESS action from key with ctrl', () => {
- let action = inputActions.keyPress('b', true);
- expect(action.type).to.equal(actions.INPUT_KEY_PRESS);
- expect(action.key).to.equal('<C-B>');
- });
});
describe("clearKeys", () => {
diff --git a/test/content/components/common/input.test.js b/test/content/components/common/input.test.js
new file mode 100644
index 0000000..912ac34
--- /dev/null
+++ b/test/content/components/common/input.test.js
@@ -0,0 +1,102 @@
+import InputComponent from 'content/components/common/input';
+import { expect } from "chai";
+
+describe('InputComponent', () => {
+ it('register callbacks', () => {
+ let component = new InputComponent(window.document);
+ component.onKey((key) => {
+ expect(key).is.equals('a');
+ });
+ component.onKeyDown({ key: 'a' });
+ });
+
+ it('invoke callback once', () => {
+ let component = new InputComponent(window.document);
+ let a = 0, b = 0;
+ component.onKey((key) => {
+ if (key == 'a') {
+ ++a;
+ } else {
+ key == 'b'
+ ++b;
+ }
+ });
+ component.onKeyDown({ key: 'a' });
+ component.onKeyDown({ key: 'b' });
+ component.onKeyPress({ key: 'a' });
+ component.onKeyUp({ key: 'a' });
+ component.onKeyPress({ key: 'b' });
+ component.onKeyUp({ key: 'b' });
+
+ expect(a).is.equals(1);
+ expect(b).is.equals(1);
+ })
+
+ it('add prefix when ctrl pressed', () => {
+ let component = new InputComponent(window.document);
+ component.onKey((key) => {
+ expect(key).is.equals('<C-A>');
+ });
+ component.onKeyDown({ key: 'a', ctrlKey: true });
+ })
+
+ it('press X', () => {
+ let component = new InputComponent(window.document);
+ component.onKey((key) => {
+ expect(key).is.equals('X');
+ });
+ component.onKeyDown({ key: 'X', shiftKey: true });
+ })
+
+ it('press <Shift> + <Esc>', () => {
+ let component = new InputComponent(window.document);
+ component.onKey((key) => {
+ expect(key).is.equals('<S-Esc>');
+ });
+ component.onKeyDown({ key: 'Escape', shiftKey: true });
+ })
+
+ it('press <Ctrl> + <Esc>', () => {
+ let component = new InputComponent(window.document);
+ component.onKey((key) => {
+ expect(key).is.equals('<C-Esc>');
+ });
+ component.onKeyDown({ key: 'Escape', ctrlKey: true });
+ })
+
+ it('does not invoke only meta keys', () => {
+ let component = new InputComponent(window.document);
+ component.onKey((key) => {
+ expect.fail();
+ });
+ component.onKeyDown({ key: 'Shift' });
+ component.onKeyDown({ key: 'Control' });
+ component.onKeyDown({ key: 'Alt' });
+ component.onKeyDown({ key: 'OS' });
+ })
+
+ it('ignores events from input elements', () => {
+ ['input', 'textarea', 'select'].forEach((name) => {
+ let target = window.document.createElement(name);
+ let component = new InputComponent(target);
+ component.onKey((key) => {
+ expect.fail();
+ });
+ component.onKeyDown({ key: 'x', target });
+ });
+ });
+
+ it('ignores events from contenteditable elements', () => {
+ let target = window.document.createElement('div');
+ let component = new InputComponent(target);
+ component.onKey((key) => {
+ expect.fail();
+ });
+
+ target.setAttribute('contenteditable', '');
+ component.onKeyDown({ key: 'x', target });
+
+ target.setAttribute('contenteditable', 'true');
+ component.onKeyDown({ key: 'x', target });
+ })
+});
diff --git a/test/content/reducers/addon.test.js b/test/content/reducers/addon.test.js
new file mode 100644
index 0000000..93f97e8
--- /dev/null
+++ b/test/content/reducers/addon.test.js
@@ -0,0 +1,38 @@
+import { expect } from "chai";
+import actions from 'content/actions';
+import addonReducer from 'content/reducers/addon';
+
+describe("addon reducer", () => {
+ it('return the initial state', () => {
+ let state = addonReducer(undefined, {});
+ expect(state).to.have.property('enabled', true);
+ });
+
+ it('return next state for ADDON_ENABLE', () => {
+ let action = { type: actions.ADDON_ENABLE};
+ let prev = { enabled: false };
+ let state = addonReducer(prev, action);
+
+ expect(state.enabled).is.equal(true);
+ });
+
+ it('return next state for ADDON_DISABLE', () => {
+ let action = { type: actions.ADDON_DISABLE};
+ let prev = { enabled: true };
+ let state = addonReducer(prev, action);
+
+ expect(state.enabled).is.equal(false);
+ });
+
+ it('return next state for ADDON_TOGGLE_ENABLED', () => {
+ let action = { type: actions.ADDON_TOGGLE_ENABLED };
+ let state = { enabled: false };
+
+ state = addonReducer(state, action);
+ expect(state.enabled).is.equal(true);
+
+ state = addonReducer(state, action);
+ expect(state.enabled).is.equal(false);
+ });
+
+});