Merge pull request #86 from ueokande/10-disable-temporary
Disable add-on temporary
This commit is contained in:
commit
7639e99b75
16 changed files with 269 additions and 27 deletions
|
@ -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
|
||||
|
||||
|
|
15
src/content/actions/addon.js
Normal file
15
src/content/actions/addon.js
Normal file
|
@ -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 };
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
24
src/content/reducers/addon.js
Normal file
24
src/content/reducers/addon.js
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
});
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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',
|
||||
|
|
26
test/content/actions/addon.test.js
Normal file
26
test/content/actions/addon.test.js
Normal file
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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", () => {
|
||||
|
|
102
test/content/components/common/input.test.js
Normal file
102
test/content/components/common/input.test.js
Normal file
|
@ -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 });
|
||||
})
|
||||
});
|
38
test/content/reducers/addon.test.js
Normal file
38
test/content/reducers/addon.test.js
Normal file
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
Reference in a new issue