aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--QA.md106
-rw-r--r--README.md3
-rw-r--r--manifest.json5
-rw-r--r--src/background/actions/operation.js20
-rw-r--r--src/background/components/background.js6
-rw-r--r--src/background/tabs.js32
-rw-r--r--src/console/actions/console.js20
-rw-r--r--src/console/actions/index.js2
-rw-r--r--src/console/components/console.js164
-rw-r--r--src/console/index.html5
-rw-r--r--src/console/index.js4
-rw-r--r--src/console/reducers/index.js32
-rw-r--r--src/console/site.scss13
-rw-r--r--src/content/actions/find.js55
-rw-r--r--src/content/actions/index.js3
-rw-r--r--src/content/actions/operation.js9
-rw-r--r--src/content/actions/setting.js15
-rw-r--r--src/content/components/common/follow.js2
-rw-r--r--src/content/components/common/hint.css2
-rw-r--r--src/content/components/common/input.js21
-rw-r--r--src/content/components/common/keymapper.js25
-rw-r--r--src/content/components/top-content/find.js54
-rw-r--r--src/content/components/top-content/follow-controller.js2
-rw-r--r--src/content/components/top-content/index.js6
-rw-r--r--src/content/console-frame.scss3
-rw-r--r--src/content/console-frames.js10
-rw-r--r--src/content/navigates.js31
-rw-r--r--src/content/reducers/find.js18
-rw-r--r--src/content/reducers/index.js3
-rw-r--r--src/content/reducers/input.js6
-rw-r--r--src/content/reducers/setting.js3
-rw-r--r--src/content/scrolls.js4
-rw-r--r--src/shared/default-settings.js7
-rw-r--r--src/shared/messages.js10
-rw-r--r--src/shared/operations.js11
-rw-r--r--src/shared/utils/keys.js86
-rw-r--r--test/console/actions/console.test.js21
-rw-r--r--test/console/reducers/console.test.js14
-rw-r--r--test/content/actions/setting.test.js23
-rw-r--r--test/content/components/common/input.test.js41
-rw-r--r--test/content/navigates.test.js156
-rw-r--r--test/content/reducers/find.test.js23
-rw-r--r--test/content/reducers/input.test.js12
-rw-r--r--test/content/reducers/setting.test.js2
-rw-r--r--test/shared/utils/keys.test.js145
-rw-r--r--test/shared/utils/re.test.js (renamed from test/shared/util/re.test.js)0
46 files changed, 948 insertions, 287 deletions
diff --git a/QA.md b/QA.md
index 798128e..c1f413d 100644
--- a/QA.md
+++ b/QA.md
@@ -1,10 +1,10 @@
-### Checklist for testing Vim Vixen
+## Checklist for testing Vim Vixen
-#### Operations
+### Operations
Test operations with default key maps.
-##### Scrolling
+#### Scrolling
- [ ] <kbd>k</kbd> or <kbd>Ctrl</kbd>+<kbd>Y</kbd>, <kbd>j</kbd> or <kbd>Ctrl</kbd>+<kbd>E</kbd>: scroll up and down
- [ ] <kbd>h</kbd>, <kbd>l</kbd>: scroll left and right
@@ -13,7 +13,7 @@ Test operations with default key maps.
- [ ] <kbd>0</kbd>, <kbd>$</kbd>: scroll to leftmost and rightmost
- [ ] <kbd>g</kbd><kbd>g</kbd>, <kbd>G</kbd>: scroll to top and bottom
-##### Console
+#### Console
The behaviors of the console are tested in [Console section](#consoles).
@@ -22,32 +22,47 @@ The behaviors of the console are tested in [Console section](#consoles).
- [ ] <kbd>O</kbd>, <kbd>T</kbd>, <kbd>W</kbd>: open a console with `open`, `tabopen`, `winopen` and current URL
- [ ] <kbd>b</kbd>: open a consolw with `buffer`
-##### Tabs
+#### Tabs
- [ ] <kbd>d</kbd>: delete current tab
- [ ] <kbd>u</kbd>: reopen close tab
- [ ] <kbd>K</kbd>, <kbd>J</kbd>: select prev and next tab
+- [ ] <kbd>g0</kbd>, <kbd>g$</kbd>: select first and last tab
- [ ] <kbd>r</kbd>: reload current tab
- [ ] <kbd>R</kbd>: reload current tab without cache
+- [ ] <kbd>zd</kbd>: duplicate current tab
+- [ ] <kbd>zp</kbd>: toggle pin/unpin state on current tab
-##### Navigation
+#### Navigation
-- [ ] <kbd>f</kbd>: start following links
-- [ ] <kbd>F</kbd>: start following links and open in new tab
- [ ] <kbd>H</kbd>, <kbd>L</kbd>: go back and forward in histories
- [ ] <kbd>[</kbd><kbd>[</kbd>, <kbd>]</kbd><kbd>]</kbd>: find prev and next links and open it
- [ ] <kbd>g</kbd><kbd>u</kbd>: go to parent directory
- [ ] <kbd>g</kbd><kbd>U</kbd>: go to root directory
-##### Misc
+#### Misc
- [ ] <kbd>z</kbd><kbd>i</kbd>, <kbd>z</kbd><kbd>o</kbd>: zoom-in and zoom-out
- [ ] <kbd>z</kbd><kbd>z</kbd>: set zoom level as default
- [ ] <kbd>y</kbd>: yank current URL and show a message
+- [ ] Toggle enabled/disabled of plugin bu <kbd>Shift</kbd>+<kbd>Esc</kbd>
-#### Consoles
+### Following links
-##### Exec a command
+- [ ] <kbd>f</kbd>: start following links
+- [ ] <kbd>F</kbd>: start following links and open in new tab
+- [ ] open link with target='_blank' in new tab by <kbd>f</kbd>
+- [ ] open link with target='_blank' in new tab by <kbd>F</kbd>
+- [ ] Show hints on following on a page containing `<frame>`/`<iframe>`
+- [ ] Show hints only inside viewport of the frame on following on a page containing `<frame>`/`<iframe>`
+- [ ] Show hints only inside top window on following on a page containing `<frame>`/`<iframe>`
+- [ ] Select link and open it in the frame in `<iframe>`/`<frame`> on following by <kbd>f</kbd>
+- [ ] Select link and open it in new tab in `<iframe>`/`<frame`> on following by <kbd>F</kbd>
+- [ ] Select link and open it in `<area>` tags, for <kbd>f</kbd> and <kbd>F</kbd>
+
+### Consoles
+
+#### Exec a command
- [ ] `<EMPTY>`, `<SP>`: do nothing
<br>
@@ -70,9 +85,9 @@ The behaviors of the console are tested in [Console section](#consoles).
- [ ] `buffer 0`, `buffer 99`: shows an error
- [ ] select tabs rotationally when more than two tabs are matched
-#### Completions
+### Completions
-##### History and search engines
+#### History and search engines
- [ ] `open`: show no completions
- [ ] `open<SP>`: show all engines and some history items
@@ -80,16 +95,21 @@ The behaviors of the console are tested in [Console section](#consoles).
- [ ] `open foo bar`: complete history items matched with keywords `foo` and `bar`
- [ ] also `tabopen` and `winopen`
- shortening commands such as `o` are not test in this release
+- [ ] Show competions for `:open`/`:tabopen`/`:buffer` on opning just after closed
-##### Buffer command
+#### Buffer command
- [ ] `buffer`: show no completions
- [ ] `buffer<SP>`: show all opened tabs in completion
- [ ] `buffer x`: show tabs which has title and URL matches with `x`
-#### Settings
+#### Misc
+
+- [ ] Select next item by <kbd>Tab</kbd> and previous item by <kbd>Shift</kbd>+<kbd>Tab</kbd>
-##### Validations
+### Settings
+
+#### Validations
- [ ] show error on invalid json
- [ ] show error when top-level keys has keys other than `keymaps`, `search`, and `blacklist`
@@ -102,47 +122,39 @@ The behaviors of the console are tested in [Console section](#consoles).
- validations in `"search"` section are not tested in this release
-##### Updating
+#### `"blacklist"` section
+
+- [ ] `github.com/a` blocks `github.com/a`, and not blocks `github.com/aa`
+- [ ] `github.com/a*` blocks both `github.com/a` and `github.com/aa`
+- [ ] `github.com/` blocks `github.com/`, and not blocks `github.com/a`
+- [ ] `github.com` blocks both `github.com/` and `github.com/a`
+- [ ] `*.github.com` blocks `gist.github.com/`, and not `github.com`
+
+#### Updating
- [ ] changes are updated on textarea blure when no errors
- [ ] changes are not updated on textarea blure when errors occurs
- [ ] keymap settings are applied to open tabs without reload
- [ ] search settings are applied to open tabs without reload
-#### Events are fired on Slack and Twitter (#54)
+### For certain sites
+- [ ] scoll on Hacker News
+- [ ] able to scroll on Gmail and Slack
- [ ] Fucus text box on Twitter or Slack, press <kbd>j</kbd>, then <kbd>j</kbd> is typed in the box
- [ ] Focus the text box on Twitter or Slack on following mode
-#### Multi frame support (#61)
-
-- [ ] Show hints on following on a page containing `<frame>`/`<iframe>`
-- [ ] Show hints only inside viewport of the frame on following on a page containing `<frame>`/`<iframe>`
-- [ ] Show hints only inside top window on following on a page containing `<frame>`/`<iframe>`
-- [ ] Select link and open it in the frame in `<iframe>`/`<frame`> on following by <kbd>f</kbd>
-- [ ] Select link and open it in new tab in `<iframe>`/`<frame`> on following by <kbd>F</kbd>
-
-#### Empty suggestion (#65)
-
-- [ ] Show competions for `:open`/`:tabopen`/`:buffer` on console after closed
+## Find mode
-#### Disable add-on temporary (#86)
+- [ ] open console with <kbd>/</kbd>
+- [ ] highlight a word on <kbd>Enter</kb> pressed in find console
+- [ ] Search next/prev by <kbd>n</kbd>/<kbd>N</kbd>
+- [ ] Wrap search by <kbd>n</kbd>/<kbd>N</kbd>
+- [ ] Find with last keyword if keyword is empty
-- [ ] Toggle enabled/disabled of plugin bu <kbd>Shift</kbd>+<kbd>Esc</kbd>
-
-#### URL blacklist (#90)
+## Misc
-- [ ] `github.com/a` blocks `github.com/a`, and not blocks `github.com/aa`
-- [ ] `github.com/a*` blocks both `github.com/a` and `github.com/aa`
-- [ ] `github.com/` blocks `github.com/`, and not blocks `github.com/a`
-- [ ] `github.com` blocks both `github.com/` and `github.com/a`
-- [ ] `*.github.com` blocks `gist.github.com/`, and not `github.com`
-
-#### Improve for aberration pages (#93)
-
-- [ ] able to scroll on Gmail and Slack
-
-#### Link with target='_blank' link (#94)
-
-- [ ] open link with target='_blank' in new tab by <kbd>f</kbd>
-- [ ] open link with target='_blank' in new tab by <kbd>F</kbd>
+- [ ] Work after plugin reload
+- [ ] Work on `about:blank`
+- [ ] Able to map `<A-Z>` key.
+- [ ] Open file menu by <kbd>Alt</kbd>+<kbd>F</kbd> (Other than Mac OS)
diff --git a/README.md b/README.md
index 8540d47..e8a9835 100644
--- a/README.md
+++ b/README.md
@@ -32,8 +32,11 @@ The default mappings are as follows:
- <kbd>d</kbd>: delete current tab
- <kbd>u</kbd>: reopen close tab
- <kbd>K</kbd>, <kbd>J</kbd>: select prev or next tab
+- <kbd>g0</kbd>, <kbd>g$</kbd>: select first or last tab
- <kbd>r</kbd>: reload current tab
- <kbd>R</kbd>: reload current tab without cache
+- <kbd>zp</kbd>: toggle pin/unpin current tab
+- <kbd>zd</kbd>: duplicate current tab
### Navigation
- <kbd>f</kbd>: start following links in the page
diff --git a/manifest.json b/manifest.json
index 268dae8..5fd1c1a 100644
--- a/manifest.json
+++ b/manifest.json
@@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "Vim Vixen",
"description": "Vim Vixen",
- "version": "0.3",
+ "version": "0.5",
"icons": {
"48": "resources/icon_48x48.png",
"96": "resources/icon_96x96.png"
@@ -17,7 +17,8 @@
"all_frames": true,
"matches": [ "<all_urls>" ],
"js": [ "build/content.js" ],
- "run_at": "document_end"
+ "run_at": "document_end",
+ "match_about_blank": true
}
],
"background": {
diff --git a/src/background/actions/operation.js b/src/background/actions/operation.js
index b94c117..1e4990c 100644
--- a/src/background/actions/operation.js
+++ b/src/background/actions/operation.js
@@ -10,6 +10,9 @@ const sendConsoleShowCommand = (tab, command) => {
});
};
+// This switch statement is only gonna get longer as more
+// features are added, so disable complexity check
+/* eslint-disable complexity */
const exec = (operation, tab) => {
switch (operation.type) {
case operations.TAB_CLOSE:
@@ -20,8 +23,20 @@ const exec = (operation, tab) => {
return tabs.selectPrevTab(tab.index, operation.count);
case operations.TAB_NEXT:
return tabs.selectNextTab(tab.index, operation.count);
+ case operations.TAB_FIRST:
+ return tabs.selectFirstTab();
+ case operations.TAB_LAST:
+ return tabs.selectLastTab();
case operations.TAB_RELOAD:
return tabs.reload(tab, operation.cache);
+ case operations.TAB_PIN:
+ return tabs.updateTabPinned(tab, true);
+ case operations.TAB_UNPIN:
+ return tabs.updateTabPinned(tab, false);
+ case operations.TAB_TOGGLE_PINNED:
+ return tabs.toggleTabPinned(tab);
+ case operations.TAB_DUPLICATE:
+ return tabs.duplicate(tab.id);
case operations.ZOOM_IN:
return zooms.zoomIn();
case operations.ZOOM_OUT:
@@ -50,9 +65,14 @@ const exec = (operation, tab) => {
return sendConsoleShowCommand(tab, 'winopen ');
case operations.COMMAND_SHOW_BUFFER:
return sendConsoleShowCommand(tab, 'buffer ');
+ case operations.FIND_START:
+ return browser.tabs.sendMessage(tab.id, {
+ type: messages.CONSOLE_SHOW_FIND
+ });
default:
return Promise.resolve();
}
};
+/* eslint-enable complexity */
export { exec };
diff --git a/src/background/components/background.js b/src/background/components/background.js
index a5f4f5f..2d94310 100644
--- a/src/background/components/background.js
+++ b/src/background/components/background.js
@@ -34,11 +34,7 @@ export default class BackgroundComponent {
}
return this.store.dispatch(
tabActions.openToTab(message.url, sender.tab), sender);
- case messages.CONSOLE_BLURRED:
- return browser.tabs.sendMessage(sender.tab.id, {
- type: messages.CONSOLE_HIDE_COMMAND,
- });
- case messages.CONSOLE_ENTERED:
+ case messages.CONSOLE_ENTER_COMMAND:
return commands.exec(message.text, settings.value).catch((e) => {
return browser.tabs.sendMessage(sender.tab.id, {
type: messages.CONSOLE_SHOW_ERROR,
diff --git a/src/background/tabs.js b/src/background/tabs.js
index eed3252..d641616 100644
--- a/src/background/tabs.js
+++ b/src/background/tabs.js
@@ -79,6 +79,20 @@ const selectNextTab = (current, count) => {
});
};
+const selectFirstTab = () => {
+ return browser.tabs.query({ currentWindow: true }).then((tabs) => {
+ let id = tabs[0].id;
+ return browser.tabs.update(id, { active: true });
+ });
+};
+
+const selectLastTab = () => {
+ return browser.tabs.query({ currentWindow: true }).then((tabs) => {
+ let id = tabs[tabs.length - 1].id;
+ return browser.tabs.update(id, { active: true });
+ });
+};
+
const reload = (current, cache) => {
return browser.tabs.reload(
current.id,
@@ -86,7 +100,23 @@ const reload = (current, cache) => {
);
};
+const updateTabPinned = (current, pinned) => {
+ return browser.tabs.query({ currentWindow: true, active: true })
+ .then(() => {
+ return browser.tabs.update(current.id, { pinned: pinned });
+ });
+};
+
+const toggleTabPinned = (current) => {
+ updateTabPinned(current, !current.pinned);
+};
+
+const duplicate = (id) => {
+ return browser.tabs.duplicate(id);
+};
+
export {
closeTab, reopenTab, selectAt, selectByKeyword, getCompletions,
- selectPrevTab, selectNextTab, reload
+ selectPrevTab, selectNextTab, selectFirstTab, selectLastTab, reload,
+ updateTabPinned, toggleTabPinned, duplicate
};
diff --git a/src/console/actions/console.js b/src/console/actions/console.js
index 0d891bb..2cf8e8d 100644
--- a/src/console/actions/console.js
+++ b/src/console/actions/console.js
@@ -7,6 +7,12 @@ const showCommand = (text) => {
};
};
+const showFind = () => {
+ return {
+ type: actions.CONSOLE_SHOW_FIND,
+ };
+};
+
const showError = (text) => {
return {
type: actions.CONSOLE_SHOW_ERROR,
@@ -27,10 +33,18 @@ const hideCommand = () => {
};
};
-const setCompletions = (completions) => {
+const setConsoleText = (consoleText) => {
+ return {
+ type: actions.CONSOLE_SET_CONSOLE_TEXT,
+ consoleText,
+ };
+};
+
+const setCompletions = (completionSource, completions) => {
return {
type: actions.CONSOLE_SET_COMPLETIONS,
- completions: completions
+ completionSource,
+ completions,
};
};
@@ -47,6 +61,6 @@ const completionPrev = () => {
};
export {
- showCommand, showError, showInfo, hideCommand,
+ showCommand, showFind, showError, showInfo, hideCommand, setConsoleText,
setCompletions, completionNext, completionPrev
};
diff --git a/src/console/actions/index.js b/src/console/actions/index.js
index c4f88cd..a85e329 100644
--- a/src/console/actions/index.js
+++ b/src/console/actions/index.js
@@ -4,7 +4,9 @@ export default {
CONSOLE_SHOW_ERROR: 'console.show.error',
CONSOLE_SHOW_INFO: 'console.show.info',
CONSOLE_HIDE_COMMAND: 'console.hide.command',
+ CONSOLE_SET_CONSOLE_TEXT: 'console.set.command',
CONSOLE_SET_COMPLETIONS: 'console.set.completions',
CONSOLE_COMPLETION_NEXT: 'console.completion.next',
CONSOLE_COMPLETION_PREV: 'console.completion.prev',
+ CONSOLE_SHOW_FIND: 'console.show.find',
};
diff --git a/src/console/components/console.js b/src/console/components/console.js
index 5028e2a..7bc3364 100644
--- a/src/console/components/console.js
+++ b/src/console/components/console.js
@@ -1,45 +1,44 @@
import messages from 'shared/messages';
import * as consoleActions from 'console/actions/console';
+const inputShownMode = (state) => {
+ return ['command', 'find'].includes(state.mode);
+};
+
export default class ConsoleComponent {
constructor(wrapper, store) {
this.wrapper = wrapper;
- this.prevState = {};
- this.completionOrigin = '';
this.store = store;
+ this.prevMode = '';
let doc = this.wrapper.ownerDocument;
let input = doc.querySelector('#vimvixen-console-command-input');
+
input.addEventListener('blur', this.onBlur.bind(this));
input.addEventListener('keydown', this.onKeyDown.bind(this));
input.addEventListener('input', this.onInput.bind(this));
- this.hideCommand();
- this.hideMessage();
-
store.subscribe(() => {
this.update();
});
+ this.update();
}
onBlur() {
- return browser.runtime.sendMessage({
- type: messages.CONSOLE_BLURRED,
- });
+ let state = this.store.getState();
+ if (state.mode === 'command') {
+ this.hideCommand();
+ }
}
onKeyDown(e) {
- let doc = this.wrapper.ownerDocument;
- let input = doc.querySelector('#vimvixen-console-command-input');
-
switch (e.keyCode) {
case KeyboardEvent.DOM_VK_ESCAPE:
- return input.blur();
+ return this.hideCommand();
case KeyboardEvent.DOM_VK_RETURN:
- return browser.runtime.sendMessage({
- type: messages.CONSOLE_ENTERED,
- text: e.target.value
- }).then(this.onBlur);
+ e.stopPropagation();
+ e.preventDefault();
+ return this.onEntered(e.target.value);
case KeyboardEvent.DOM_VK_TAB:
if (e.shiftKey) {
this.store.dispatch(consoleActions.completionPrev());
@@ -52,94 +51,105 @@ export default class ConsoleComponent {
}
}
+ onEntered(value) {
+ let state = this.store.getState();
+ if (state.mode === 'command') {
+ browser.runtime.sendMessage({
+ type: messages.CONSOLE_ENTER_COMMAND,
+ text: value,
+ });
+ this.hideCommand();
+ } else if (state.mode === 'find') {
+ this.hideCommand();
+ window.top.postMessage(JSON.stringify({
+ type: messages.CONSOLE_ENTER_FIND,
+ text: value,
+ }), '*');
+ }
+ }
+
onInput(e) {
- let doc = this.wrapper.ownerDocument;
- let input = doc.querySelector('#vimvixen-console-command-input');
- this.completionOrigin = input.value;
+ this.store.dispatch(consoleActions.setConsoleText(e.target.value));
+ let source = e.target.value;
return browser.runtime.sendMessage({
type: messages.CONSOLE_QUERY_COMPLETIONS,
- text: e.target.value,
+ text: source,
}).then((completions) => {
- this.store.dispatch(consoleActions.setCompletions(completions));
+ this.store.dispatch(consoleActions.setCompletions(source, completions));
});
}
- update() {
- let state = this.store.getState();
- if (this.prevState.mode !== 'command' && state.mode === 'command') {
- this.showCommand(state.commandText);
- } else if (state.mode !== 'command') {
- this.hideCommand();
- }
+ onInputShown(state) {
+ let doc = this.wrapper.ownerDocument;
+ let input = doc.querySelector('#vimvixen-console-command-input');
- if (state.mode === 'error' || state.mode === 'info') {
- this.showMessage(state.mode, state.messageText);
- } else {
- this.hideMessage();
- }
+ input.focus();
+ window.focus();
- if (state.groupSelection >= 0 && state.itemSelection >= 0) {
- let group = state.completions[state.groupSelection];
- let item = group.items[state.itemSelection];
- this.setCommandValue(item.content);
- } else if (state.completions.length > 0 &&
- JSON.stringify(this.prevState.completions) ===
- JSON.stringify(state.completions)) {
- // Reset input only completion groups not changed (unselected an item in
- // completion) in order to avoid to override previous input
- this.setCommandCompletionOrigin();
+ if (state.mode === 'command') {
+ this.onInput({ target: input });
}
+ }
- this.prevState = state;
+ hideCommand() {
+ this.store.dispatch(consoleActions.hideCommand());
+ window.top.postMessage(JSON.stringify({
+ type: messages.CONSOLE_UNFOCUS,
+ }), '*');
}
- showCommand(text) {
- let doc = this.wrapper.ownerDocument;
- let command = doc.querySelector('#vimvixen-console-command');
- let input = doc.querySelector('#vimvixen-console-command-input');
+ update() {
+ let state = this.store.getState();
- command.style.display = 'block';
- input.value = text;
- input.focus();
+ this.updateMessage(state);
+ this.updateCommand(state);
+ this.updatePrompt(state);
- window.focus();
- this.onInput({ target: input });
+ if (this.prevMode !== state.mode && inputShownMode(state)) {
+ this.onInputShown(state);
+ }
+ this.prevMode = state.mode;
}
- hideCommand() {
+ updateMessage(state) {
let doc = this.wrapper.ownerDocument;
- let command = doc.querySelector('#vimvixen-console-command');
- command.style.display = 'none';
- }
+ let box = doc.querySelector('.vimvixen-console-message');
+ let display = 'none';
+ let classList = ['vimvixen-console-message'];
- setCommandValue(value) {
- let doc = this.wrapper.ownerDocument;
- let input = doc.querySelector('#vimvixen-console-command-input');
- input.value = value;
+ if (state.mode === 'error' || state.mode === 'info') {
+ display = 'block';
+ classList.push('vimvixen-console-' + state.mode);
+ }
+
+ box.className = classList.join(' ');
+ box.style.display = display;
+ box.textContent = state.messageText;
}
- setCommandCompletionOrigin() {
+ updateCommand(state) {
let doc = this.wrapper.ownerDocument;
+ let command = doc.querySelector('#vimvixen-console-command');
let input = doc.querySelector('#vimvixen-console-command-input');
- input.value = this.completionOrigin;
- }
- showMessage(mode, text) {
- let doc = this.wrapper.ownerDocument;
- let error = doc.querySelector('#vimvixen-console-message');
- error.classList.remove(
- 'vimvixen-console-info',
- 'vimvixen-console-error'
- );
- error.classList.add('vimvixen-console-' + mode);
- error.textContent = text;
- error.style.display = 'block';
+ let display = 'none';
+ if (inputShownMode(state)) {
+ display = 'block';
+ }
+
+ command.style.display = display;
+ input.value = state.consoleText;
}
- hideMessage() {
+ updatePrompt(state) {
+ let classList = ['vimvixen-console-command-prompt'];
+ if (inputShownMode(state)) {
+ classList.push('prompt-' + state.mode);
+ }
+
let doc = this.wrapper.ownerDocument;
- let error = doc.querySelector('#vimvixen-console-message');
- error.style.display = 'none';
+ let ele = doc.querySelector('.vimvixen-console-command-prompt');
+ ele.className = classList.join(' ');
}
}
diff --git a/src/console/index.html b/src/console/index.html
index f41a8dc..e049b5e 100644
--- a/src/console/index.html
+++ b/src/console/index.html
@@ -6,9 +6,8 @@
<script src='console.js'></script>
</head>
<body class='vimvixen-console'>
- <p id='vimvixen-console-message'
- class='vimvixen-console-message'></p>
- <div id='vimvixen-console-command'>
+ <p class='vimvixen-console-message'></p>
+ <div id='vimvixen-console-command' class='vimvixen-console-command-wrapper'>
<ul id='vimvixen-console-completion' class='vimvixen-console-completion'></ul>
<div class='vimvixen-console-command'>
<i class='vimvixen-console-command-prompt'></i><input
diff --git a/src/console/index.js b/src/console/index.js
index 36473fe..86edd9a 100644
--- a/src/console/index.js
+++ b/src/console/index.js
@@ -18,12 +18,12 @@ const onMessage = (message) => {
switch (message.type) {
case messages.CONSOLE_SHOW_COMMAND:
return store.dispatch(consoleActions.showCommand(message.command));
+ case messages.CONSOLE_SHOW_FIND:
+ return store.dispatch(consoleActions.showFind());
case messages.CONSOLE_SHOW_ERROR:
return store.dispatch(consoleActions.showError(message.text));
case messages.CONSOLE_SHOW_INFO:
return store.dispatch(consoleActions.showInfo(message.text));
- case messages.CONSOLE_HIDE_COMMAND:
- return store.dispatch(consoleActions.hideCommand());
}
};
diff --git a/src/console/reducers/index.js b/src/console/reducers/index.js
index d4affa7..60c0007 100644
--- a/src/console/reducers/index.js
+++ b/src/console/reducers/index.js
@@ -3,7 +3,8 @@ import actions from 'console/actions';
const defaultState = {
mode: '',
messageText: '',
- commandText: '',
+ consoleText: '',
+ completionSource: '',
completions: [],
groupSelection: -1,
itemSelection: -1,
@@ -43,13 +44,25 @@ const prevSelection = (state) => {
return [state.groupSelection, state.itemSelection - 1];
};
+const nextConsoleText = (completions, group, item, defaults) => {
+ if (group < 0 || item < 0) {
+ return defaults;
+ }
+ return completions[group].items[item].content;
+};
+
export default function reducer(state = defaultState, action = {}) {
switch (action.type) {
case actions.CONSOLE_SHOW_COMMAND:
return Object.assign({}, state, {
mode: 'command',
- commandText: action.text,
- errorShown: false,
+ consoleText: action.text,
+ completions: []
+ });
+ case actions.CONSOLE_SHOW_FIND:
+ return Object.assign({}, state, {
+ mode: 'find',
+ consoleText: '',
completions: []
});
case actions.CONSOLE_SHOW_ERROR:
@@ -64,11 +77,16 @@ export default function reducer(state = defaultState, action = {}) {
});
case actions.CONSOLE_HIDE_COMMAND:
return Object.assign({}, state, {
- mode: state.mode === 'command' ? '' : state.mode,
+ mode: state.mode === 'command' || state.mode === 'find' ? '' : state.mode,
+ });
+ case actions.CONSOLE_SET_CONSOLE_TEXT:
+ return Object.assign({}, state, {
+ consoleText: action.consoleText,
});
case actions.CONSOLE_SET_COMPLETIONS:
return Object.assign({}, state, {
completions: action.completions,
+ completionSource: action.completionSource,
groupSelection: -1,
itemSelection: -1,
});
@@ -77,6 +95,9 @@ export default function reducer(state = defaultState, action = {}) {
return Object.assign({}, state, {
groupSelection: next[0],
itemSelection: next[1],
+ consoleText: nextConsoleText(
+ state.completions, next[0], next[1],
+ state.completionSource),
});
}
case actions.CONSOLE_COMPLETION_PREV: {
@@ -84,6 +105,9 @@ export default function reducer(state = defaultState, action = {}) {
return Object.assign({}, state, {
groupSelection: next[0],
itemSelection: next[1],
+ consoleText: nextConsoleText(
+ state.completions, next[0], next[1],
+ state.completionSource),
});
}
default:
diff --git a/src/console/site.scss b/src/console/site.scss
index e5cb2df..5aaea12 100644
--- a/src/console/site.scss
+++ b/src/console/site.scss
@@ -12,7 +12,6 @@ body {
}
.vimvixen-console {
- border-top: 1px solid gray;
bottom: 0;
margin: 0;
padding: 0;
@@ -24,6 +23,10 @@ body {
line-height: 16px;
}
+ &-command-wrapper {
+ border-top: 1px solid gray;
+ }
+
&-completion {
background-color: white;
@@ -85,9 +88,15 @@ body {
display: flex;
&-prompt:before {
+ @include consoole-font;
+ }
+
+ &-prompt.prompt-command:before {
content: ':';
+ }
- @include consoole-font;
+ &-prompt.prompt-find:before {
+ content: '/';
}
&-input {
diff --git a/src/content/actions/find.js b/src/content/actions/find.js
new file mode 100644
index 0000000..80d6210
--- /dev/null
+++ b/src/content/actions/find.js
@@ -0,0 +1,55 @@
+//
+// window.find(aString, aCaseSensitive, aBackwards, aWrapAround,
+// aWholeWord, aSearchInFrames);
+//
+// NOTE: window.find is not standard API
+// https://developer.mozilla.org/en-US/docs/Web/API/Window/find
+
+import actions from 'content/actions';
+import * as consoleFrames from '../console-frames';
+
+const postPatternNotFound = (pattern) => {
+ return consoleFrames.postError(
+ window.document,
+ 'Pattern not found: ' + pattern);
+};
+
+const find = (string, backwards) => {
+ let caseSensitive = false;
+ let wrapScan = true;
+
+
+ // NOTE: aWholeWord dows not implemented, and aSearchInFrames does not work
+ // because of same origin policy
+ return window.find(string, caseSensitive, backwards, wrapScan);
+};
+
+const findNext = (keyword, reset, backwards) => {
+ if (reset) {
+ window.getSelection().removeAllRanges();
+ }
+
+ let found = find(keyword, backwards);
+ if (!found) {
+ window.getSelection().removeAllRanges();
+ found = find(keyword, backwards);
+ }
+ if (!found) {
+ postPatternNotFound(keyword);
+ }
+ return {
+ type: actions.FIND_SET_KEYWORD,
+ keyword,
+ found,
+ };
+};
+
+const next = (keyword, reset) => {
+ return findNext(keyword, reset, false);
+};
+
+const prev = (keyword, reset) => {
+ return findNext(keyword, reset, true);
+};
+
+export { next, prev };
diff --git a/src/content/actions/index.js b/src/content/actions/index.js
index 8cc2303..7e32e12 100644
--- a/src/content/actions/index.js
+++ b/src/content/actions/index.js
@@ -21,4 +21,7 @@ export default {
FOLLOW_CONTROLLER_DISABLE: 'follow.controller.disable',
FOLLOW_CONTROLLER_KEY_PRESS: 'follow.controller.key.press',
FOLLOW_CONTROLLER_BACKSPACE: 'follow.controller.backspace',
+
+ // Find
+ FIND_SET_KEYWORD: 'find.set.keyword',
};
diff --git a/src/content/actions/operation.js b/src/content/actions/operation.js
index 897f361..767f14b 100644
--- a/src/content/actions/operation.js
+++ b/src/content/actions/operation.js
@@ -6,6 +6,7 @@ import * as urls from 'content/urls';
import * as consoleFrames from 'content/console-frames';
import * as addonActions from './addon';
+// eslint-disable-next-line complexity
const exec = (operation) => {
switch (operation.type) {
case operations.ADDON_ENABLE:
@@ -14,6 +15,14 @@ const exec = (operation) => {
return addonActions.disable();
case operations.ADDON_TOGGLE_ENABLED:
return addonActions.toggleEnabled();
+ case operations.FIND_NEXT:
+ return window.top.postMessage(JSON.stringify({
+ type: messages.FIND_NEXT,
+ }), '*');
+ case operations.FIND_PREV:
+ return window.top.postMessage(JSON.stringify({
+ type: messages.FIND_PREV,
+ }), '*');
case operations.SCROLL_VERTICALLY:
return scrolls.scrollVertically(window, operation.count);
case operations.SCROLL_HORIZONALLY:
diff --git a/src/content/actions/setting.js b/src/content/actions/setting.js
index c874294..0238c71 100644
--- a/src/content/actions/setting.js
+++ b/src/content/actions/setting.js
@@ -1,9 +1,22 @@
import actions from 'content/actions';
+import * as keyUtils from 'shared/utils/keys';
const set = (value) => {
+ let entries = [];
+ if (value.keymaps) {
+ entries = Object.entries(value.keymaps).map((entry) => {
+ return [
+ keyUtils.fromMapKeys(entry[0]),
+ entry[1],
+ ];
+ });
+ }
+
return {
type: actions.SETTING_SET,
- value,
+ value: Object.assign({}, value, {
+ keymaps: entries,
+ })
};
};
diff --git a/src/content/components/common/follow.js b/src/content/components/common/follow.js
index 15b2a98..7717154 100644
--- a/src/content/components/common/follow.js
+++ b/src/content/components/common/follow.js
@@ -47,7 +47,7 @@ export default class Follow {
}
this.win.parent.postMessage(JSON.stringify({
type: messages.FOLLOW_KEY_PRESS,
- key,
+ key: key.key,
}), '*');
return true;
}
diff --git a/src/content/components/common/hint.css b/src/content/components/common/hint.css
index 119dd21..1f2ab20 100644
--- a/src/content/components/common/hint.css
+++ b/src/content/components/common/hint.css
@@ -4,7 +4,7 @@
font-weight: bold;
position: absolute;
text-transform: uppercase;
- z-index: 100000;
+ z-index: 2147483647;
font-size: 12px;
color: black;
}
diff --git a/src/content/components/common/input.js b/src/content/components/common/input.js
index 8b1d35d..22b0a91 100644
--- a/src/content/components/common/input.js
+++ b/src/content/components/common/input.js
@@ -1,22 +1,5 @@
import * as dom from 'shared/utils/dom';
-
-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;
-};
+import * as keys from 'shared/utils/keys';
export default class InputComponent {
constructor(target) {
@@ -64,7 +47,7 @@ export default class InputComponent {
return;
}
- let key = mapKey(e);
+ let key = keys.fromKeyboardEvent(e);
for (let listener of this.onKeyListeners) {
let stop = listener(key);
diff --git a/src/content/components/common/keymapper.js b/src/content/components/common/keymapper.js
index 1da3c0d..fb8fabe 100644
--- a/src/content/components/common/keymapper.js
+++ b/src/content/components/common/keymapper.js
@@ -1,6 +1,19 @@
import * as inputActions from 'content/actions/input';
import * as operationActions from 'content/actions/operation';
import operations from 'shared/operations';
+import * as keyUtils from 'shared/utils/keys';
+
+const mapStartsWith = (mapping, keys) => {
+ if (mapping.length < keys.length) {
+ return false;
+ }
+ for (let i = 0; i < keys.length; ++i) {
+ if (!keyUtils.equals(mapping[i], keys[i])) {
+ return false;
+ }
+ }
+ return true;
+};
export default class KeymapperComponent {
constructor(store) {
@@ -12,16 +25,16 @@ export default class KeymapperComponent {
let state = this.store.getState();
let input = state.input;
- let keymaps = state.setting.keymaps;
+ let keymaps = new Map(state.setting.keymaps);
- let matched = Object.keys(keymaps).filter((keyStr) => {
- return keyStr.startsWith(input.keys);
+ let matched = Array.from(keymaps.keys()).filter((mapping) => {
+ return mapStartsWith(mapping, input.keys);
});
if (!state.addon.enabled) {
// available keymaps are only ADDON_ENABLE and ADDON_TOGGLE_ENABLED if
// the addon disabled
matched = matched.filter((keys) => {
- let type = keymaps[keys].type;
+ let type = keymaps.get(keys).type;
return type === operations.ADDON_ENABLE ||
type === operations.ADDON_TOGGLE_ENABLED;
});
@@ -30,10 +43,10 @@ export default class KeymapperComponent {
this.store.dispatch(inputActions.clearKeys());
return false;
} else if (matched.length > 1 ||
- matched.length === 1 && input.keys !== matched[0]) {
+ matched.length === 1 && input.keys.length < matched[0].length) {
return true;
}
- let operation = keymaps[matched];
+ let operation = keymaps.get(matched[0]);
this.store.dispatch(operationActions.exec(operation));
this.store.dispatch(inputActions.clearKeys());
return true;
diff --git a/src/content/components/top-content/find.js b/src/content/components/top-content/find.js
new file mode 100644
index 0000000..bccf040
--- /dev/null
+++ b/src/content/components/top-content/find.js
@@ -0,0 +1,54 @@
+import * as findActions from 'content/actions/find';
+import messages from 'shared/messages';
+import * as consoleFrames from '../../console-frames';
+
+export default class FindComponent {
+ constructor(win, store) {
+ this.win = win;
+ this.store = store;
+
+ messages.onMessage(this.onMessage.bind(this));
+ }
+
+ onMessage(message) {
+ switch (message.type) {
+ case messages.CONSOLE_ENTER_FIND:
+ return this.start(message.text);
+ case messages.FIND_NEXT:
+ return this.next();
+ case messages.FIND_PREV:
+ return this.prev();
+ }
+ }
+
+ start(text) {
+ let state = this.store.getState().find;
+
+ if (text.length === 0) {
+ return this.store.dispatch(findActions.next(state.keyword, true));
+ }
+ return this.store.dispatch(findActions.next(text, true));
+ }
+
+ next() {
+ let state = this.store.getState().find;
+
+ if (!state.found) {
+ return consoleFrames.postError(
+ window.document,
+ 'Pattern not found: ' + state.keyword);
+ }
+ return this.store.dispatch(findActions.next(state.keyword, false));
+ }
+
+ prev() {
+ let state = this.store.getState().find;
+
+ if (!state.found) {
+ return consoleFrames.postError(
+ window.document,
+ 'Pattern not found: ' + state.keyword);
+ }
+ return this.store.dispatch(findActions.prev(state.keyword, false));
+ }
+}
diff --git a/src/content/components/top-content/follow-controller.js b/src/content/components/top-content/follow-controller.js
index 38869e6..d373177 100644
--- a/src/content/components/top-content/follow-controller.js
+++ b/src/content/components/top-content/follow-controller.js
@@ -76,7 +76,7 @@ export default class FollowController {
this.activate();
this.store.dispatch(followControllerActions.disable());
break;
- case 'Escape':
+ case 'Esc':
this.store.dispatch(followControllerActions.disable());
break;
case 'Backspace':
diff --git a/src/content/components/top-content/index.js b/src/content/components/top-content/index.js
index f6afbfa..cf21ec4 100644
--- a/src/content/components/top-content/index.js
+++ b/src/content/components/top-content/index.js
@@ -1,5 +1,6 @@
import CommonComponent from '../common';
import FollowController from './follow-controller';
+import FindComponent from './find';
import * as consoleFrames from '../../console-frames';
import * as addonActions from '../../actions/addon';
import messages from 'shared/messages';
@@ -14,11 +15,14 @@ export default class TopContent {
new CommonComponent(win, store); // eslint-disable-line no-new
new FollowController(win, store); // eslint-disable-line no-new
+ new FindComponent(win, store); // eslint-disable-line no-new
// TODO make component
consoleFrames.initialize(this.win.document);
messages.onMessage(this.onMessage.bind(this));
+
+ this.store.subscribe(() => this.update());
}
update() {
@@ -45,7 +49,7 @@ export default class TopContent {
onMessage(message) {
switch (message.type) {
- case messages.CONSOLE_HIDE_COMMAND:
+ case messages.CONSOLE_UNFOCUS:
this.win.focus();
consoleFrames.blur(window.document);
return Promise.resolve();
diff --git a/src/content/console-frame.scss b/src/content/console-frame.scss
index 33bfff3..dece648 100644
--- a/src/content/console-frame.scss
+++ b/src/content/console-frame.scss
@@ -6,7 +6,8 @@
width: 100%;
height: 100%;
position: fixed;
- z-index: 10000;
+ z-index: 2147483647;
border: none;
+ background-color: unset;
pointer-events:none;
}
diff --git a/src/content/console-frames.js b/src/content/console-frames.js
index 35b975f..515ae09 100644
--- a/src/content/console-frames.js
+++ b/src/content/console-frames.js
@@ -1,4 +1,5 @@
import './console-frame.scss';
+import messages from 'shared/messages';
const initialize = (doc) => {
let iframe = doc.createElement('iframe');
@@ -20,4 +21,11 @@ const postMessage = (doc, message) => {
iframe.contentWindow.postMessage(JSON.stringify(message), '*');
};
-export { initialize, blur, postMessage };
+const postError = (doc, message) => {
+ return postMessage(doc, {
+ type: messages.CONSOLE_SHOW_ERROR,
+ text: message,
+ });
+};
+
+export { initialize, blur, postMessage, postError };
diff --git a/src/content/navigates.js b/src/content/navigates.js
index 64e5fc0..3e12a6f 100644
--- a/src/content/navigates.js
+++ b/src/content/navigates.js
@@ -2,13 +2,14 @@ const PREV_LINK_PATTERNS = [
/\bprev\b/i, /\bprevious\b/i, /\bback\b/i,
/</, /\u2039/, /\u2190/, /\xab/, /\u226a/, /<</
];
+
const NEXT_LINK_PATTERNS = [
/\bnext\b/i,
/>/, /\u203a/, /\u2192/, /\xbb/, /\u226b/, />>/
];
const findLinkByPatterns = (win, patterns) => {
- let links = win.document.getElementsByTagName('a');
+ const links = win.document.getElementsByTagName('a');
return Array.prototype.find.call(links, (link) => {
return patterns.some(ptn => ptn.test(link.textContent));
});
@@ -22,30 +23,32 @@ const historyNext = (win) => {
win.history.forward();
};
-const linkPrev = (win) => {
- let link = win.document.querySelector('a[rel=prev]');
+const linkCommon = (win, rel, patterns) => {
+ let link = win.document.querySelector(`link[rel~=${rel}][href]`);
+
if (link) {
- return link.click();
+ win.location = link.getAttribute('href');
+ return;
}
- link = findLinkByPatterns(win, PREV_LINK_PATTERNS);
+
+ link = win.document.querySelector(`a[rel~=${rel}]`) ||
+ findLinkByPatterns(win, patterns);
+
if (link) {
link.click();
}
};
+const linkPrev = (win) => {
+ linkCommon(win, 'prev', PREV_LINK_PATTERNS);
+};
+
const linkNext = (win) => {
- let link = win.document.querySelector('a[rel=next]');
- if (link) {
- return link.click();
- }
- link = findLinkByPatterns(win, NEXT_LINK_PATTERNS);
- if (link) {
- link.click();
- }
+ linkCommon(win, 'next', NEXT_LINK_PATTERNS);
};
const parent = (win) => {
- let loc = win.location;
+ const loc = win.location;
if (loc.hash !== '') {
loc.hash = '';
return;
diff --git a/src/content/reducers/find.js b/src/content/reducers/find.js
new file mode 100644
index 0000000..eb43c37
--- /dev/null
+++ b/src/content/reducers/find.js
@@ -0,0 +1,18 @@
+import actions from 'content/actions';
+
+const defaultState = {
+ keyword: '',
+ found: false,
+};
+
+export default function reducer(state = defaultState, action = {}) {
+ switch (action.type) {
+ case actions.FIND_SET_KEYWORD:
+ return Object.assign({}, state, {
+ keyword: action.keyword,
+ found: action.found,
+ });
+ default:
+ return state;
+ }
+}
diff --git a/src/content/reducers/index.js b/src/content/reducers/index.js
index 17c0429..2487d85 100644
--- a/src/content/reducers/index.js
+++ b/src/content/reducers/index.js
@@ -1,4 +1,5 @@
import addonReducer from './addon';
+import findReducer from './find';
import settingReducer from './setting';
import inputReducer from './input';
import followControllerReducer from './follow-controller';
@@ -6,6 +7,7 @@ import followControllerReducer from './follow-controller';
// Make setting reducer instead of re-use
const defaultState = {
addon: addonReducer(undefined, {}),
+ find: findReducer(undefined, {}),
setting: settingReducer(undefined, {}),
input: inputReducer(undefined, {}),
followController: followControllerReducer(undefined, {}),
@@ -14,6 +16,7 @@ const defaultState = {
export default function reducer(state = defaultState, action = {}) {
return Object.assign({}, state, {
addon: addonReducer(state.addon, action),
+ find: findReducer(state.find, action),
setting: settingReducer(state.setting, action),
input: inputReducer(state.input, action),
followController: followControllerReducer(state.followController, action),
diff --git a/src/content/reducers/input.js b/src/content/reducers/input.js
index 9457604..134aa95 100644
--- a/src/content/reducers/input.js
+++ b/src/content/reducers/input.js
@@ -1,18 +1,18 @@
import actions from 'content/actions';
const defaultState = {
- keys: ''
+ keys: []
};
export default function reducer(state = defaultState, action = {}) {
switch (action.type) {
case actions.INPUT_KEY_PRESS:
return Object.assign({}, state, {
- keys: state.keys + action.key
+ keys: state.keys.concat([action.key]),
});
case actions.INPUT_CLEAR_KEYS:
return Object.assign({}, state, {
- keys: '',
+ keys: [],
});
default:
return state;
diff --git a/src/content/reducers/setting.js b/src/content/reducers/setting.js
index b6f6c58..a23027f 100644
--- a/src/content/reducers/setting.js
+++ b/src/content/reducers/setting.js
@@ -1,7 +1,8 @@
import actions from 'content/actions';
const defaultState = {
- keymaps: {},
+ // keymaps is and arrays of key-binding pairs, which is entries of Map
+ keymaps: [],
};
export default function reducer(state = defaultState, action = {}) {
diff --git a/src/content/scrolls.js b/src/content/scrolls.js
index d88320f..ef38273 100644
--- a/src/content/scrolls.js
+++ b/src/content/scrolls.js
@@ -104,14 +104,14 @@ const scrollBottom = (win) => {
const scrollHome = (win) => {
let target = scrollTarget(win);
let x = 0;
- let y = target.scrollLeft;
+ let y = target.scrollTop;
target.scrollTo(x, y);
};
const scrollEnd = (win) => {
let target = scrollTarget(win);
let x = target.scrollWidth;
- let y = target.scrollLeft;
+ let y = target.scrollTop;
target.scrollTo(x, y);
};
diff --git a/src/shared/default-settings.js b/src/shared/default-settings.js
index 60e7876..608890b 100644
--- a/src/shared/default-settings.js
+++ b/src/shared/default-settings.js
@@ -28,8 +28,12 @@ export default {
"u": { "type": "tabs.reopen" },
"K": { "type": "tabs.prev", "count": 1 },
"J": { "type": "tabs.next", "count": 1 },
+ "g0": { "type": "tabs.first" },
+ "g$": { "type": "tabs.last" },
"r": { "type": "tabs.reload", "cache": false },
"R": { "type": "tabs.reload", "cache": true },
+ "zp": { "type": "tabs.pin.toggle" },
+ "zd": { "type": "tabs.duplicate" },
"zi": { "type": "zoom.in" },
"zo": { "type": "zoom.out" },
"zz": { "type": "zoom.neutral" },
@@ -42,6 +46,9 @@ export default {
"gu": { "type": "navigate.parent" },
"gU": { "type": "navigate.root" },
"y": { "type": "urls.yank" },
+ "/": { "type": "find.start" },
+ "n": { "type": "find.next" },
+ "N": { "type": "find.prev" },
"<S-Esc>": { "type": "addon.toggle.enabled" }
},
"search": {
diff --git a/src/shared/messages.js b/src/shared/messages.js
index dc497b6..de00a3f 100644
--- a/src/shared/messages.js
+++ b/src/shared/messages.js
@@ -24,13 +24,14 @@ const onMessage = (listener) => {
export default {
BACKGROUND_OPERATION: 'background.operation',
- CONSOLE_BLURRED: 'console.blured',
- CONSOLE_ENTERED: 'console.entered',
+ CONSOLE_UNFOCUS: 'console.unfocus',
+ CONSOLE_ENTER_COMMAND: 'console.enter.command',
+ CONSOLE_ENTER_FIND: 'console.enter.find',
CONSOLE_QUERY_COMPLETIONS: 'console.query.completions',
CONSOLE_SHOW_COMMAND: 'console.show.command',
CONSOLE_SHOW_ERROR: 'console.show.error',
CONSOLE_SHOW_INFO: 'console.show.info',
- CONSOLE_HIDE_COMMAND: 'console.hide.command',
+ CONSOLE_SHOW_FIND: 'console.show.find',
FOLLOW_START: 'follow.start',
FOLLOW_REQUEST_COUNT_TARGETS: 'follow.request.count.targets',
@@ -41,6 +42,9 @@ export default {
FOLLOW_ACTIVATE: 'follow.activate',
FOLLOW_KEY_PRESS: 'follow.key.press',
+ FIND_NEXT: 'find.next',
+ FIND_PREV: 'find.prev',
+
OPEN_URL: 'open.url',
SETTINGS_RELOAD: 'settings.reload',
diff --git a/src/shared/operations.js b/src/shared/operations.js
index d5c2985..4c221ba 100644
--- a/src/shared/operations.js
+++ b/src/shared/operations.js
@@ -36,7 +36,13 @@ export default {
TAB_REOPEN: 'tabs.reopen',
TAB_PREV: 'tabs.prev',
TAB_NEXT: 'tabs.next',
+ TAB_FIRST: 'tabs.first',
+ TAB_LAST: 'tabs.last',
TAB_RELOAD: 'tabs.reload',
+ TAB_PIN: 'tabs.pin',
+ TAB_UNPIN: 'tabs.unpin',
+ TAB_TOGGLE_PINNED: 'tabs.pin.toggle',
+ TAB_DUPLICATE: 'tabs.duplicate',
// Zooms
ZOOM_IN: 'zoom.in',
@@ -45,4 +51,9 @@ export default {
// Url yank
URLS_YANK: 'urls.yank',
+
+ // Find
+ FIND_START: 'find.start',
+ FIND_NEXT: 'find.next',
+ FIND_PREV: 'find.prev',
};
diff --git a/src/shared/utils/keys.js b/src/shared/utils/keys.js
new file mode 100644
index 0000000..fba8ce3
--- /dev/null
+++ b/src/shared/utils/keys.js
@@ -0,0 +1,86 @@
+const modifierdKeyName = (name) => {
+ if (name.length === 1) {
+ return name;
+ } else if (name === 'Escape') {
+ return 'Esc';
+ }
+ return name;
+};
+
+const fromKeyboardEvent = (e) => {
+ let key = modifierdKeyName(e.key);
+ let shift = e.shiftKey;
+ if (key.length === 1 && key.toUpperCase() === key.toLowerCase()) {
+ // make shift false for symbols to enable key bindings by symbold keys.
+ // But this limits key bindings by symbol keys with Shift (such as Shift+$>.
+ shift = false;
+ }
+
+ return {
+ key: modifierdKeyName(e.key),
+ shiftKey: shift,
+ ctrlKey: e.ctrlKey,
+ altKey: e.altKey,
+ metaKey: e.metaKey,
+ };
+};
+
+const fromMapKey = (key) => {
+ if (key.startsWith('<') && key.endsWith('>')) {
+ let inner = key.slice(1, -1);
+ let shift = inner.includes('S-');
+ let base = inner.slice(inner.lastIndexOf('-') + 1);
+ if (shift && base.length === 1) {
+ base = base.toUpperCase();
+ } else if (!shift && base.length === 1) {
+ base = base.toLowerCase();
+ }
+ return {
+ key: base,
+ shiftKey: inner.includes('S-'),
+ ctrlKey: inner.includes('C-'),
+ altKey: inner.includes('A-'),
+ metaKey: inner.includes('M-'),
+ };
+ }
+ return {
+ key: key,
+ shiftKey: key.toLowerCase() !== key,
+ ctrlKey: false,
+ altKey: false,
+ metaKey: false,
+ };
+};
+
+const fromMapKeys = (keys) => {
+ const fromMapKeysRecursive = (remainings, mappedKeys) => {
+ if (remainings.length === 0) {
+ return mappedKeys;
+ }
+
+ let nextPos = 1;
+ if (remainings.startsWith('<')) {
+ let ltPos = remainings.indexOf('>');
+ if (ltPos > 0) {
+ nextPos = ltPos + 1;
+ }
+ }
+
+ return fromMapKeysRecursive(
+ remainings.slice(nextPos),
+ mappedKeys.concat([fromMapKey(remainings.slice(0, nextPos))])
+ );
+ };
+
+ return fromMapKeysRecursive(keys, []);
+};
+
+const equals = (e1, e2) => {
+ return e1.key === e2.key &&
+ e1.ctrlKey === e2.ctrlKey &&
+ e1.metaKey === e2.metaKey &&
+ e1.altKey === e2.altKey &&
+ e1.shiftKey === e2.shiftKey;
+};
+
+export { fromKeyboardEvent, fromMapKey, fromMapKeys, equals };
diff --git a/test/console/actions/console.test.js b/test/console/actions/console.test.js
index 3b02d4a..9af13d4 100644
--- a/test/console/actions/console.test.js
+++ b/test/console/actions/console.test.js
@@ -11,6 +11,13 @@ describe("console actions", () => {
});
});
+ describe("showFind", () => {
+ it('create CONSOLE_SHOW_FIND action', () => {
+ let action = consoleActions.showFind();
+ expect(action.type).to.equal(actions.CONSOLE_SHOW_FIND);
+ });
+ });
+
describe("showInfo", () => {
it('create CONSOLE_SHOW_INFO action', () => {
let action = consoleActions.showInfo('an info');
@@ -27,17 +34,26 @@ describe("console actions", () => {
});
});
- describe("hide", () => {
+ describe("hideCommand", () => {
it('create CONSOLE_HIDE_COMMAND action', () => {
let action = consoleActions.hideCommand();
expect(action.type).to.equal(actions.CONSOLE_HIDE_COMMAND);
});
});
+ describe('setConsoleText', () => {
+ it('create CONSOLE_SET_CONSOLE_TEXT action', () => {
+ let action = consoleActions.setConsoleText('hello world');
+ expect(action.type).to.equal(actions.CONSOLE_SET_CONSOLE_TEXT);
+ expect(action.consoleText).to.equal('hello world');
+ });
+ });
+
describe("setCompletions", () => {
it('create CONSOLE_SET_COMPLETIONS action', () => {
- let action = consoleActions.setCompletions([1,2,3]);
+ let action = consoleActions.setCompletions('query', [1, 2, 3]);
expect(action.type).to.equal(actions.CONSOLE_SET_COMPLETIONS);
+ expect(action.completionSource).to.deep.equal('query');
expect(action.completions).to.deep.equal([1, 2, 3]);
});
});
@@ -56,4 +72,3 @@ describe("console actions", () => {
});
});
});
-
diff --git a/test/console/reducers/console.test.js b/test/console/reducers/console.test.js
index 4f85e55..438d513 100644
--- a/test/console/reducers/console.test.js
+++ b/test/console/reducers/console.test.js
@@ -7,7 +7,7 @@ describe("console reducer", () => {
let state = reducer(undefined, {});
expect(state).to.have.property('mode', '');
expect(state).to.have.property('messageText', '');
- expect(state).to.have.property('commandText', '');
+ expect(state).to.have.property('consoleText', '');
expect(state).to.have.deep.property('completions', []);
expect(state).to.have.property('groupSelection', -1);
expect(state).to.have.property('itemSelection', -1);
@@ -17,7 +17,7 @@ describe("console reducer", () => {
let action = { type: actions.CONSOLE_SHOW_COMMAND, text: 'open ' };
let state = reducer({}, action);
expect(state).to.have.property('mode', 'command');
- expect(state).to.have.property('commandText', 'open ');
+ expect(state).to.have.property('consoleText', 'open ');
});
it('return next state for CONSOLE_SHOW_INFO', () => {
@@ -43,6 +43,16 @@ describe("console reducer", () => {
expect(state).to.have.property('mode', 'error');
});
+ it('return next state for CONSOLE_SET_CONSOLE_TEXT', () => {
+ let action = {
+ type: actions.CONSOLE_SET_CONSOLE_TEXT,
+ consoleText: 'hello world'
+ }
+ let state = reducer({}, action)
+
+ expect(state).to.have.property('consoleText', 'hello world');
+ });
+
it ('return next state for CONSOLE_SET_COMPLETIONS', () => {
let state = {
groupSelection: 0,
diff --git a/test/content/actions/setting.test.js b/test/content/actions/setting.test.js
index 8855f04..1248edf 100644
--- a/test/content/actions/setting.test.js
+++ b/test/content/actions/setting.test.js
@@ -7,7 +7,28 @@ describe("setting actions", () => {
it('create SETTING_SET action', () => {
let action = settingActions.set({ red: 'apple', yellow: 'banana' });
expect(action.type).to.equal(actions.SETTING_SET);
- expect(action.value).to.deep.equal({ red: 'apple', yellow: 'banana' });
+ expect(action.value.red).to.equal('apple');
+ expect(action.value.yellow).to.equal('banana');
+ expect(action.value.keymaps).to.be.empty;
+ });
+
+ it('converts keymaps', () => {
+ let action = settingActions.set({
+ keymaps: {
+ 'dd': 'remove current tab',
+ 'z<C-A>': 'increment',
+ }
+ });
+ let keymaps = action.value.keymaps;
+ let map = new Map(keymaps);
+ expect(map).to.have.deep.all.keys(
+ [
+ [{ key: 'd', shiftKey: false, ctrlKey: false, altKey: false, metaKey: false },
+ { key: 'd', shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }],
+ [{ key: 'z', shiftKey: false, ctrlKey: false, altKey: false, metaKey: false },
+ { key: 'a', shiftKey: false, ctrlKey: true, altKey: false, metaKey: false }],
+ ]
+ );
});
});
});
diff --git a/test/content/components/common/input.test.js b/test/content/components/common/input.test.js
index 912ac34..a346cf6 100644
--- a/test/content/components/common/input.test.js
+++ b/test/content/components/common/input.test.js
@@ -4,20 +4,21 @@ import { expect } from "chai";
describe('InputComponent', () => {
it('register callbacks', () => {
let component = new InputComponent(window.document);
+ let key = { key: 'a', ctrlKey: true, shiftKey: false, altKey: false, metaKey: false };
component.onKey((key) => {
- expect(key).is.equals('a');
+ expect(key).to.deep.equal(key);
});
- component.onKeyDown({ key: 'a' });
+ component.onKeyDown(key);
});
it('invoke callback once', () => {
let component = new InputComponent(window.document);
let a = 0, b = 0;
component.onKey((key) => {
- if (key == 'a') {
+ if (key.key == 'a') {
++a;
} else {
- key == 'b'
+ key.key == 'b'
++b;
}
});
@@ -32,38 +33,6 @@ describe('InputComponent', () => {
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) => {
diff --git a/test/content/navigates.test.js b/test/content/navigates.test.js
index b5144e9..d8a3316 100644
--- a/test/content/navigates.test.js
+++ b/test/content/navigates.test.js
@@ -1,56 +1,138 @@
-import { expect } from "chai";
+import { expect } from 'chai';
import * as navigates from 'content/navigates';
+const testRel = (done, rel, html) => {
+ const method = rel === 'prev' ? 'linkPrev' : 'linkNext';
+ document.body.innerHTML = html;
+ navigates[method](window);
+ setTimeout(() => {
+ expect(document.location.hash).to.equal(`#${rel}`);
+ done();
+ }, 0);
+};
+
+const testPrev = html => done => testRel(done, 'prev', html);
+const testNext = html => done => testRel(done, 'next', html);
+
describe('navigates module', () => {
describe('#linkPrev', () => {
- it('clicks prev link by text content', (done) => {
- document.body.innerHTML = '<a href="#dummy">xprevx</a> <a href="#prev">go to prev</a>';
- navigates.linkPrev(window);
- setTimeout(() => {
- expect(document.location.hash).to.equal('#prev');
- done();
- }, 0);
- });
+ it('navigates to <link> elements whose rel attribute is "prev"', testPrev(
+ '<link rel="prev" href="#prev" />'
+ ));
- it('clicks a[rel=prev] element preferentially', (done) => {
- document.body.innerHTML = '<a href="#dummy">prev</a> <a rel="prev" href="#prev">rel</a>';
- navigates.linkPrev(window);
- setTimeout(() => {
- expect(document.location.hash).to.equal('#prev');
- done();
- }, 0);
- });
- });
+ it('navigates to <link> elements whose rel attribute starts with "prev"', testPrev(
+ '<link rel="prev bar" href="#prev" />'
+ ));
+
+ it('navigates to <link> elements whose rel attribute ends with "prev"', testPrev(
+ '<link rel="foo prev" href="#prev" />'
+ ));
+
+ it('navigates to <link> elements whose rel attribute contains "prev"', testPrev(
+ '<link rel="foo prev bar" href="#prev" />'
+ ));
+
+ it('navigates to <a> elements whose rel attribute is "prev"', testPrev(
+ '<a rel="prev" href="#prev">click me</a>'
+ ));
+
+ it('navigates to <a> elements whose rel attribute starts with "prev"', testPrev(
+ '<a rel="prev bar" href="#prev">click me</a>'
+ ));
+
+ it('navigates to <a> elements whose rel attribute ends with "prev"', testPrev(
+ '<a rel="foo prev" href="#prev">click me</a>'
+ ));
+
+ it('navigates to <a> elements whose rel attribute contains "prev"', testPrev(
+ '<a rel="foo prev bar" href="#prev">click me</a>'
+ ));
+
+ it('navigates to <a> elements whose text matches "prev"', testPrev(
+ '<a href="#dummy">preview</a><a href="#prev">go to prev</a>'
+ ));
+
+ it('navigates to <a> elements whose text matches "previous"', testPrev(
+ '<a href="#dummy">preview</a><a href="#prev">go to previous</a>'
+ ));
+
+ it('navigates to <a> elements whose decoded text matches "<<"', testPrev(
+ '<a href="#dummy">click me</a><a href="#prev">&lt;&lt;</a>'
+ ));
+
+ it('navigates to matching <a> elements by clicking', testPrev(
+ `<a rel="prev" href="#dummy" onclick="return location = '#prev', false">go to prev</a>`
+ ));
+
+ it('prefers link[rel~=prev] to a[rel~=prev]', testPrev(
+ '<a rel="prev" href="#dummy">click me</a><link rel="prev" href="#prev" />'
+ ));
+ it('prefers a[rel~=prev] to a::text(pattern)', testPrev(
+ '<a href="#dummy">go to prev</a><a rel="prev" href="#prev">click me</a>'
+ ));
+ });
describe('#linkNext', () => {
- it('clicks next link by text content', (done) => {
- document.body.innerHTML = '<a href="#dummy">xnextx</a> <a href="#next">go to next</a>';
- navigates.linkNext(window);
- setTimeout(() => {
- expect(document.location.hash).to.equal('#next');
- done();
- }, 0);
- });
+ it('navigates to <link> elements whose rel attribute is "next"', testNext(
+ '<link rel="next" href="#next" />'
+ ));
- it('clicks a[rel=next] element preferentially', (done) => {
- document.body.innerHTML = '<a href="#dummy">next</a> <a rel="next" href="#next">rel</a>';
- navigates.linkNext(window);
- setTimeout(() => {
- expect(document.location.hash).to.equal('#next');
- done();
- }, 0);
- });
+ it('navigates to <link> elements whose rel attribute starts with "next"', testNext(
+ '<link rel="next bar" href="#next" />'
+ ));
+
+ it('navigates to <link> elements whose rel attribute ends with "next"', testNext(
+ '<link rel="foo next" href="#next" />'
+ ));
+
+ it('navigates to <link> elements whose rel attribute contains "next"', testNext(
+ '<link rel="foo next bar" href="#next" />'
+ ));
+
+ it('navigates to <a> elements whose rel attribute is "next"', testNext(
+ '<a rel="next" href="#next">click me</a>'
+ ));
+
+ it('navigates to <a> elements whose rel attribute starts with "next"', testNext(
+ '<a rel="next bar" href="#next">click me</a>'
+ ));
+
+ it('navigates to <a> elements whose rel attribute ends with "next"', testNext(
+ '<a rel="foo next" href="#next">click me</a>'
+ ));
+
+ it('navigates to <a> elements whose rel attribute contains "next"', testNext(
+ '<a rel="foo next bar" href="#next">click me</a>'
+ ));
+
+ it('navigates to <a> elements whose text matches "next"', testNext(
+ '<a href="#dummy">inextricable</a><a href="#next">go to next</a>'
+ ));
+
+ it('navigates to <a> elements whose decoded text matches ">>"', testNext(
+ '<a href="#dummy">click me</a><a href="#next">&gt;&gt;</a>'
+ ));
+
+ it('navigates to matching <a> elements by clicking', testNext(
+ `<a rel="next" href="#dummy" onclick="return location = '#next', false">go to next</a>`
+ ));
+
+ it('prefers link[rel~=next] to a[rel~=next]', testNext(
+ '<a rel="next" href="#dummy">click me<><link rel="next" href="#next" />'
+ ));
+
+ it('prefers a[rel~=next] to a::text(pattern)', testNext(
+ '<a href="#dummy">go to next</a><a rel="next" href="#next">click me</a>'
+ ));
});
describe('#parent', () => {
// NOTE: not able to test location
it('removes hash', () => {
- window.location.hash = "#section-1";
+ window.location.hash = '#section-1';
navigates.parent(window);
expect(document.location.hash).to.be.empty;
});
});
});
-
-
diff --git a/test/content/reducers/find.test.js b/test/content/reducers/find.test.js
new file mode 100644
index 0000000..93625da
--- /dev/null
+++ b/test/content/reducers/find.test.js
@@ -0,0 +1,23 @@
+import { expect } from "chai";
+import actions from 'content/actions';
+import findReducer from 'content/reducers/find';
+
+describe("find reducer", () => {
+ it('return the initial state', () => {
+ let state = findReducer(undefined, {});
+ expect(state).to.have.property('keyword', '');
+ expect(state).to.have.property('found', false);
+ });
+
+ it('return next state for FIND_SET_KEYWORD', () => {
+ let action = {
+ type: actions.FIND_SET_KEYWORD,
+ keyword: 'xyz',
+ found: true,
+ };
+ let state = findReducer({}, action);
+
+ expect(state.keyword).is.equal('xyz');
+ expect(state.found).to.be.true;
+ });
+});
diff --git a/test/content/reducers/input.test.js b/test/content/reducers/input.test.js
index d5e5f6b..d0b5655 100644
--- a/test/content/reducers/input.test.js
+++ b/test/content/reducers/input.test.js
@@ -5,22 +5,22 @@ import inputReducer from 'content/reducers/input';
describe("input reducer", () => {
it('return the initial state', () => {
let state = inputReducer(undefined, {});
- expect(state).to.have.deep.property('keys', '');
+ expect(state).to.have.deep.property('keys', []);
});
it('return next state for INPUT_KEY_PRESS', () => {
let action = { type: actions.INPUT_KEY_PRESS, key: 'a' };
let state = inputReducer(undefined, action);
- expect(state).to.have.deep.property('keys', 'a');
+ expect(state).to.have.deep.property('keys', ['a']);
- action = { type: actions.INPUT_KEY_PRESS, key: '<C-B>' };
+ action = { type: actions.INPUT_KEY_PRESS, key: 'b' };
state = inputReducer(state, action);
- expect(state).to.have.deep.property('keys', 'a<C-B>');
+ expect(state).to.have.deep.property('keys', ['a', 'b']);
});
it('return next state for INPUT_CLEAR_KEYS', () => {
let action = { type: actions.INPUT_CLEAR_KEYS };
- let state = inputReducer({ keys: 'abc' }, action);
- expect(state).to.have.deep.property('keys', '');
+ let state = inputReducer({ keys: [1, 2, 3] }, action);
+ expect(state).to.have.deep.property('keys', []);
});
});
diff --git a/test/content/reducers/setting.test.js b/test/content/reducers/setting.test.js
index ef49594..634b299 100644
--- a/test/content/reducers/setting.test.js
+++ b/test/content/reducers/setting.test.js
@@ -5,7 +5,7 @@ import settingReducer from 'content/reducers/setting';
describe("content setting reducer", () => {
it('return the initial state', () => {
let state = settingReducer(undefined, {});
- expect(state).to.deep.equal({ keymaps: {} });
+ expect(state.keymaps).to.be.empty;
});
it('return next state for SETTING_SET', () => {
diff --git a/test/shared/utils/keys.test.js b/test/shared/utils/keys.test.js
new file mode 100644
index 0000000..5ca8b54
--- /dev/null
+++ b/test/shared/utils/keys.test.js
@@ -0,0 +1,145 @@
+import { expect } from 'chai';
+import * as keys from 'shared/utils/keys';
+
+describe("keys util", () => {
+ describe('fromKeyboardEvent', () => {
+ it('returns from keyboard input Ctrl+X', () => {
+ let k = keys.fromKeyboardEvent({
+ key: 'x', shiftKey: false, ctrlKey: true, altKey: false, metaKey: true
+ });
+ expect(k.key).to.equal('x');
+ expect(k.shiftKey).to.be.false;
+ expect(k.ctrlKey).to.be.true;
+ expect(k.altKey).to.be.false;
+ expect(k.metaKey).to.be.true;
+ });
+
+ it('returns from keyboard input Shift+Esc', () => {
+ let k = keys.fromKeyboardEvent({
+ key: 'Escape', shiftKey: true, ctrlKey: false, altKey: false, metaKey: true
+ });
+ expect(k.key).to.equal('Esc');
+ expect(k.shiftKey).to.be.true;
+ expect(k.ctrlKey).to.be.false;
+ expect(k.altKey).to.be.false;
+ expect(k.metaKey).to.be.true;
+ });
+
+ it('returns from keyboard input Ctrl+$', () => {
+ // $ required shift pressing on most keyboards
+ let k = keys.fromKeyboardEvent({
+ key: '$', shiftKey: true, ctrlKey: true, altKey: false, metaKey: false
+ });
+ expect(k.key).to.equal('$');
+ expect(k.shiftKey).to.be.false;
+ expect(k.ctrlKey).to.be.true;
+ expect(k.altKey).to.be.false;
+ expect(k.metaKey).to.be.false;
+ });
+ });
+
+ describe('fromMapKey', () => {
+ it('return for X', () => {
+ let key = keys.fromMapKey('x');
+ expect(key.key).to.equal('x');
+ expect(key.shiftKey).to.be.false;
+ expect(key.ctrlKey).to.be.false;
+ expect(key.altKey).to.be.false;
+ expect(key.metaKey).to.be.false;
+ });
+
+ it('return for Shift+X', () => {
+ let key = keys.fromMapKey('X');
+ expect(key.key).to.equal('X');
+ expect(key.shiftKey).to.be.true;
+ expect(key.ctrlKey).to.be.false;
+ expect(key.altKey).to.be.false;
+ expect(key.metaKey).to.be.false;
+ });
+
+ it('return for Ctrl+X', () => {
+ let key = keys.fromMapKey('<C-X>');
+ expect(key.key).to.equal('x');
+ expect(key.shiftKey).to.be.false;
+ expect(key.ctrlKey).to.be.true;
+ expect(key.altKey).to.be.false;
+ expect(key.metaKey).to.be.false;
+ });
+
+ it('returns for Ctrl+Meta+X', () => {
+ let key = keys.fromMapKey('<C-M-X>');
+ expect(key.key).to.equal('x');
+ expect(key.shiftKey).to.be.false;
+ expect(key.ctrlKey).to.be.true;
+ expect(key.altKey).to.be.false;
+ expect(key.metaKey).to.be.true;
+ });
+
+ it('returns for Ctrl+Shift+x', () => {
+ let key = keys.fromMapKey('<C-S-x>');
+ expect(key.key).to.equal('X');
+ expect(key.shiftKey).to.be.true;
+ expect(key.ctrlKey).to.be.true;
+ expect(key.altKey).to.be.false;
+ expect(key.metaKey).to.be.false;
+ });
+
+ it('returns for Shift+Esc', () => {
+ let key = keys.fromMapKey('<S-Esc>');
+ expect(key.key).to.equal('Esc');
+ expect(key.shiftKey).to.be.true;
+ expect(key.ctrlKey).to.be.false;
+ expect(key.altKey).to.be.false;
+ expect(key.metaKey).to.be.false;
+ });
+
+ it('returns for Ctrl+Esc', () => {
+ let key = keys.fromMapKey('<C-Esc>');
+ expect(key.key).to.equal('Esc');
+ expect(key.shiftKey).to.be.false;
+ expect(key.ctrlKey).to.be.true;
+ expect(key.altKey).to.be.false;
+ expect(key.metaKey).to.be.false;
+ });
+ });
+
+ describe('fromMapKeys', () => {
+ it('returns mapped keys for Shift+Esc', () => {
+ let keyArray = keys.fromMapKeys('<S-Esc>');
+ expect(keyArray).to.have.lengthOf(1);
+ expect(keyArray[0].key).to.equal('Esc');
+ expect(keyArray[0].shiftKey).to.be.true;
+ });
+
+ it('returns mapped keys for a<C-B><A-C>d<M-e>', () => {
+ let keyArray = keys.fromMapKeys('a<C-B><A-C>d<M-e>');
+ expect(keyArray).to.have.lengthOf(5);
+ expect(keyArray[0].key).to.equal('a');
+ expect(keyArray[1].ctrlKey).to.be.true;
+ expect(keyArray[1].key).to.equal('b');
+ expect(keyArray[2].altKey).to.be.true;
+ expect(keyArray[2].key).to.equal('c');
+ expect(keyArray[3].key).to.equal('d');
+ expect(keyArray[4].metaKey).to.be.true;
+ expect(keyArray[4].key).to.equal('e');
+ });
+ })
+
+ describe('equals', () => {
+ expect(keys.equals({
+ key: 'x',
+ ctrlKey: true,
+ }, {
+ key: 'x',
+ ctrlKey: true,
+ })).to.be.true;
+
+ expect(keys.equals({
+ key: 'X',
+ shiftKey: true,
+ }, {
+ key: 'x',
+ ctrlKey: true,
+ })).to.be.false;
+ });
+});
diff --git a/test/shared/util/re.test.js b/test/shared/utils/re.test.js
index 9ed6521..9ed6521 100644
--- a/test/shared/util/re.test.js
+++ b/test/shared/utils/re.test.js