Merge remote-tracking branch 'origin/master' into patch-1
This commit is contained in:
commit
9da2f5fd78
119 changed files with 14640 additions and 1632 deletions
45
.circleci/config.yml
Normal file
45
.circleci/config.yml
Normal file
|
@ -0,0 +1,45 @@
|
|||
version: 2
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: circleci/node:9-stretch-browsers
|
||||
environment:
|
||||
- FIREFOX_VERSION: "59.0b9"
|
||||
working_directory: ~
|
||||
steps:
|
||||
- restore_cache:
|
||||
key: firefox-bin
|
||||
paths:
|
||||
- ~/firefox
|
||||
- run:
|
||||
name: Install Firefox
|
||||
command: |
|
||||
test -d ~/firefox/${FIREFOX_VERSION} && exit 0
|
||||
url=https://ftp.mozilla.org/pub/firefox/releases/${FIREFOX_VERSION}/linux-x86_64/en-US/firefox-${FIREFOX_VERSION}.tar.bz2
|
||||
curl -sSL -o- "$url" | tar xvfj -
|
||||
mkdir -p ~/firefox
|
||||
mv firefox ~/firefox/${FIREFOX_VERSION}
|
||||
- save_cache:
|
||||
key: firefox-bin
|
||||
paths:
|
||||
- ~/firefox
|
||||
- run: sudo apt-get update && sudo apt-get install -y libgtk-3-0 libdbus-glib-1-2
|
||||
|
||||
- checkout
|
||||
- restore_cache:
|
||||
key: dependency-cache-{{ checksum "package-lock.json" }}
|
||||
- run:
|
||||
name: Install npm wee
|
||||
command: npm install
|
||||
- save_cache:
|
||||
key: dependency-cache-{{ checksum "package-lock.json" }}
|
||||
paths:
|
||||
- node_modules
|
||||
|
||||
- run: echo 'export PATH=~/firefox/$FIREFOX_VERSION:$PATH' >> $BASH_ENV
|
||||
- run: npm run lint
|
||||
- run: npm test
|
||||
- run: npm run package
|
||||
- run: npm run build
|
||||
- run: npm run ambassador:build
|
||||
- run: node e2e/web-server & npm run test:e2e
|
|
@ -36,6 +36,7 @@
|
|||
"newline-after-var": "off",
|
||||
"newline-before-return": "off",
|
||||
"newline-per-chained-call": "off",
|
||||
"no-alert": "off",
|
||||
"no-bitwise": "off",
|
||||
"no-console": ["error", { "allow": ["warn", "error"] }],
|
||||
"no-empty-function": "off",
|
||||
|
@ -44,6 +45,8 @@
|
|||
"no-plusplus": "off",
|
||||
"no-ternary": "off",
|
||||
"no-undefined": "off",
|
||||
"no-undef-init": "off",
|
||||
"no-unused-vars": ["error", { "varsIgnorePattern": "h" }],
|
||||
"no-use-before-define": "off",
|
||||
"no-warning-comments": "off",
|
||||
"object-curly-newline": ["error", { "consistent": true }],
|
||||
|
@ -65,5 +68,6 @@
|
|||
|
||||
"react/jsx-indent": ["error", 2],
|
||||
"react/prop-types": "off",
|
||||
"react/react-in-jsx-scope": "off"
|
||||
}
|
||||
}
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
|||
/node_modules/
|
||||
/build/
|
||||
/e2e/ambassador/build/
|
||||
*.zip
|
||||
|
|
12
.travis.yml
12
.travis.yml
|
@ -1,12 +0,0 @@
|
|||
language: node_js
|
||||
node_js:
|
||||
- "6"
|
||||
addons:
|
||||
firefox: "56.0"
|
||||
before_script:
|
||||
- export DISPLAY=:99.0
|
||||
- sh -e /etc/init.d/xvfb start
|
||||
script:
|
||||
- npm run lint
|
||||
- npm test
|
||||
- npm run package
|
89
QA.md
89
QA.md
|
@ -1,17 +1,14 @@
|
|||
## Checklist for testing Vim Vixen
|
||||
|
||||
### Operations
|
||||
### Keybindings in JSON settings
|
||||
|
||||
Test operations with default key maps.
|
||||
|
||||
#### 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
|
||||
- [ ] <kbd>Ctrl</kbd>+<kbd>U</kbd>, <kbd>Ctrl</kbd>+<kbd>D</kbd>: scroll up and down by half of screen
|
||||
- [ ] <kbd>Ctrl</kbd>+<kbd>B</kbd>, <kbd>Ctrl</kbd>+<kbd>F</kbd>: scroll up and down by a screen
|
||||
- [ ] <kbd>0</kbd>, <kbd>$</kbd>: scroll to leftmost and rightmost
|
||||
- [ ] <kbd>g</kbd><kbd>g</kbd>, <kbd>G</kbd>: scroll to top and bottom
|
||||
- [ ] Smooth scroll by `:set smoothscroll`
|
||||
- [ ] Non-smooth scroll by `:set nosmoothscroll`
|
||||
- [ ] Configure custom hint character by settings `"smoothscroll": true`, `"smoothscroll": false`
|
||||
|
||||
#### Console
|
||||
|
||||
|
@ -24,28 +21,26 @@ The behaviors of the console are tested in [Console section](#consoles).
|
|||
|
||||
#### Tabs
|
||||
|
||||
- [ ] <kbd>d</kbd>: delete current tab
|
||||
- [ ] <kbd>!d</kbd>: delete current tab and pinned 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
|
||||
|
||||
- [ ] <kbd>H</kbd>, <kbd>L</kbd>: go back and forward in histories
|
||||
- [ ] <kbd>H</kbd>, <kbd>L</kbd>: go back and forward in history
|
||||
- [ ] <kbd>[</kbd><kbd>[</kbd>, <kbd>]</kbd><kbd>]</kbd>: Open next/prev link in `<link>` tags.
|
||||
- [ ] <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
|
||||
|
||||
- [ ] <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
|
||||
- [ ] <kbd>p</kbd>: open clipbord's URL in current tab
|
||||
- [ ] <kbd>P</kbd>: open clipbord's URL in new tab
|
||||
- [ ] Toggle enabled/disabled of plugin bu <kbd>Shift</kbd>+<kbd>Esc</kbd>
|
||||
- [ ] Hide error and info console by <kbd>Esc</kbd>
|
||||
|
||||
### Following links
|
||||
|
||||
|
@ -59,6 +54,9 @@ The behaviors of the console are tested in [Console section](#consoles).
|
|||
- [ ] 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>
|
||||
- [ ] Configure custom hint character by `:set hintchars=012345678`
|
||||
- [ ] Configure custom hint character by settings `"hintchars": "012345678"`
|
||||
- [ ] Opened tabs is in child on Tree Style Tab
|
||||
|
||||
### Consoles
|
||||
|
||||
|
@ -82,7 +80,7 @@ The behaviors of the console are tested in [Console section](#consoles).
|
|||
- [ ] `buffer`,`buffer<SP>`: do nothing
|
||||
- [ ] `buffer <title>`, `buffer <url>`: select tab which has an title matched with
|
||||
- [ ] `buffer 1`: select leftmost tab
|
||||
- [ ] `buffer 0`, `buffer 99`: shows an error
|
||||
- [ ] `buffer 0`, `buffer <a number more than count of tabs>`: shows an error
|
||||
- [ ] select tabs rotationally when more than two tabs are matched
|
||||
|
||||
### Completions
|
||||
|
@ -109,20 +107,22 @@ The behaviors of the console are tested in [Console section](#consoles).
|
|||
|
||||
### Settings
|
||||
|
||||
#### Validations
|
||||
#### JSON Settings
|
||||
|
||||
##### Validations
|
||||
|
||||
- [ ] show error on invalid json
|
||||
- [ ] show error when top-level keys has keys other than `keymaps`, `search`, and `blacklist`
|
||||
- [ ] show error when top-level keys has keys other than `keymaps`, `search`, `blacklist`, and `properties`
|
||||
|
||||
##### `"keymaps"` section
|
||||
###### `"keymaps"` section
|
||||
|
||||
- [ ] show error on unknown operation name in `"keymaps"`
|
||||
|
||||
##### `"search"` section
|
||||
###### `"search"` section
|
||||
|
||||
- validations in `"search"` section are not tested in this release
|
||||
|
||||
#### `"blacklist"` section
|
||||
##### `"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`
|
||||
|
@ -130,27 +130,66 @@ The behaviors of the console are tested in [Console section](#consoles).
|
|||
- [ ] `github.com` blocks both `github.com/` and `github.com/a`
|
||||
- [ ] `*.github.com` blocks `gist.github.com/`, and not `github.com`
|
||||
|
||||
#### Updating
|
||||
##### 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
|
||||
|
||||
##### Properties
|
||||
|
||||
- [ ] show errors when invalid property name
|
||||
- [ ] show errors when invalid property type
|
||||
|
||||
#### Form Settings
|
||||
|
||||
<!-- validation on form settings does not implement in 0.7 -->
|
||||
|
||||
##### Search Engines
|
||||
|
||||
- [ ] able to change default
|
||||
- [ ] able to remove item
|
||||
- [ ] able to add item
|
||||
|
||||
##### `"blacklist"` section
|
||||
|
||||
- [ ] able to add item
|
||||
- [ ] able to remove item
|
||||
- [ ] `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
|
||||
|
||||
- [ ] keymap settings are applied to open tabs without reload
|
||||
- [ ] search settings are applied to open tabs without reload
|
||||
|
||||
### Settings source
|
||||
|
||||
- [ ] show confirmation dialog on switched from json to form
|
||||
- [ ] state is saved on source changed
|
||||
- [ ] on switching form -> json -> form, first and last form setting is equivalent to first one
|
||||
|
||||
### For certain sites
|
||||
|
||||
- [ ] scoll on Hacker News
|
||||
- [ ] scroll 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 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
|
||||
- [ ] Tha pages is shown in https://pitchify.com/
|
||||
- [ ] Open console in http://www.espncricinfo.com/
|
||||
|
||||
## Find mode
|
||||
|
||||
- [ ] open console with <kbd>/</kbd>
|
||||
- [ ] highlight a word on <kbd>Enter</kb> pressed in find console
|
||||
- [ ] highlight a word on <kbd>Enter</kbd> 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
|
||||
- [ ] Find keyword last used on new tab opened
|
||||
|
||||
## Misc
|
||||
|
||||
|
|
63
README.md
63
README.md
|
@ -1,5 +1,11 @@
|
|||
# Vim Vixen
|
||||
|
||||
[](https://greenkeeper.io/)
|
||||
|
||||
[](https://gitter.im/vim-vixen/vim-vixen)
|
||||
[](https://circleci.com/gh/ueokande/vim-vixen)
|
||||
[](https://david-dm.org/ueokande/vim-vixen?type=dev)
|
||||
|
||||
Vim Vixen is a Firefox add-on which allows you to navigate with keyboard on the browser.
|
||||
Firefox started to support WebExtensions API and will stop supporting add-ons using legacy APIs from version 57.
|
||||
For this reason, many legacy add-ons do not work on Firefox 57.
|
||||
|
@ -19,38 +25,30 @@ The default mappings are as follows:
|
|||
- <kbd>O</kbd>, <kbd>T</kbd>, <kbd>W</kbd>: similar to <kbd>o</kbd>, <kbd>t</kbd>, <kbd>w</kbd>, but that contains current URL
|
||||
- <kbd>b</kbd>: Select tabs by URL or title
|
||||
|
||||
#### Scrolling
|
||||
|
||||
- <kbd>j</kbd>, <kbd>k</kbd>: scroll vertically
|
||||
- <kbd>h</kbd>, <kbd>l</kbd>: scroll horizontally
|
||||
- <kbd>Ctrl</kbd>+<kbd>U</kbd>, <kbd>Ctrl</kbd>+<kbd>D</kbd>: scroll pages by half of screen
|
||||
- <kbd>Ctrl</kbd>+<kbd>B</kbd>, <kbd>Ctrl</kbd>+<kbd>F</kbd>: scroll pages by a screen
|
||||
- <kbd>0</kbd>, <kbd>$</kbd>: scroll a page to leftmost/rightmost
|
||||
- <kbd>g</kbd><kbd>g</kbd>, <kbd>G</kbd>: scroll to top/bottom
|
||||
|
||||
#### Tabs
|
||||
- <kbd>d</kbd>: delete current tab
|
||||
- <kbd>!</kbd><kbd>d</kbd>: delete pinned 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
|
||||
- <kbd>H</kbd>: go back in histories
|
||||
- <kbd>L</kbd>: go forward in histories
|
||||
- <kbd>H</kbd>: go back in history
|
||||
- <kbd>L</kbd>: go forward in history
|
||||
- <kbd>[</kbd><kbd>[</kbd>, <kbd>]</kbd><kbd>]</kbd>: find prev or 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
|
||||
- <kbd>g</kbd><kbd>i</kbd>: focus first input
|
||||
|
||||
#### Misc
|
||||
- <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>p</kbd>: open clipbord's URL in current tab
|
||||
- <kbd>P</kbd>: open clipbord's URL in new tab
|
||||
- <kbd>Shift</kbd>+<kbd>Esc</kbd>: enable or disable the add-on in current tab.
|
||||
- <kbd>/</kbd>: start to find a keyword in the page
|
||||
- <kbd>n</kbd>: find next keyword in the page
|
||||
- <kbd>N</kbd>: find prev keyword in the page
|
||||
|
||||
### Console commands
|
||||
|
||||
|
@ -94,6 +92,35 @@ Open a URL or search keywords by search engine in new window.
|
|||
|
||||
Select tabs by URL or title matched by keywords.
|
||||
|
||||
#### `:set` command
|
||||
|
||||
`:set` command can set properties on console.
|
||||
|
||||
##### `smoothscroll` property
|
||||
|
||||
Enable/disable smooth scroll.
|
||||
```
|
||||
:set smoothscroll " enable smooth scroll
|
||||
:set nosmoothscroll " disable smooth scroll
|
||||
```
|
||||
|
||||
##### `hintchars` property
|
||||
|
||||
Set hint characters
|
||||
|
||||
```
|
||||
:set hintchars=0123456789
|
||||
```
|
||||
|
||||
##### 'adjacenttab' property
|
||||
|
||||
Open a new tab on adjacent of the current tab.
|
||||
|
||||
```
|
||||
:set noadjacenttab " open a tab at last
|
||||
:set adjacenttab " open a tab adjacently
|
||||
```
|
||||
|
||||
### Search engines
|
||||
|
||||
Vim Vixen supports search by search engines like Google and Yahoo.
|
||||
|
|
28
e2e/ambassador/manifest.json
Normal file
28
e2e/ambassador/manifest.json
Normal file
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"manifest_version": 2,
|
||||
"name": "ambassador",
|
||||
"description": "WebExtension test helper",
|
||||
"version": "0.1",
|
||||
"content_scripts": [
|
||||
{
|
||||
"all_frames": true,
|
||||
"matches": [ "<all_urls>" ],
|
||||
"js": [ "build/content.js" ],
|
||||
"run_at": "document_start",
|
||||
"match_about_blank": true
|
||||
}
|
||||
],
|
||||
"background": {
|
||||
"scripts": [
|
||||
"build/background.js"
|
||||
]
|
||||
},
|
||||
"permissions": [
|
||||
"history",
|
||||
"sessions",
|
||||
"storage",
|
||||
"tabs",
|
||||
"clipboardRead",
|
||||
"activeTab"
|
||||
]
|
||||
}
|
42
e2e/ambassador/src/background/index.js
Normal file
42
e2e/ambassador/src/background/index.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
import {
|
||||
WINDOWS_CREATE, WINDOWS_REMOVE, WINDOWS_GET,
|
||||
TABS_CREATE, TABS_SELECT_AT, TABS_GET_ZOOM, TABS_SET_ZOOM,
|
||||
EVENT_KEYPRESS, EVENT_KEYDOWN, EVENT_KEYUP,
|
||||
SCROLL_GET, SCROLL_SET,
|
||||
} from '../shared/messages';
|
||||
import * as tabs from './tabs';
|
||||
import { receiveContentMessage } from './ipc';
|
||||
|
||||
receiveContentMessage((message) => {
|
||||
switch (message.type) {
|
||||
case WINDOWS_CREATE:
|
||||
return browser.windows.create({ url: message.url });
|
||||
case WINDOWS_REMOVE:
|
||||
return browser.windows.remove(message.windowId);
|
||||
case WINDOWS_GET:
|
||||
return browser.windows.get(message.windowId, { populate: true });
|
||||
case TABS_CREATE:
|
||||
return tabs.create({
|
||||
url: message.url,
|
||||
windowId: message.windowId,
|
||||
});
|
||||
case TABS_SELECT_AT:
|
||||
return tabs.selectAt({
|
||||
windowId: message.windowId,
|
||||
index: message.index,
|
||||
});
|
||||
case TABS_GET_ZOOM:
|
||||
return browser.tabs.getZoom(message.tabId);
|
||||
case TABS_SET_ZOOM:
|
||||
return browser.tabs.setZoom(message.tabId, message.factor);
|
||||
case EVENT_KEYPRESS:
|
||||
case EVENT_KEYDOWN:
|
||||
case EVENT_KEYUP:
|
||||
case SCROLL_GET:
|
||||
case SCROLL_SET:
|
||||
return browser.tabs.sendMessage(
|
||||
message.tabId,
|
||||
message
|
||||
);
|
||||
}
|
||||
});
|
7
e2e/ambassador/src/background/ipc.js
Normal file
7
e2e/ambassador/src/background/ipc.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
const receiveContentMessage = (func) => {
|
||||
browser.runtime.onMessage.addListener((message) => {
|
||||
return func(message);
|
||||
});
|
||||
};
|
||||
|
||||
export { receiveContentMessage };
|
26
e2e/ambassador/src/background/tabs.js
Normal file
26
e2e/ambassador/src/background/tabs.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
const create = (props = {}) => {
|
||||
return new Promise((resolve) => {
|
||||
browser.tabs.create(props).then((createdTab) => {
|
||||
let callback = (tabId, changeInfo, tab) => {
|
||||
if (tab.url !== 'about:blank' && tabId === createdTab.id &&
|
||||
changeInfo.status === 'complete') {
|
||||
browser.tabs.onUpdated.removeListener(callback);
|
||||
resolve(tab);
|
||||
}
|
||||
};
|
||||
browser.tabs.onUpdated.addListener(callback);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const selectAt = (props = {}) => {
|
||||
return browser.tabs.query({ windowId: props.windowId }).then((tabs) => {
|
||||
let target = tabs[props.index];
|
||||
return browser.tabs.update(target.id, { active: true });
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export {
|
||||
create, selectAt
|
||||
};
|
29
e2e/ambassador/src/client/ipc.js
Normal file
29
e2e/ambassador/src/client/ipc.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { METHOD_REQUEST, METHOD_RESPONSE } from '../shared/messages';
|
||||
|
||||
const generateId = () => {
|
||||
return Math.random().toString();
|
||||
};
|
||||
|
||||
const send = (message) => {
|
||||
return new Promise((resolve) => {
|
||||
let id = generateId();
|
||||
let callback = (e) => {
|
||||
let packet = e.data;
|
||||
if (e.source !== window || packet.method !== METHOD_RESPONSE ||
|
||||
packet.id !== id) {
|
||||
return;
|
||||
}
|
||||
window.removeEventListener('message', callback);
|
||||
resolve(packet.message);
|
||||
};
|
||||
window.addEventListener('message', callback);
|
||||
|
||||
window.postMessage({
|
||||
id,
|
||||
method: METHOD_REQUEST,
|
||||
message
|
||||
}, window.origin);
|
||||
});
|
||||
};
|
||||
|
||||
export { send };
|
31
e2e/ambassador/src/client/keys.js
Normal file
31
e2e/ambassador/src/client/keys.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { EVENT_KEYPRESS, EVENT_KEYDOWN, EVENT_KEYUP } from '../shared/messages';
|
||||
import * as ipc from './ipc';
|
||||
|
||||
const NEUTRAL_MODIFIERS = { shiftKey: false, altKey: false, ctrlKey: false };
|
||||
|
||||
const press = (tabId, key, modifiers = NEUTRAL_MODIFIERS) => {
|
||||
return ipc.send(Object.assign({}, modifiers, {
|
||||
type: EVENT_KEYPRESS,
|
||||
tabId,
|
||||
key,
|
||||
}));
|
||||
};
|
||||
|
||||
const down = (tabId, key, modifiers = NEUTRAL_MODIFIERS) => {
|
||||
return ipc.send(Object.assign({}, modifiers, {
|
||||
type: EVENT_KEYDOWN,
|
||||
tabId,
|
||||
key,
|
||||
}));
|
||||
};
|
||||
|
||||
|
||||
const up = (tabId, key, modifiers = NEUTRAL_MODIFIERS) => {
|
||||
return ipc.send(Object.assign({}, modifiers, {
|
||||
type: EVENT_KEYUP,
|
||||
tabId,
|
||||
key,
|
||||
}));
|
||||
};
|
||||
|
||||
export { press, down, up };
|
20
e2e/ambassador/src/client/scrolls.js
Normal file
20
e2e/ambassador/src/client/scrolls.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { SCROLL_GET, SCROLL_SET } from '../shared/messages';
|
||||
import * as ipc from './ipc';
|
||||
|
||||
const get = (tabId) => {
|
||||
return ipc.send({
|
||||
type: SCROLL_GET,
|
||||
tabId,
|
||||
});
|
||||
};
|
||||
|
||||
const set = (tabId, x, y) => {
|
||||
return ipc.send({
|
||||
type: SCROLL_SET,
|
||||
tabId,
|
||||
x,
|
||||
y,
|
||||
});
|
||||
};
|
||||
|
||||
export { get, set };
|
37
e2e/ambassador/src/client/tabs.js
Normal file
37
e2e/ambassador/src/client/tabs.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
import {
|
||||
TABS_CREATE, TABS_SELECT_AT, TABS_GET_ZOOM, TABS_SET_ZOOM,
|
||||
} from '../shared/messages';
|
||||
import * as ipc from './ipc';
|
||||
|
||||
const create = (windowId, url) => {
|
||||
return ipc.send({
|
||||
type: TABS_CREATE,
|
||||
windowId,
|
||||
url,
|
||||
});
|
||||
};
|
||||
|
||||
const selectAt = (windowId, index) => {
|
||||
return ipc.send({
|
||||
type: TABS_SELECT_AT,
|
||||
windowId,
|
||||
index,
|
||||
});
|
||||
};
|
||||
|
||||
const getZoom = (tabId) => {
|
||||
return ipc.send({
|
||||
tabId,
|
||||
type: TABS_GET_ZOOM,
|
||||
});
|
||||
};
|
||||
|
||||
const setZoom = (tabId, factor) => {
|
||||
return ipc.send({
|
||||
type: TABS_SET_ZOOM,
|
||||
tabId,
|
||||
factor,
|
||||
});
|
||||
};
|
||||
|
||||
export { create, selectAt, getZoom, setZoom };
|
27
e2e/ambassador/src/client/windows.js
Normal file
27
e2e/ambassador/src/client/windows.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
import {
|
||||
WINDOWS_CREATE, WINDOWS_REMOVE, WINDOWS_GET
|
||||
} from '../shared/messages';
|
||||
import * as ipc from './ipc';
|
||||
|
||||
const create = (url) => {
|
||||
return ipc.send({
|
||||
type: WINDOWS_CREATE,
|
||||
url,
|
||||
});
|
||||
};
|
||||
|
||||
const remove = (windowId) => {
|
||||
return ipc.send({
|
||||
type: WINDOWS_REMOVE,
|
||||
windowId,
|
||||
});
|
||||
};
|
||||
|
||||
const get = (windowId) => {
|
||||
return ipc.send({
|
||||
type: WINDOWS_GET,
|
||||
windowId,
|
||||
});
|
||||
};
|
||||
|
||||
export { create, remove, get };
|
31
e2e/ambassador/src/content/events.js
Normal file
31
e2e/ambassador/src/content/events.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
const keypress = (opts) => {
|
||||
let event = new KeyboardEvent('keypress', {
|
||||
key: opts.key,
|
||||
altKey: opts.altKey,
|
||||
shiftKey: opts.shiftKey,
|
||||
ctrlKey: opts.ctrlKey
|
||||
});
|
||||
document.body.dispatchEvent(event);
|
||||
};
|
||||
|
||||
const keydown = (opts) => {
|
||||
let event = new KeyboardEvent('keydown', {
|
||||
key: opts.key,
|
||||
altKey: opts.altKey,
|
||||
shiftKey: opts.shiftKey,
|
||||
ctrlKey: opts.ctrlKey
|
||||
});
|
||||
document.body.dispatchEvent(event);
|
||||
};
|
||||
|
||||
const keyup = (opts) => {
|
||||
let event = new KeyboardEvent('keyup', {
|
||||
key: opts.key,
|
||||
altKey: opts.altKey,
|
||||
shiftKey: opts.shiftKey,
|
||||
ctrlKey: opts.ctrlKey
|
||||
});
|
||||
document.body.dispatchEvent(event);
|
||||
};
|
||||
|
||||
export { keypress, keydown, keyup };
|
30
e2e/ambassador/src/content/index.js
Normal file
30
e2e/ambassador/src/content/index.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
import {
|
||||
EVENT_KEYPRESS, EVENT_KEYDOWN, EVENT_KEYUP,
|
||||
SCROLL_GET, SCROLL_SET,
|
||||
} from '../shared/messages';
|
||||
import * as ipc from './ipc';
|
||||
import * as events from './events';
|
||||
import * as scrolls from './scrolls';
|
||||
|
||||
ipc.receivePageMessage((message) => {
|
||||
return ipc.sendToBackground(message);
|
||||
});
|
||||
|
||||
ipc.receiveBackgroundMesssage((message) => {
|
||||
switch (message.type) {
|
||||
case EVENT_KEYPRESS:
|
||||
events.keypress(message);
|
||||
break;
|
||||
case EVENT_KEYDOWN:
|
||||
events.keydown(message);
|
||||
break;
|
||||
case EVENT_KEYUP:
|
||||
events.keyup(message);
|
||||
break;
|
||||
case SCROLL_GET:
|
||||
return Promise.resolve(scrolls.get());
|
||||
case SCROLL_SET:
|
||||
return Promise.resolve(scrolls.set(message.x, message.y));
|
||||
}
|
||||
return Promise.resolve({});
|
||||
});
|
40
e2e/ambassador/src/content/ipc.js
Normal file
40
e2e/ambassador/src/content/ipc.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { METHOD_REQUEST, METHOD_RESPONSE } from '../shared/messages';
|
||||
|
||||
const sendToBackground = (message) => {
|
||||
return browser.runtime.sendMessage(message);
|
||||
};
|
||||
|
||||
const receiveBackgroundMesssage = (func) => {
|
||||
return browser.runtime.onMessage.addListener((message) => {
|
||||
return Promise.resolve(func(message));
|
||||
});
|
||||
};
|
||||
|
||||
const receivePageMessage = (func) => {
|
||||
window.addEventListener('message', (e) => {
|
||||
let packet = e.data;
|
||||
if (e.origin !== window.origin || packet.method !== METHOD_REQUEST) {
|
||||
return;
|
||||
}
|
||||
|
||||
let resp = {
|
||||
id: packet.id,
|
||||
method: METHOD_RESPONSE,
|
||||
};
|
||||
let respMessage = func(packet.message);
|
||||
if (respMessage instanceof Promise) {
|
||||
return respMessage.then((data) => {
|
||||
resp.message = data;
|
||||
e.source.postMessage(resp, e.origin);
|
||||
});
|
||||
} else if (respMessage) {
|
||||
resp.message = respMessage;
|
||||
}
|
||||
e.source.postMessage(resp, e.origin);
|
||||
});
|
||||
};
|
||||
|
||||
export {
|
||||
sendToBackground, receiveBackgroundMesssage,
|
||||
receivePageMessage,
|
||||
};
|
20
e2e/ambassador/src/content/scrolls.js
Normal file
20
e2e/ambassador/src/content/scrolls.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
const get = () => {
|
||||
let element = document.documentElement;
|
||||
return {
|
||||
xMax: element.scrollWidth - element.clientWidth,
|
||||
yMax: element.scrollHeight - element.clientHeight,
|
||||
x: element.scrollLeft,
|
||||
y: element.scrollTop,
|
||||
frameWidth: element.clientWidth,
|
||||
frameHeight: element.clientHeight,
|
||||
};
|
||||
};
|
||||
|
||||
const set = (x, y) => {
|
||||
let element = document.documentElement;
|
||||
element.scrollLeft = x;
|
||||
element.scrollTop = y;
|
||||
return get();
|
||||
};
|
||||
|
||||
export { get, set };
|
34
e2e/ambassador/src/shared/messages.js
Normal file
34
e2e/ambassador/src/shared/messages.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
const METHOD_REQUEST = 'request';
|
||||
const METHOD_RESPONSE = 'response';
|
||||
const WINDOWS_CREATE = 'windows.create';
|
||||
const WINDOWS_REMOVE = 'windows.remove';
|
||||
const WINDOWS_GET = 'windows.get';
|
||||
const TABS_CREATE = 'tabs.create';
|
||||
const TABS_SELECT_AT = 'tabs.selectAt';
|
||||
const TABS_GET_ZOOM = 'tabs.get.zoom';
|
||||
const TABS_SET_ZOOM = 'tabs.set.zoom';
|
||||
const EVENT_KEYPRESS = 'event.keypress';
|
||||
const EVENT_KEYDOWN = 'event.keydown';
|
||||
const EVENT_KEYUP = 'event.keyup';
|
||||
const SCROLL_GET = 'scroll.get';
|
||||
const SCROLL_SET = 'scroll.set';
|
||||
|
||||
export {
|
||||
METHOD_REQUEST,
|
||||
METHOD_RESPONSE,
|
||||
|
||||
WINDOWS_CREATE,
|
||||
WINDOWS_REMOVE,
|
||||
WINDOWS_GET,
|
||||
|
||||
TABS_CREATE,
|
||||
TABS_SELECT_AT,
|
||||
TABS_GET_ZOOM,
|
||||
TABS_SET_ZOOM,
|
||||
|
||||
EVENT_KEYPRESS,
|
||||
EVENT_KEYDOWN,
|
||||
EVENT_KEYUP,
|
||||
SCROLL_GET,
|
||||
SCROLL_SET,
|
||||
};
|
24
e2e/ambassador/webpack.config.js
Normal file
24
e2e/ambassador/webpack.config.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
const path = require('path');
|
||||
|
||||
const src = path.resolve(__dirname, 'src');
|
||||
const dist = path.resolve(__dirname, 'build');
|
||||
|
||||
config = {
|
||||
entry: {
|
||||
content: path.join(src, 'content'),
|
||||
background: path.join(src, 'background')
|
||||
},
|
||||
|
||||
output: {
|
||||
path: dist,
|
||||
filename: '[name].js'
|
||||
},
|
||||
|
||||
resolve: {
|
||||
extensions: [ '.js' ],
|
||||
modules: [path.join(__dirname, 'src'), 'node_modules']
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = config
|
||||
|
151
e2e/contents/scroll.test.js
Normal file
151
e2e/contents/scroll.test.js
Normal file
|
@ -0,0 +1,151 @@
|
|||
import { expect } from "chai";
|
||||
import * as windows from "../ambassador/src/client/windows";
|
||||
import * as tabs from "../ambassador/src/client/tabs";
|
||||
import * as keys from "../ambassador/src/client/keys";
|
||||
import * as scrolls from "../ambassador/src/client/scrolls";
|
||||
|
||||
const SERVER_URL = "localhost:11111";
|
||||
|
||||
describe("scroll test", () => {
|
||||
let targetWindow;
|
||||
let targetTab;
|
||||
|
||||
before(() => {
|
||||
return windows.create().then((win) => {
|
||||
targetWindow = win;
|
||||
return tabs.create(targetWindow.id, SERVER_URL);
|
||||
}).then((tab) => {
|
||||
targetTab = tab;
|
||||
});
|
||||
});
|
||||
|
||||
after(() => {
|
||||
return windows.remove(targetWindow.id);
|
||||
});
|
||||
|
||||
it('scrolls up by k', () => {
|
||||
let before
|
||||
return scrolls.set(targetTab.id, 100, 100).then((scroll) => {
|
||||
before = scroll;
|
||||
return keys.press(targetTab.id, 'k');
|
||||
}).then(() => {
|
||||
return scrolls.get(targetTab.id);
|
||||
}).then((actual) => {
|
||||
expect(actual.y).to.be.lessThan(before.y);
|
||||
});
|
||||
});
|
||||
|
||||
it('scrolls down by j', () => {
|
||||
let before
|
||||
return scrolls.set(targetTab.id, 100, 100).then((scroll) => {
|
||||
before = scroll;
|
||||
return keys.press(targetTab.id, 'j');
|
||||
}).then(() => {
|
||||
return scrolls.get(targetTab.id);
|
||||
}).then((actual) => {
|
||||
expect(actual.y).to.be.greaterThan(before.y);
|
||||
});
|
||||
});
|
||||
|
||||
it('scrolls left by h', () => {
|
||||
let before
|
||||
return scrolls.set(targetTab.id, 100, 100).then((scroll) => {
|
||||
before = scroll;
|
||||
return keys.press(targetTab.id, 'h');
|
||||
}).then(() => {
|
||||
return scrolls.get(targetTab.id);
|
||||
}).then((actual) => {
|
||||
expect(actual.x).to.be.lessThan(before.x);
|
||||
});
|
||||
});
|
||||
|
||||
it('scrolls top by gg', () => {
|
||||
return scrolls.set(targetTab.id, 100, 100).then((scroll) => {
|
||||
return keys.press(targetTab.id, 'g');
|
||||
}).then(() => {
|
||||
return keys.press(targetTab.id, 'g');
|
||||
}).then(() => {
|
||||
return scrolls.get(targetTab.id);
|
||||
}).then((actual) => {
|
||||
expect(actual.y).to.be.equals(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('scrolls bottom by G', () => {
|
||||
return scrolls.set(targetTab.id, 100, 100).then((scroll) => {
|
||||
return keys.press(targetTab.id, 'G', { shiftKey: true });
|
||||
}).then(() => {
|
||||
return scrolls.get(targetTab.id);
|
||||
}).then((actual) => {
|
||||
expect(actual.y).to.be.equals(actual.yMax);
|
||||
});
|
||||
});
|
||||
|
||||
it('scrolls bottom by 0', () => {
|
||||
return scrolls.set(targetTab.id, 100, 100).then((scroll) => {
|
||||
return keys.press(targetTab.id, '0');
|
||||
}).then(() => {
|
||||
return scrolls.get(targetTab.id);
|
||||
}).then((actual) => {
|
||||
expect(actual.x).to.be.equals(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('scrolls bottom by $', () => {
|
||||
return scrolls.set(targetTab.id, 100, 100).then((scroll) => {
|
||||
return keys.press(targetTab.id, '$');
|
||||
}).then(() => {
|
||||
return scrolls.get(targetTab.id);
|
||||
}).then((actual) => {
|
||||
expect(actual.x).to.be.equals(actual.xMax);
|
||||
});
|
||||
});
|
||||
|
||||
it('scrolls bottom by <C-U>', () => {
|
||||
let before
|
||||
return scrolls.set(targetTab.id, 5000, 5000).then((scroll) => {
|
||||
before = scroll;
|
||||
return keys.press(targetTab.id, 'u', { ctrlKey: true });
|
||||
}).then(() => {
|
||||
return scrolls.get(targetTab.id);
|
||||
}).then((actual) => {
|
||||
expect(actual.y).to.closeTo(before.y - before.frameHeight / 2, 1);
|
||||
});
|
||||
});
|
||||
|
||||
it('scrolls bottom by <C-D>', () => {
|
||||
let before
|
||||
return scrolls.set(targetTab.id, 5000, 5000).then((scroll) => {
|
||||
before = scroll;
|
||||
return keys.press(targetTab.id, 'd', { ctrlKey: true });
|
||||
}).then(() => {
|
||||
return scrolls.get(targetTab.id);
|
||||
}).then((actual) => {
|
||||
expect(actual.y).to.closeTo(before.y + before.frameHeight / 2, 1);
|
||||
});
|
||||
});
|
||||
|
||||
it('scrolls bottom by <C-B>', () => {
|
||||
let before
|
||||
return scrolls.set(targetTab.id, 5000, 5000).then((scroll) => {
|
||||
before = scroll;
|
||||
return keys.press(targetTab.id, 'b', { ctrlKey: true });
|
||||
}).then(() => {
|
||||
return scrolls.get(targetTab.id);
|
||||
}).then((actual) => {
|
||||
expect(actual.y).to.equals(before.y - before.frameHeight);
|
||||
});
|
||||
});
|
||||
|
||||
it('scrolls bottom by <C-F>', () => {
|
||||
let before
|
||||
return scrolls.set(targetTab.id, 5000, 5000).then((scroll) => {
|
||||
before = scroll;
|
||||
return keys.press(targetTab.id, 'f', { ctrlKey: true });
|
||||
}).then(() => {
|
||||
return scrolls.get(targetTab.id);
|
||||
}).then((actual) => {
|
||||
expect(actual.y).to.equals(before.y + before.frameHeight);
|
||||
});
|
||||
});
|
||||
});
|
216
e2e/contents/tab.test.js
Normal file
216
e2e/contents/tab.test.js
Normal file
|
@ -0,0 +1,216 @@
|
|||
import { expect } from "chai";
|
||||
import * as windows from "../ambassador/src/client/windows";
|
||||
import * as tabs from "../ambassador/src/client/tabs";
|
||||
import * as keys from "../ambassador/src/client/keys";
|
||||
|
||||
const SERVER_URL = "localhost:11111/";
|
||||
|
||||
describe("tab test", () => {
|
||||
let targetWindow;
|
||||
|
||||
beforeEach(() => {
|
||||
return windows.create(SERVER_URL).then((win) => {
|
||||
targetWindow = win;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
return windows.remove(targetWindow.id);
|
||||
});
|
||||
|
||||
it('deletes tab by d', () => {
|
||||
let before;
|
||||
let targetTab;
|
||||
return tabs.create(targetWindow.id, SERVER_URL).then((tab) => {
|
||||
targetTab = tab;
|
||||
return windows.get(targetWindow.id);
|
||||
}).then((win) => {
|
||||
before = win;
|
||||
return keys.press(targetTab.id, 'd');
|
||||
}).then(() => {
|
||||
return windows.get(targetWindow.id);
|
||||
}).then((actual) => {
|
||||
expect(actual.tabs).to.have.lengthOf(before.tabs.length - 1);
|
||||
});
|
||||
});
|
||||
|
||||
it('duplicates tab by zd', () => {
|
||||
let before;
|
||||
let targetTab;
|
||||
return tabs.create(targetWindow.id, SERVER_URL).then((tab) => {
|
||||
targetTab = tab;
|
||||
return windows.get(targetWindow.id)
|
||||
}).then((win) => {;
|
||||
before = win;
|
||||
return keys.press(targetTab.id, 'z');
|
||||
}).then(() => {
|
||||
return keys.press(targetTab.id, 'd');
|
||||
}).then(() => {
|
||||
return windows.get(targetWindow.id);
|
||||
}).then((actual) => {
|
||||
expect(actual.tabs).to.have.lengthOf(before.tabs.length + 1);
|
||||
});
|
||||
})
|
||||
|
||||
it('makes pinned by zp', () => {
|
||||
let before;
|
||||
let targetTab;
|
||||
return tabs.create(targetWindow.id, SERVER_URL).then((tab) => {
|
||||
targetTab = tab;
|
||||
return windows.get(targetWindow.id)
|
||||
}).then((win) => {;
|
||||
before = win;
|
||||
return keys.press(targetTab.id, 'z');
|
||||
}).then(() => {
|
||||
return keys.press(targetTab.id, 'p');
|
||||
}).then(() => {
|
||||
return windows.get(targetWindow.id);
|
||||
}).then((actual) => {
|
||||
expect(actual.tabs[0].pinned).to.be.true;
|
||||
});
|
||||
})
|
||||
|
||||
it('selects previous tab by K', () => {
|
||||
return Promise.resolve().then(() => {
|
||||
return tabs.create(targetWindow.id, SERVER_URL + '#1')
|
||||
}).then(() => {
|
||||
return tabs.create(targetWindow.id, SERVER_URL + '#2')
|
||||
}).then(() => {
|
||||
return tabs.create(targetWindow.id, SERVER_URL + '#3');
|
||||
}).then(() => {
|
||||
return tabs.selectAt(targetWindow.id, 2);
|
||||
}).then((tab) => {
|
||||
return keys.press(tab.id, 'K', { shiftKey: true });
|
||||
}).then(() => {
|
||||
return windows.get(targetWindow.id);
|
||||
}).then((win) => {
|
||||
expect(win.tabs[1].active).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
it('selects previous tab by K rotatory', () => {
|
||||
return Promise.resolve().then(() => {
|
||||
return tabs.create(targetWindow.id, SERVER_URL + '#1')
|
||||
}).then(() => {
|
||||
return tabs.create(targetWindow.id, SERVER_URL + '#2')
|
||||
}).then(() => {
|
||||
return tabs.create(targetWindow.id, SERVER_URL + '#3');
|
||||
}).then(() => {
|
||||
return tabs.selectAt(targetWindow.id, 0);
|
||||
}).then((tab) => {
|
||||
return keys.press(tab.id, 'K', { shiftKey: true });
|
||||
}).then(() => {
|
||||
return windows.get(targetWindow.id);
|
||||
}).then((win) => {
|
||||
expect(win.tabs[3].active).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
it('selects next tab by J', () => {
|
||||
return Promise.resolve().then(() => {
|
||||
return tabs.create(targetWindow.id, SERVER_URL + '#1')
|
||||
}).then(() => {
|
||||
return tabs.create(targetWindow.id, SERVER_URL + '#2')
|
||||
}).then(() => {
|
||||
return tabs.create(targetWindow.id, SERVER_URL + '#3');
|
||||
}).then(() => {
|
||||
return tabs.selectAt(targetWindow.id, 2);
|
||||
}).then((tab) => {
|
||||
return keys.press(tab.id, 'J', { shiftKey: true });
|
||||
}).then(() => {
|
||||
return windows.get(targetWindow.id);
|
||||
}).then((win) => {
|
||||
expect(win.tabs[3].active).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
it('selects previous tab by J rotatory', () => {
|
||||
return Promise.resolve().then(() => {
|
||||
return tabs.create(targetWindow.id, SERVER_URL + '#1')
|
||||
}).then(() => {
|
||||
return tabs.create(targetWindow.id, SERVER_URL + '#2')
|
||||
}).then(() => {
|
||||
return tabs.create(targetWindow.id, SERVER_URL + '#3');
|
||||
}).then(() => {
|
||||
return tabs.selectAt(targetWindow.id, 3);
|
||||
}).then((tab) => {
|
||||
return keys.press(tab.id, 'J', { shiftKey: true });
|
||||
}).then(() => {
|
||||
return windows.get(targetWindow.id);
|
||||
}).then((win) => {
|
||||
expect(win.tabs[0].active).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
it('selects first tab by g0', () => {
|
||||
return Promise.resolve().then(() => {
|
||||
return tabs.create(targetWindow.id, SERVER_URL + '#1')
|
||||
}).then(() => {
|
||||
return tabs.create(targetWindow.id, SERVER_URL + '#2')
|
||||
}).then(() => {
|
||||
return tabs.create(targetWindow.id, SERVER_URL + '#3');
|
||||
}).then(() => {
|
||||
return tabs.selectAt(targetWindow.id, 2);
|
||||
}).then((tab) => {
|
||||
return keys.press(tab.id, 'g').then(() => tab);
|
||||
}).then((tab) => {
|
||||
return keys.press(tab.id, '0');
|
||||
}).then(() => {
|
||||
return windows.get(targetWindow.id);
|
||||
}).then((win) => {
|
||||
expect(win.tabs[0].active).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
it('selects last tab by g$', () => {
|
||||
return Promise.resolve().then(() => {
|
||||
return tabs.create(targetWindow.id, SERVER_URL + '#1')
|
||||
}).then(() => {
|
||||
return tabs.create(targetWindow.id, SERVER_URL + '#2')
|
||||
}).then(() => {
|
||||
return tabs.create(targetWindow.id, SERVER_URL + '#3');
|
||||
}).then(() => {
|
||||
return tabs.selectAt(targetWindow.id, 2);
|
||||
}).then((tab) => {
|
||||
return keys.press(tab.id, 'g').then(() => tab);
|
||||
}).then((tab) => {
|
||||
return keys.press(tab.id, '$');
|
||||
}).then(() => {
|
||||
return windows.get(targetWindow.id);
|
||||
}).then((win) => {
|
||||
expect(win.tabs[3].active).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
it('selects last selected tab by <C-6>', () => {
|
||||
return Promise.resolve().then(() => {
|
||||
return tabs.create(targetWindow.id, SERVER_URL + '#1')
|
||||
}).then(() => {
|
||||
return tabs.create(targetWindow.id, SERVER_URL + '#2')
|
||||
}).then(() => {
|
||||
return tabs.create(targetWindow.id, SERVER_URL + '#3');
|
||||
}).then(() => {
|
||||
return tabs.selectAt(targetWindow.id, 1);
|
||||
}).then(() => {
|
||||
return tabs.selectAt(targetWindow.id, 3);
|
||||
}).then((tab) => {
|
||||
return keys.press(tab.id, '6', { ctrlKey: true });
|
||||
}).then(() => {
|
||||
return windows.get(targetWindow.id);
|
||||
}).then((win) => {
|
||||
expect(win.tabs[1].active).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
it('deletes tab by d', () => {
|
||||
return Promise.resolve().then(() => {
|
||||
return tabs.create(targetWindow.id, SERVER_URL + '#1');
|
||||
}).then((tab) => {
|
||||
return keys.press(tab.id, 'd');
|
||||
}).then(() => {
|
||||
return windows.get(targetWindow.id);
|
||||
}).then((win) => {
|
||||
expect(win.tabs).to.have.lengthOf(1);
|
||||
});
|
||||
});
|
||||
});
|
72
e2e/contents/zoom.test.js
Normal file
72
e2e/contents/zoom.test.js
Normal file
|
@ -0,0 +1,72 @@
|
|||
import { expect } from "chai";
|
||||
import * as windows from "../ambassador/src/client/windows";
|
||||
import * as tabs from "../ambassador/src/client/tabs";
|
||||
import * as keys from "../ambassador/src/client/keys";
|
||||
|
||||
const SERVER_URL = "localhost:11111/";
|
||||
|
||||
describe("zoom test", () => {
|
||||
let targetWindow;
|
||||
let targetTab;
|
||||
|
||||
before(() => {
|
||||
return windows.create(SERVER_URL).then((win) => {
|
||||
targetWindow = win;
|
||||
});
|
||||
});
|
||||
|
||||
after(() => {
|
||||
return windows.remove(targetWindow.id);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
return tabs.create(targetWindow.id, SERVER_URL).then((tab) => {
|
||||
targetTab = tab;
|
||||
});
|
||||
});
|
||||
|
||||
it('zooms-in by zi', () => {
|
||||
let before;
|
||||
return tabs.getZoom(targetTab.id).then((zoom) => {
|
||||
before = zoom;
|
||||
return keys.press(targetTab.id, 'z');
|
||||
}).then(() => {
|
||||
return keys.press(targetTab.id, 'i');
|
||||
}).then(() => {
|
||||
return tabs.getZoom(targetTab.id);
|
||||
}).then((actual) => {
|
||||
expect(actual).to.be.greaterThan(before);
|
||||
});
|
||||
});
|
||||
|
||||
it('zooms-in by zo', () => {
|
||||
let before;
|
||||
return tabs.getZoom(targetTab.id).then((zoom) => {
|
||||
before = zoom;
|
||||
return keys.press(targetTab.id, 'z');
|
||||
}).then(() => {
|
||||
return keys.press(targetTab.id, 'o');
|
||||
}).then(() => {
|
||||
return tabs.getZoom(targetTab.id);
|
||||
}).then((actual) => {
|
||||
expect(actual).to.be.lessThan(before);
|
||||
});
|
||||
});
|
||||
|
||||
it('zooms-in by zz', () => {
|
||||
let before;
|
||||
tabs.setZoom(targetTab.id, 1.5).then(() => {
|
||||
return tabs.getZoom(targetTab.id);
|
||||
}).then((zoom) => {
|
||||
before = zoom;
|
||||
return keys.press(targetTab.id, 'z');
|
||||
}).then(() => {
|
||||
return keys.press(targetTab.id, 'z');
|
||||
}).then(() => {
|
||||
return tabs.getZoom(targetTab.id);
|
||||
}).then((actual) => {
|
||||
expect(actual).to.be.lessThan(before);
|
||||
expect(actual).to.be.be(1);
|
||||
});
|
||||
});
|
||||
});
|
10
e2e/karma-delay.js
Normal file
10
e2e/karma-delay.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
'use strict';
|
||||
|
||||
window.__karma__.start = (function(start){
|
||||
return function(){
|
||||
var args = arguments
|
||||
setTimeout(() => {
|
||||
start(args)
|
||||
}, 3000);
|
||||
};
|
||||
}(window.__karma__.start));
|
53
e2e/karma-webext-launcher.js
Normal file
53
e2e/karma-webext-launcher.js
Normal file
|
@ -0,0 +1,53 @@
|
|||
'use strict'
|
||||
|
||||
var fs = require('fs')
|
||||
var path = require('path')
|
||||
|
||||
var PREFS = {
|
||||
'browser.shell.checkDefaultBrowser': 'false',
|
||||
'browser.bookmarks.restore_default_bookmarks': 'false',
|
||||
'dom.disable_open_during_load': 'false',
|
||||
'dom.max_script_run_time': '0',
|
||||
'dom.min_background_timeout_value': '10',
|
||||
'extensions.autoDisableScopes': '0',
|
||||
'extensions.enabledScopes': '15',
|
||||
}
|
||||
|
||||
var FirefoxWebExt = function (id, baseBrowserDecorator, args) {
|
||||
baseBrowserDecorator(this)
|
||||
|
||||
this._start = function (url) {
|
||||
var self = this
|
||||
var command = this._getCommand()
|
||||
|
||||
let prefArgs = [].concat(...Object.keys(PREFS).map((key) => {
|
||||
return ['--pref', key + '=' + PREFS[key]];
|
||||
}));
|
||||
let sourceDirArgs = [].concat(...args.sourceDirs.map((dir) => {
|
||||
return ['--source-dir', dir];
|
||||
}));
|
||||
|
||||
self._execCommand(
|
||||
command,
|
||||
['run', '--start-url', url, '--no-input'].concat(sourceDirArgs, prefArgs)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FirefoxWebExt.prototype = {
|
||||
name: 'FirefoxWebExt',
|
||||
|
||||
DEFAULT_CMD: {
|
||||
linux: 'node_modules/web-ext/bin/web-ext',
|
||||
darwin: 'node_modules/web-ext/bin/web-ext',
|
||||
win32: 'node_modules/web-ext/bin/web-ext',
|
||||
}
|
||||
}
|
||||
|
||||
FirefoxWebExt.$inject = ['id', 'baseBrowserDecorator', 'args']
|
||||
|
||||
// PUBLISH DI MODULE
|
||||
module.exports = {
|
||||
'launcher:FirefoxWebExt': ['type', FirefoxWebExt],
|
||||
}
|
||||
|
51
e2e/karma.conf.js
Normal file
51
e2e/karma.conf.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
module.exports = function (config) {
|
||||
|
||||
config.set({
|
||||
basePath: '',
|
||||
frameworks: ['mocha'],
|
||||
files: [
|
||||
'karma-delay.js',
|
||||
'**/*.test.js'
|
||||
],
|
||||
|
||||
preprocessors: {
|
||||
'**/*.test.js': ['webpack']
|
||||
},
|
||||
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
|
||||
customLaunchers: {
|
||||
FirefoxWebExtRunner: {
|
||||
base: 'FirefoxWebExt',
|
||||
sourceDirs: [ '.', 'e2e/ambassador'],
|
||||
},
|
||||
},
|
||||
browsers: ['FirefoxWebExtRunner'],
|
||||
sauceLabs: {
|
||||
username: 'michael_jackson'
|
||||
},
|
||||
|
||||
singleRun: true,
|
||||
|
||||
webpackMiddleware: {
|
||||
noInfo: true
|
||||
},
|
||||
|
||||
reporters: ['mocha'],
|
||||
|
||||
plugins: [
|
||||
require('./karma-webext-launcher'),
|
||||
'karma-mocha',
|
||||
'karma-webpack',
|
||||
'karma-mocha-reporter',
|
||||
],
|
||||
|
||||
client: {
|
||||
mocha: {
|
||||
timeout: 5000
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
14
e2e/web-server/index.js
Normal file
14
e2e/web-server/index.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
var http = require('http');
|
||||
|
||||
const content =
|
||||
'<!DOCTYPE html>' +
|
||||
'<html lang="en">' +
|
||||
'<body style="width:10000px; height:10000px">' +
|
||||
'</body>' +
|
||||
'</html">' ;
|
||||
|
||||
|
||||
http.createServer(function (req, res) {
|
||||
res.writeHead(200, {'Content-Type': 'text/html'});
|
||||
res.end(content);
|
||||
}).listen(11111, '127.0.0.1');
|
|
@ -7,11 +7,13 @@ module.exports = function (config) {
|
|||
frameworks: ['mocha'],
|
||||
files: [
|
||||
'test/**/*.test.js',
|
||||
'test/**/*.test.jsx',
|
||||
'test/**/*.html'
|
||||
],
|
||||
|
||||
preprocessors: {
|
||||
'test/**/*.test.js': [ 'webpack' ],
|
||||
'test/**/*.test.jsx': [ 'webpack' ],
|
||||
'test/**/*.html': ['html2js']
|
||||
},
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"manifest_version": 2,
|
||||
"name": "Vim Vixen",
|
||||
"description": "Vim Vixen",
|
||||
"version": "0.5",
|
||||
"version": "0.12",
|
||||
"icons": {
|
||||
"48": "resources/icon_48x48.png",
|
||||
"96": "resources/icon_96x96.png"
|
||||
|
@ -30,7 +30,8 @@
|
|||
"history",
|
||||
"sessions",
|
||||
"storage",
|
||||
"tabs"
|
||||
"tabs",
|
||||
"clipboardRead"
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
"build/console.html",
|
||||
|
|
11980
package-lock.json
generated
11980
package-lock.json
generated
File diff suppressed because it is too large
Load diff
43
package.json
43
package.json
|
@ -2,12 +2,14 @@
|
|||
"name": "vim-vixen",
|
||||
"description": "Vim vixen",
|
||||
"scripts": {
|
||||
"start": "webpack -w --debug --devtool inline-source-map",
|
||||
"lint": "eslint --ext .jsx,.js src",
|
||||
"build": "NODE_ENV=production webpack --progress --display-error-details",
|
||||
"package": "npm run build && ./package.sh",
|
||||
"lint": "eslint src",
|
||||
"test": "karma start"
|
||||
"start": "webpack --mode development -w --debug --devtool inline-source-map",
|
||||
"build": "NODE_ENV=production webpack --mode production --progress --display-error-details",
|
||||
"package": "npm run build && script/package.sh",
|
||||
"lint": "eslint --ext .jsx,.js src e2e/ambassador/src",
|
||||
"test": "karma start",
|
||||
"test:e2e": "karma start e2e/karma.conf.js",
|
||||
"ambassador:start": "webpack --mode production -w --debug --context e2e/ambassador --config e2e/ambassador/webpack.config.js --devtool inline-source-map",
|
||||
"ambassador:build": "webpack --mode production --context e2e/ambassador --config e2e/ambassador/webpack.config.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -20,31 +22,30 @@
|
|||
},
|
||||
"homepage": "https://github.com/ueokande/vim-vixen",
|
||||
"devDependencies": {
|
||||
"adm-zip": "^0.4.7",
|
||||
"babel-cli": "^6.24.1",
|
||||
"babel-eslint": "^7.2.3",
|
||||
"babel-eslint": "^8.0.2",
|
||||
"babel-loader": "^7.1.1",
|
||||
"babel-minify-webpack-plugin": "^0.2.0",
|
||||
"babel-plugin-transform-react-jsx": "^6.24.1",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"babel-preset-preact": "^1.1.0",
|
||||
"chai": "^4.1.1",
|
||||
"css-loader": "^0.28.4",
|
||||
"css-loader": "^0.28.10",
|
||||
"eslint": "^4.7.0",
|
||||
"eslint-plugin-react": "^7.4.0",
|
||||
"html-webpack-plugin": "^2.30.1",
|
||||
"karma": "^1.7.0",
|
||||
"html-webpack-plugin": "^3.0.6",
|
||||
"karma": "^2.0.0",
|
||||
"karma-firefox-launcher": "^1.0.1",
|
||||
"karma-html2js-preprocessor": "^1.1.0",
|
||||
"karma-mocha": "^1.3.0",
|
||||
"karma-mocha-reporter": "^2.2.3",
|
||||
"karma-sourcemap-loader": "^0.3.7",
|
||||
"karma-webpack": "^2.0.4",
|
||||
"mocha": "^3.5.0",
|
||||
"karma-webpack": "^3.0.0",
|
||||
"mocha": "^5.0.0",
|
||||
"node-sass": "^4.5.3",
|
||||
"react": "^16.0.0",
|
||||
"react-dom": "^16.0.0",
|
||||
"sass-loader": "^6.0.6",
|
||||
"style-loader": "^0.18.2",
|
||||
"webpack": "^3.5.3"
|
||||
"preact": "^8.2.6",
|
||||
"sass-loader": "^7.0.1",
|
||||
"style-loader": "^0.21.0",
|
||||
"web-ext": "github:ueokande/web-ext#patched-2.4.0",
|
||||
"webpack": "^4.1.1",
|
||||
"webpack-cli": "^2.0.11"
|
||||
}
|
||||
}
|
||||
|
|
55
script/package.js
Normal file
55
script/package.js
Normal file
|
@ -0,0 +1,55 @@
|
|||
let path = require('path');
|
||||
let fs = require('fs');
|
||||
let AdmZip = require('adm-zip');
|
||||
let manifest = require('../manifest');
|
||||
|
||||
manifest.iconFiles = function() {
|
||||
return Object.keys(this.icons).map(key => this.icons[key]);
|
||||
};
|
||||
|
||||
manifest.contentScriptFiles = function() {
|
||||
let files = this.content_scripts.map(entry => entry.js);
|
||||
return [].concat.apply([], files);
|
||||
};
|
||||
|
||||
manifest.backgroundScriptFiles = function() {
|
||||
return this.background.scripts;
|
||||
|
||||
};
|
||||
|
||||
manifest.webAccessibleResourceFiles = function() {
|
||||
return this.web_accessible_resources;
|
||||
};
|
||||
|
||||
manifest.optionFiles = function() {
|
||||
let uiFile = this.options_ui.page;
|
||||
let dir = path.dirname(uiFile);
|
||||
let html = fs.readFileSync(uiFile, 'utf-8');
|
||||
|
||||
let files = [uiFile];
|
||||
let regex = /<\s*script\s+src\s*=\s*'(.*)'\s*>/g;
|
||||
let match = regex.exec(html);
|
||||
while (match) {
|
||||
files.push(path.join(dir, match[1]));
|
||||
match = regex.exec(html);
|
||||
}
|
||||
return files;
|
||||
};
|
||||
|
||||
let files = []
|
||||
.concat('manifest.json')
|
||||
.concat(manifest.iconFiles())
|
||||
.concat(manifest.contentScriptFiles())
|
||||
.concat(manifest.backgroundScriptFiles())
|
||||
.concat(manifest.webAccessibleResourceFiles())
|
||||
.concat(manifest.optionFiles());
|
||||
let zip = new AdmZip();
|
||||
let output = `${manifest.version}.zip`;
|
||||
console.log(output);
|
||||
for (let f of files) {
|
||||
let dir = path.dirname(f);
|
||||
zip.addLocalFile(f, dir);
|
||||
console.log('=>', path.join(dir, f));
|
||||
}
|
||||
|
||||
zip.writeZip(output);
|
79
src/background/actions/command.js
Normal file
79
src/background/actions/command.js
Normal file
|
@ -0,0 +1,79 @@
|
|||
import actions from '../actions';
|
||||
import * as tabs from 'background/tabs';
|
||||
import * as parsers from 'shared/commands/parsers';
|
||||
import * as properties from 'shared/settings/properties';
|
||||
|
||||
const openCommand = (url) => {
|
||||
return browser.tabs.query({
|
||||
active: true, currentWindow: true
|
||||
}).then((gotTabs) => {
|
||||
if (gotTabs.length > 0) {
|
||||
return browser.tabs.update(gotTabs[0].id, { url: url });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const tabopenCommand = (url) => {
|
||||
return browser.tabs.create({ url: url });
|
||||
};
|
||||
|
||||
const winopenCommand = (url) => {
|
||||
return browser.windows.create({ url });
|
||||
};
|
||||
|
||||
const bufferCommand = (keywords) => {
|
||||
if (keywords.length === 0) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
let keywordsStr = keywords.join(' ');
|
||||
return browser.tabs.query({
|
||||
active: true, currentWindow: true
|
||||
}).then((gotTabs) => {
|
||||
if (gotTabs.length > 0) {
|
||||
if (isNaN(keywordsStr)) {
|
||||
return tabs.selectByKeyword(gotTabs[0], keywordsStr);
|
||||
}
|
||||
let index = parseInt(keywordsStr, 10) - 1;
|
||||
return tabs.selectAt(index);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const setCommand = (args) => {
|
||||
if (!args[0]) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
let [name, value] = parsers.parseSetOption(args[0], properties.types);
|
||||
return {
|
||||
type: actions.SETTING_SET_PROPERTY,
|
||||
name,
|
||||
value
|
||||
};
|
||||
};
|
||||
|
||||
const exec = (line, settings) => {
|
||||
let [name, args] = parsers.parseCommandLine(line);
|
||||
|
||||
switch (name) {
|
||||
case 'o':
|
||||
case 'open':
|
||||
return openCommand(parsers.normalizeUrl(args, settings.search));
|
||||
case 't':
|
||||
case 'tabopen':
|
||||
return tabopenCommand(parsers.normalizeUrl(args, settings.search));
|
||||
case 'w':
|
||||
case 'winopen':
|
||||
return winopenCommand(parsers.normalizeUrl(args, settings.search));
|
||||
case 'b':
|
||||
case 'buffer':
|
||||
return bufferCommand(args);
|
||||
case 'set':
|
||||
return setCommand(args);
|
||||
case '':
|
||||
return Promise.resolve();
|
||||
}
|
||||
throw new Error(name + ' command is not defined');
|
||||
};
|
||||
|
||||
export { exec };
|
10
src/background/actions/find.js
Normal file
10
src/background/actions/find.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
import actions from './index';
|
||||
|
||||
const setKeyword = (keyword) => {
|
||||
return {
|
||||
type: actions.FIND_SET_KEYWORD,
|
||||
keyword,
|
||||
};
|
||||
};
|
||||
|
||||
export { setKeyword };
|
8
src/background/actions/index.js
Normal file
8
src/background/actions/index.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
export default {
|
||||
// Settings
|
||||
SETTING_SET_SETTINGS: 'setting.set.settings',
|
||||
SETTING_SET_PROPERTY: 'setting.set.property',
|
||||
|
||||
// Find
|
||||
FIND_SET_KEYWORD: 'find.set.keyword',
|
||||
};
|
|
@ -17,6 +17,8 @@ const exec = (operation, tab) => {
|
|||
switch (operation.type) {
|
||||
case operations.TAB_CLOSE:
|
||||
return tabs.closeTab(tab.id);
|
||||
case operations.TAB_CLOSE_FORCE:
|
||||
return tabs.closeTabForce(tab.id);
|
||||
case operations.TAB_REOPEN:
|
||||
return tabs.reopenTab();
|
||||
case operations.TAB_PREV:
|
||||
|
@ -27,6 +29,8 @@ const exec = (operation, tab) => {
|
|||
return tabs.selectFirstTab();
|
||||
case operations.TAB_LAST:
|
||||
return tabs.selectLastTab();
|
||||
case operations.TAB_PREV_SEL:
|
||||
return tabs.selectPrevSelTab();
|
||||
case operations.TAB_RELOAD:
|
||||
return tabs.reload(tab, operation.cache);
|
||||
case operations.TAB_PIN:
|
||||
|
@ -69,6 +73,10 @@ const exec = (operation, tab) => {
|
|||
return browser.tabs.sendMessage(tab.id, {
|
||||
type: messages.CONSOLE_SHOW_FIND
|
||||
});
|
||||
case operations.CANCEL:
|
||||
return browser.tabs.sendMessage(tab.id, {
|
||||
type: messages.CONSOLE_HIDE,
|
||||
});
|
||||
default:
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
|
21
src/background/actions/setting.js
Normal file
21
src/background/actions/setting.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
import actions from '../actions';
|
||||
import * as settingsStorage from 'shared/settings/storage';
|
||||
|
||||
const load = () => {
|
||||
return settingsStorage.loadValue().then((value) => {
|
||||
return {
|
||||
type: actions.SETTING_SET_SETTINGS,
|
||||
value,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const setProperty = (name, value) => {
|
||||
return {
|
||||
type: actions.SETTING_SET_PROPERTY,
|
||||
name,
|
||||
value,
|
||||
};
|
||||
};
|
||||
|
||||
export { load, setProperty };
|
|
@ -1,9 +1,21 @@
|
|||
const openNewTab = (url) => {
|
||||
return browser.tabs.create({ url: url });
|
||||
const openNewTab = (url, openerTabId, background = false, adjacent = false) => {
|
||||
if (adjacent) {
|
||||
return browser.tabs.query({
|
||||
active: true, currentWindow: true
|
||||
}).then((tabs) => {
|
||||
return browser.tabs.create({
|
||||
url,
|
||||
openerTabId,
|
||||
active: !background,
|
||||
index: tabs[0].index + 1
|
||||
});
|
||||
});
|
||||
}
|
||||
return browser.tabs.create({ url, active: !background });
|
||||
};
|
||||
|
||||
const openToTab = (url, tab) => {
|
||||
return browser.tabs.update(tab.id, { url: url });
|
||||
};
|
||||
|
||||
export { openToTab, openNewTab };
|
||||
export { openNewTab, openToTab };
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import messages from 'shared/messages';
|
||||
import * as operationActions from 'background/actions/operation';
|
||||
import * as settingsActions from 'settings/actions/setting';
|
||||
import * as commandActions from 'background/actions/command';
|
||||
import * as settingActions from 'background/actions/setting';
|
||||
import * as findActions from 'background/actions/find';
|
||||
import * as tabActions from 'background/actions/tab';
|
||||
import * as commands from 'shared/commands';
|
||||
|
||||
|
@ -22,6 +24,8 @@ export default class BackgroundComponent {
|
|||
|
||||
onMessage(message, sender) {
|
||||
let settings = this.store.getState().setting;
|
||||
let find = this.store.getState().find;
|
||||
|
||||
switch (message.type) {
|
||||
case messages.BACKGROUND_OPERATION:
|
||||
return this.store.dispatch(
|
||||
|
@ -29,25 +33,31 @@ export default class BackgroundComponent {
|
|||
sender);
|
||||
case messages.OPEN_URL:
|
||||
if (message.newTab) {
|
||||
return this.store.dispatch(
|
||||
tabActions.openNewTab(message.url), sender);
|
||||
let action = tabActions.openNewTab(
|
||||
message.url, sender.tab.id, message.background,
|
||||
settings.value.properties.adjacenttab);
|
||||
return this.store.dispatch(action, sender);
|
||||
}
|
||||
return this.store.dispatch(
|
||||
tabActions.openToTab(message.url, sender.tab), sender);
|
||||
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,
|
||||
text: e.message,
|
||||
});
|
||||
});
|
||||
this.store.dispatch(
|
||||
commandActions.exec(message.text, settings.value),
|
||||
sender
|
||||
);
|
||||
return this.broadcastSettingsChanged();
|
||||
case messages.SETTINGS_QUERY:
|
||||
return Promise.resolve(this.store.getState().setting.value);
|
||||
case messages.CONSOLE_QUERY_COMPLETIONS:
|
||||
return commands.complete(message.text, settings.value);
|
||||
case messages.SETTINGS_RELOAD:
|
||||
this.store.dispatch(settingsActions.load());
|
||||
this.store.dispatch(settingActions.load());
|
||||
return this.broadcastSettingsChanged();
|
||||
case messages.FIND_GET_KEYWORD:
|
||||
return Promise.resolve(find.keyword);
|
||||
case messages.FIND_SET_KEYWORD:
|
||||
this.store.dispatch(findActions.setKeyword(message.keyword));
|
||||
return Promise.resolve({});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -76,7 +76,6 @@ const getCompletions = (keyword) => {
|
|||
.sort((x, y) => x[0].visitCount < y[0].visitCount)
|
||||
.slice(0, 10)
|
||||
.map(item => item[0])
|
||||
.sort((x, y) => x.url > y.url)
|
||||
)[0];
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import * as settingsActions from 'settings/actions/setting';
|
||||
import * as settingActions from 'background/actions/setting';
|
||||
import messages from 'shared/messages';
|
||||
import BackgroundComponent from 'background/components/background';
|
||||
import reducers from 'background/reducers';
|
||||
|
@ -16,4 +16,4 @@ const store = createStore(reducers, (e, sender) => {
|
|||
// eslint-disable-next-line no-unused-vars
|
||||
const backgroundComponent = new BackgroundComponent(store);
|
||||
|
||||
store.dispatch(settingsActions.load());
|
||||
store.dispatch(settingActions.load());
|
||||
|
|
16
src/background/reducers/find.js
Normal file
16
src/background/reducers/find.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
import actions from 'content/actions';
|
||||
|
||||
const defaultState = {
|
||||
keyword: null,
|
||||
};
|
||||
|
||||
export default function reducer(state = defaultState, action = {}) {
|
||||
switch (action.type) {
|
||||
case actions.FIND_SET_KEYWORD:
|
||||
return Object.assign({}, state, {
|
||||
keyword: action.keyword,
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -1,12 +1,15 @@
|
|||
import settingReducer from 'settings/reducers/setting';
|
||||
import settingReducer from './setting';
|
||||
import findReducer from './find';
|
||||
|
||||
// Make setting reducer instead of re-use
|
||||
const defaultState = {
|
||||
setting: settingReducer(undefined, {}),
|
||||
find: findReducer(undefined, {}),
|
||||
};
|
||||
|
||||
export default function reducer(state = defaultState, action = {}) {
|
||||
return Object.assign({}, state, {
|
||||
setting: settingReducer(state.setting, action),
|
||||
find: findReducer(state.find, action),
|
||||
});
|
||||
}
|
||||
|
|
24
src/background/reducers/setting.js
Normal file
24
src/background/reducers/setting.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import actions from 'background/actions';
|
||||
|
||||
const defaultState = {
|
||||
value: {},
|
||||
};
|
||||
|
||||
export default function reducer(state = defaultState, action = {}) {
|
||||
switch (action.type) {
|
||||
case actions.SETTING_SET_SETTINGS:
|
||||
return {
|
||||
value: action.value,
|
||||
};
|
||||
case actions.SETTING_SET_PROPERTY:
|
||||
return {
|
||||
value: Object.assign({}, state.value, {
|
||||
properties: Object.assign({}, state.value.properties,
|
||||
{ [action.name]: action.value })
|
||||
})
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,22 @@
|
|||
let prevSelTab = 1;
|
||||
let currSelTab = 1;
|
||||
|
||||
browser.tabs.onActivated.addListener((activeInfo) => {
|
||||
return browser.tabs.query({ currentWindow: true }).then(() => {
|
||||
prevSelTab = currSelTab;
|
||||
currSelTab = activeInfo.tabId;
|
||||
});
|
||||
});
|
||||
|
||||
const closeTab = (id) => {
|
||||
return browser.tabs.get(id).then((tab) => {
|
||||
if (!tab.pinned) {
|
||||
return browser.tabs.remove(id);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const closeTabForce = (id) => {
|
||||
return browser.tabs.remove(id);
|
||||
};
|
||||
|
||||
|
@ -51,7 +69,7 @@ const selectByKeyword = (current, keyword) => {
|
|||
const getCompletions = (keyword) => {
|
||||
return browser.tabs.query({ currentWindow: true }).then((tabs) => {
|
||||
let matched = tabs.filter((t) => {
|
||||
return t.url.includes(keyword) || t.title.includes(keyword);
|
||||
return t.url.includes(keyword) || t.title && t.title.includes(keyword);
|
||||
});
|
||||
return matched;
|
||||
});
|
||||
|
@ -93,6 +111,10 @@ const selectLastTab = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const selectPrevSelTab = () => {
|
||||
return browser.tabs.update(prevSelTab, { active: true });
|
||||
};
|
||||
|
||||
const reload = (current, cache) => {
|
||||
return browser.tabs.reload(
|
||||
current.id,
|
||||
|
@ -116,7 +138,8 @@ const duplicate = (id) => {
|
|||
};
|
||||
|
||||
export {
|
||||
closeTab, reopenTab, selectAt, selectByKeyword, getCompletions,
|
||||
selectPrevTab, selectNextTab, selectFirstTab, selectLastTab, reload,
|
||||
updateTabPinned, toggleTabPinned, duplicate
|
||||
closeTab, closeTabForce, reopenTab, selectAt, selectByKeyword,
|
||||
getCompletions, selectPrevTab, selectNextTab, selectFirstTab,
|
||||
selectLastTab, selectPrevSelTab, reload, updateTabPinned,
|
||||
toggleTabPinned, duplicate
|
||||
};
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
import actions from 'console/actions';
|
||||
|
||||
const hide = () => {
|
||||
return {
|
||||
type: actions.CONSOLE_HIDE,
|
||||
};
|
||||
};
|
||||
|
||||
const showCommand = (text) => {
|
||||
return {
|
||||
type: actions.CONSOLE_SHOW_COMMAND,
|
||||
|
@ -61,6 +67,6 @@ const completionPrev = () => {
|
|||
};
|
||||
|
||||
export {
|
||||
showCommand, showFind, showError, showInfo, hideCommand, setConsoleText,
|
||||
hide, showCommand, showFind, showError, showInfo, hideCommand, setConsoleText,
|
||||
setCompletions, completionNext, completionPrev
|
||||
};
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
export default {
|
||||
// console commands
|
||||
CONSOLE_HIDE: 'console.hide',
|
||||
CONSOLE_SHOW_COMMAND: 'console.show.command',
|
||||
CONSOLE_SHOW_ERROR: 'console.show.error',
|
||||
CONSOLE_SHOW_INFO: 'console.show.info',
|
||||
|
|
|
@ -31,14 +31,33 @@ export default class ConsoleComponent {
|
|||
}
|
||||
}
|
||||
|
||||
doEnter(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
return this.onEntered(e.target.value);
|
||||
}
|
||||
|
||||
selectNext(e) {
|
||||
this.store.dispatch(consoleActions.completionNext());
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
selectPrev(e) {
|
||||
this.store.dispatch(consoleActions.completionPrev());
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
onKeyDown(e) {
|
||||
if (e.keyCode === KeyboardEvent.DOM_VK_ESCAPE && e.ctrlKey) {
|
||||
return this.hideCommand();
|
||||
}
|
||||
switch (e.keyCode) {
|
||||
case KeyboardEvent.DOM_VK_ESCAPE:
|
||||
return this.hideCommand();
|
||||
case KeyboardEvent.DOM_VK_RETURN:
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
return this.onEntered(e.target.value);
|
||||
return this.doEnter(e);
|
||||
case KeyboardEvent.DOM_VK_TAB:
|
||||
if (e.shiftKey) {
|
||||
this.store.dispatch(consoleActions.completionPrev());
|
||||
|
@ -48,6 +67,26 @@ export default class ConsoleComponent {
|
|||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
break;
|
||||
case KeyboardEvent.DOM_VK_OPEN_BRACKET:
|
||||
if (e.ctrlKey) {
|
||||
return this.hideCommand();
|
||||
}
|
||||
break;
|
||||
case KeyboardEvent.DOM_VK_M:
|
||||
if (e.ctrlKey) {
|
||||
return this.doEnter(e);
|
||||
}
|
||||
break;
|
||||
case KeyboardEvent.DOM_VK_N:
|
||||
if (e.ctrlKey) {
|
||||
this.selectNext(e);
|
||||
}
|
||||
break;
|
||||
case KeyboardEvent.DOM_VK_P:
|
||||
if (e.ctrlKey) {
|
||||
this.selectPrev(e);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,8 @@ const onMessage = (message) => {
|
|||
return store.dispatch(consoleActions.showError(message.text));
|
||||
case messages.CONSOLE_SHOW_INFO:
|
||||
return store.dispatch(consoleActions.showInfo(message.text));
|
||||
case messages.CONSOLE_HIDE:
|
||||
return store.dispatch(consoleActions.hide());
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -53,6 +53,10 @@ const nextConsoleText = (completions, group, item, defaults) => {
|
|||
|
||||
export default function reducer(state = defaultState, action = {}) {
|
||||
switch (action.type) {
|
||||
case actions.CONSOLE_HIDE:
|
||||
return Object.assign({}, state, {
|
||||
mode: '',
|
||||
});
|
||||
case actions.CONSOLE_SHOW_COMMAND:
|
||||
return Object.assign({}, state, {
|
||||
mode: 'command',
|
||||
|
|
|
@ -69,6 +69,8 @@ body {
|
|||
|
||||
&-message {
|
||||
@include consoole-font;
|
||||
|
||||
border-top: 1px solid gray;
|
||||
}
|
||||
|
||||
&-error {
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
// NOTE: window.find is not standard API
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Window/find
|
||||
|
||||
import messages from 'shared/messages';
|
||||
import actions from 'content/actions';
|
||||
import * as consoleFrames from '../console-frames';
|
||||
|
||||
|
@ -14,6 +15,13 @@ const postPatternNotFound = (pattern) => {
|
|||
'Pattern not found: ' + pattern);
|
||||
};
|
||||
|
||||
const postPatternFound = (pattern) => {
|
||||
return consoleFrames.postInfo(
|
||||
window.document,
|
||||
'Pattern found: ' + pattern,
|
||||
);
|
||||
};
|
||||
|
||||
const find = (string, backwards) => {
|
||||
let caseSensitive = false;
|
||||
let wrapScan = true;
|
||||
|
@ -24,32 +32,49 @@ const find = (string, backwards) => {
|
|||
return window.find(string, caseSensitive, backwards, wrapScan);
|
||||
};
|
||||
|
||||
const findNext = (keyword, reset, backwards) => {
|
||||
const findNext = (currentKeyword, reset, backwards) => {
|
||||
if (reset) {
|
||||
window.getSelection().removeAllRanges();
|
||||
}
|
||||
|
||||
let found = find(keyword, backwards);
|
||||
if (!found) {
|
||||
window.getSelection().removeAllRanges();
|
||||
found = find(keyword, backwards);
|
||||
let promise = Promise.resolve(currentKeyword);
|
||||
if (currentKeyword) {
|
||||
browser.runtime.sendMessage({
|
||||
type: messages.FIND_SET_KEYWORD,
|
||||
keyword: currentKeyword,
|
||||
});
|
||||
} else {
|
||||
promise = browser.runtime.sendMessage({
|
||||
type: messages.FIND_GET_KEYWORD,
|
||||
});
|
||||
}
|
||||
if (!found) {
|
||||
postPatternNotFound(keyword);
|
||||
}
|
||||
return {
|
||||
type: actions.FIND_SET_KEYWORD,
|
||||
keyword,
|
||||
found,
|
||||
};
|
||||
|
||||
return promise.then((keyword) => {
|
||||
let found = find(keyword, backwards);
|
||||
if (!found) {
|
||||
window.getSelection().removeAllRanges();
|
||||
found = find(keyword, backwards);
|
||||
}
|
||||
if (found) {
|
||||
postPatternFound(keyword);
|
||||
} else {
|
||||
postPatternNotFound(keyword);
|
||||
}
|
||||
|
||||
return {
|
||||
type: actions.FIND_SET_KEYWORD,
|
||||
keyword,
|
||||
found,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const next = (keyword, reset) => {
|
||||
return findNext(keyword, reset, false);
|
||||
const next = (currentKeyword, reset) => {
|
||||
return findNext(currentKeyword, reset, false);
|
||||
};
|
||||
|
||||
const prev = (keyword, reset) => {
|
||||
return findNext(keyword, reset, true);
|
||||
const prev = (currentKeyword, reset) => {
|
||||
return findNext(currentKeyword, reset, true);
|
||||
};
|
||||
|
||||
export { next, prev };
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import actions from 'content/actions';
|
||||
|
||||
const enable = (newTab) => {
|
||||
const enable = (newTab, background) => {
|
||||
return {
|
||||
type: actions.FOLLOW_CONTROLLER_ENABLE,
|
||||
newTab,
|
||||
background,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -2,12 +2,16 @@ import operations from 'shared/operations';
|
|||
import messages from 'shared/messages';
|
||||
import * as scrolls from 'content/scrolls';
|
||||
import * as navigates from 'content/navigates';
|
||||
import * as focuses from 'content/focuses';
|
||||
import * as urls from 'content/urls';
|
||||
import * as consoleFrames from 'content/console-frames';
|
||||
import * as addonActions from './addon';
|
||||
import * as properties from 'shared/settings/properties';
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
const exec = (operation) => {
|
||||
const exec = (operation, repeat, settings) => {
|
||||
let smoothscroll = settings.properties.smoothscroll ||
|
||||
properties.defaults.smoothscroll;
|
||||
switch (operation.type) {
|
||||
case operations.ADDON_ENABLE:
|
||||
return addonActions.enable();
|
||||
|
@ -24,23 +28,24 @@ const exec = (operation) => {
|
|||
type: messages.FIND_PREV,
|
||||
}), '*');
|
||||
case operations.SCROLL_VERTICALLY:
|
||||
return scrolls.scrollVertically(window, operation.count);
|
||||
return scrolls.scrollVertically(operation.count, smoothscroll, repeat);
|
||||
case operations.SCROLL_HORIZONALLY:
|
||||
return scrolls.scrollHorizonally(window, operation.count);
|
||||
return scrolls.scrollHorizonally(operation.count, smoothscroll, repeat);
|
||||
case operations.SCROLL_PAGES:
|
||||
return scrolls.scrollPages(window, operation.count);
|
||||
return scrolls.scrollPages(operation.count, smoothscroll, repeat);
|
||||
case operations.SCROLL_TOP:
|
||||
return scrolls.scrollTop(window);
|
||||
return scrolls.scrollTop(smoothscroll, repeat);
|
||||
case operations.SCROLL_BOTTOM:
|
||||
return scrolls.scrollBottom(window);
|
||||
return scrolls.scrollBottom(smoothscroll, repeat);
|
||||
case operations.SCROLL_HOME:
|
||||
return scrolls.scrollHome(window);
|
||||
return scrolls.scrollHome(smoothscroll, repeat);
|
||||
case operations.SCROLL_END:
|
||||
return scrolls.scrollEnd(window);
|
||||
return scrolls.scrollEnd(smoothscroll, repeat);
|
||||
case operations.FOLLOW_START:
|
||||
return window.top.postMessage(JSON.stringify({
|
||||
type: messages.FOLLOW_START,
|
||||
newTab: operation.newTab
|
||||
newTab: operation.newTab,
|
||||
background: operation.background,
|
||||
}), '*');
|
||||
case operations.NAVIGATE_HISTORY_PREV:
|
||||
return navigates.historyPrev(window);
|
||||
|
@ -54,12 +59,13 @@ const exec = (operation) => {
|
|||
return navigates.parent(window);
|
||||
case operations.NAVIGATE_ROOT:
|
||||
return navigates.root(window);
|
||||
case operations.FOCUS_INPUT:
|
||||
return focuses.focusInput();
|
||||
case operations.URLS_YANK:
|
||||
urls.yank(window);
|
||||
return consoleFrames.postMessage(window.document, {
|
||||
type: messages.CONSOLE_SHOW_INFO,
|
||||
text: 'Current url yanked',
|
||||
});
|
||||
return consoleFrames.postInfo(window.document, 'Current url yanked');
|
||||
case operations.URLS_PASTE:
|
||||
return urls.paste(window, operation.newTab ? operation.newTab : false);
|
||||
default:
|
||||
browser.runtime.sendMessage({
|
||||
type: messages.BACKGROUND_OPERATION,
|
||||
|
|
|
@ -1,10 +1,17 @@
|
|||
import actions from 'content/actions';
|
||||
import * as keyUtils from 'shared/utils/keys';
|
||||
import operations from 'shared/operations';
|
||||
|
||||
const reservedKeymaps = {
|
||||
'<Esc>': { type: operations.CANCEL },
|
||||
'<C-[>': { type: operations.CANCEL },
|
||||
};
|
||||
|
||||
const set = (value) => {
|
||||
let entries = [];
|
||||
if (value.keymaps) {
|
||||
entries = Object.entries(value.keymaps).map((entry) => {
|
||||
let keymaps = Object.assign({}, value.keymaps, reservedKeymaps);
|
||||
entries = Object.entries(keymaps).map((entry) => {
|
||||
return [
|
||||
keyUtils.fromMapKeys(entry[0]),
|
||||
entry[1],
|
||||
|
|
|
@ -4,7 +4,8 @@ import * as dom from 'shared/utils/dom';
|
|||
|
||||
const TARGET_SELECTOR = [
|
||||
'a', 'button', 'input', 'textarea', 'area',
|
||||
'[contenteditable=true]', '[contenteditable=""]', '[tabindex]'
|
||||
'[contenteditable=true]', '[contenteditable=""]', '[tabindex]',
|
||||
'[role="button"]'
|
||||
].join(',');
|
||||
|
||||
|
||||
|
@ -29,11 +30,27 @@ const inViewport = (win, element, viewSize, framePosition) => {
|
|||
return true;
|
||||
};
|
||||
|
||||
const isAriaHiddenOrAriaDisabled = (win, element) => {
|
||||
if (!element || win.document.documentElement === element) {
|
||||
return false;
|
||||
}
|
||||
for (let attr of ['aria-hidden', 'aria-disabled']) {
|
||||
if (element.hasAttribute(attr)) {
|
||||
let hidden = element.getAttribute(attr).toLowerCase();
|
||||
if (hidden === '' || hidden === 'true') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return isAriaHiddenOrAriaDisabled(win, element.parentNode);
|
||||
};
|
||||
|
||||
export default class Follow {
|
||||
constructor(win, store) {
|
||||
this.win = win;
|
||||
this.store = store;
|
||||
this.newTab = false;
|
||||
this.background = false;
|
||||
this.hints = {};
|
||||
this.targets = [];
|
||||
|
||||
|
@ -47,6 +64,7 @@ export default class Follow {
|
|||
this.win.parent.postMessage(JSON.stringify({
|
||||
type: messages.FOLLOW_KEY_PRESS,
|
||||
key: key.key,
|
||||
ctrlKey: key.ctrlKey,
|
||||
}), '*');
|
||||
return true;
|
||||
}
|
||||
|
@ -68,6 +86,7 @@ export default class Follow {
|
|||
type: messages.OPEN_URL,
|
||||
url: element.href,
|
||||
newTab: true,
|
||||
background: this.background,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -79,12 +98,13 @@ export default class Follow {
|
|||
}), '*');
|
||||
}
|
||||
|
||||
createHints(keysArray, newTab) {
|
||||
createHints(keysArray, newTab, background) {
|
||||
if (keysArray.length !== this.targets.length) {
|
||||
throw new Error('illegal hint count');
|
||||
}
|
||||
|
||||
this.newTab = newTab;
|
||||
this.background = background;
|
||||
this.hints = {};
|
||||
for (let i = 0; i < keysArray.length; ++i) {
|
||||
let keys = keysArray[i];
|
||||
|
@ -150,7 +170,8 @@ export default class Follow {
|
|||
case messages.FOLLOW_REQUEST_COUNT_TARGETS:
|
||||
return this.countHints(sender, message.viewSize, message.framePosition);
|
||||
case messages.FOLLOW_CREATE_HINTS:
|
||||
return this.createHints(message.keysArray, message.newTab);
|
||||
return this.createHints(
|
||||
message.keysArray, message.newTab, message.background);
|
||||
case messages.FOLLOW_SHOW_HINTS:
|
||||
return this.showHints(message.keys);
|
||||
case messages.FOLLOW_ACTIVATE:
|
||||
|
@ -170,6 +191,7 @@ export default class Follow {
|
|||
style.visibility !== 'hidden' &&
|
||||
element.type !== 'hidden' &&
|
||||
element.offsetHeight > 0 &&
|
||||
!isAriaHiddenOrAriaDisabled(win, element) &&
|
||||
inViewport(win, element, viewSize, framePosition);
|
||||
});
|
||||
return filtered;
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import * as dom from 'shared/utils/dom';
|
||||
import * as keys from 'shared/utils/keys';
|
||||
|
||||
const cancelKey = (e) => {
|
||||
return e.key === 'Escape' || e.key === '[' && e.ctrlKey;
|
||||
};
|
||||
|
||||
export default class InputComponent {
|
||||
constructor(target) {
|
||||
this.pressed = {};
|
||||
|
@ -37,7 +41,7 @@ export default class InputComponent {
|
|||
|
||||
capture(e) {
|
||||
if (this.fromInput(e)) {
|
||||
if (e.key === 'Escape' && e.target.blur) {
|
||||
if (cancelKey(e) && e.target.blur) {
|
||||
e.target.blur();
|
||||
}
|
||||
return;
|
||||
|
|
|
@ -47,7 +47,8 @@ export default class KeymapperComponent {
|
|||
return true;
|
||||
}
|
||||
let operation = keymaps.get(matched[0]);
|
||||
this.store.dispatch(operationActions.exec(operation));
|
||||
this.store.dispatch(operationActions.exec(
|
||||
operation, key.repeat, state.setting));
|
||||
this.store.dispatch(inputActions.clearKeys());
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
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) {
|
||||
|
@ -32,23 +31,11 @@ export default class FindComponent {
|
|||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import * as followControllerActions from 'content/actions/follow-controller';
|
||||
import messages from 'shared/messages';
|
||||
import HintKeyProducer from 'content/hint-key-producer';
|
||||
|
||||
const DEFAULT_HINT_CHARSET = 'abcdefghijklmnopqrstuvwxyz';
|
||||
import * as properties from 'shared/settings/properties';
|
||||
|
||||
const broadcastMessage = (win, message) => {
|
||||
let json = JSON.stringify(message);
|
||||
|
@ -29,11 +28,11 @@ export default class FollowController {
|
|||
switch (message.type) {
|
||||
case messages.FOLLOW_START:
|
||||
return this.store.dispatch(
|
||||
followControllerActions.enable(message.newTab));
|
||||
followControllerActions.enable(message.newTab, message.background));
|
||||
case messages.FOLLOW_RESPONSE_COUNT_TARGETS:
|
||||
return this.create(message.count, sender);
|
||||
case messages.FOLLOW_KEY_PRESS:
|
||||
return this.keyPress(message.key);
|
||||
return this.keyPress(message.key, message.ctrlKey);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -70,7 +69,11 @@ export default class FollowController {
|
|||
});
|
||||
}
|
||||
|
||||
keyPress(key) {
|
||||
keyPress(key, ctrlKey) {
|
||||
if (key === '[' && ctrlKey) {
|
||||
this.store.dispatch(followControllerActions.disable());
|
||||
return true;
|
||||
}
|
||||
switch (key) {
|
||||
case 'Enter':
|
||||
this.activate();
|
||||
|
@ -84,7 +87,7 @@ export default class FollowController {
|
|||
this.store.dispatch(followControllerActions.backspace());
|
||||
break;
|
||||
default:
|
||||
if (DEFAULT_HINT_CHARSET.includes(key)) {
|
||||
if (this.hintchars().includes(key)) {
|
||||
this.store.dispatch(followControllerActions.keyPress(key));
|
||||
}
|
||||
break;
|
||||
|
@ -93,7 +96,7 @@ export default class FollowController {
|
|||
}
|
||||
|
||||
count() {
|
||||
this.producer = new HintKeyProducer(DEFAULT_HINT_CHARSET);
|
||||
this.producer = new HintKeyProducer(this.hintchars());
|
||||
let doc = this.win.document;
|
||||
let viewWidth = this.win.innerWidth || doc.documentElement.clientWidth;
|
||||
let viewHeight = this.win.innerHeight || doc.documentElement.clientHeight;
|
||||
|
@ -126,6 +129,7 @@ export default class FollowController {
|
|||
type: messages.FOLLOW_CREATE_HINTS,
|
||||
keysArray: produced,
|
||||
newTab: this.state.newTab,
|
||||
background: this.state.background,
|
||||
}), '*');
|
||||
}
|
||||
|
||||
|
@ -135,4 +139,9 @@ export default class FollowController {
|
|||
type: messages.FOLLOW_REMOVE_HINTS,
|
||||
});
|
||||
}
|
||||
|
||||
hintchars() {
|
||||
return this.store.getState().setting.properties.hintchars ||
|
||||
properties.defaults.hintchars;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,4 +28,11 @@ const postError = (doc, message) => {
|
|||
});
|
||||
};
|
||||
|
||||
export { initialize, blur, postMessage, postError };
|
||||
const postInfo = (doc, message) => {
|
||||
return postMessage(doc, {
|
||||
type: messages.CONSOLE_SHOW_INFO,
|
||||
text: message,
|
||||
});
|
||||
};
|
||||
|
||||
export { initialize, blur, postError, postInfo };
|
||||
|
|
13
src/content/focuses.js
Normal file
13
src/content/focuses.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
import * as doms from 'shared/utils/dom';
|
||||
|
||||
const focusInput = () => {
|
||||
let inputTypes = ['email', 'number', 'search', 'tel', 'text', 'url'];
|
||||
let inputSelector = inputTypes.map(type => `input[type=${type}]`).join(',');
|
||||
let targets = window.document.querySelectorAll(inputSelector + ',textarea');
|
||||
let target = Array.from(targets).find(doms.isVisible);
|
||||
if (target) {
|
||||
target.focus();
|
||||
}
|
||||
};
|
||||
|
||||
export { focusInput };
|
|
@ -1,17 +1,18 @@
|
|||
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 REL_PATTERN = {
|
||||
prev: /^(?:prev(?:ious)?|older)\b|\u2039|\u2190|\xab|\u226a|<</i,
|
||||
next: /^(?:next|newer)\b|\u203a|\u2192|\xbb|\u226b|>>/i,
|
||||
};
|
||||
|
||||
const findLinkByPatterns = (win, patterns) => {
|
||||
let links = win.document.getElementsByTagName('a');
|
||||
return Array.prototype.find.call(links, (link) => {
|
||||
return patterns.some(ptn => ptn.test(link.textContent));
|
||||
});
|
||||
// Return the last element in the document matching the supplied selector
|
||||
// and the optional filter, or null if there are no matches.
|
||||
const selectLast = (win, selector, filter) => {
|
||||
let nodes = win.document.querySelectorAll(selector);
|
||||
|
||||
if (filter) {
|
||||
nodes = Array.from(nodes).filter(filter);
|
||||
}
|
||||
|
||||
return nodes.length ? nodes[nodes.length - 1] : null;
|
||||
};
|
||||
|
||||
const historyPrev = (win) => {
|
||||
|
@ -22,30 +23,37 @@ const historyNext = (win) => {
|
|||
win.history.forward();
|
||||
};
|
||||
|
||||
const linkPrev = (win) => {
|
||||
let link = win.document.querySelector('a[rel=prev]');
|
||||
// Code common to linkPrev and linkNext which navigates to the specified page.
|
||||
const linkRel = (win, rel) => {
|
||||
let link = selectLast(win, `link[rel~=${rel}][href]`);
|
||||
|
||||
if (link) {
|
||||
return link.click();
|
||||
win.location = link.href;
|
||||
return;
|
||||
}
|
||||
link = findLinkByPatterns(win, PREV_LINK_PATTERNS);
|
||||
|
||||
const pattern = REL_PATTERN[rel];
|
||||
|
||||
link = selectLast(win, `a[rel~=${rel}][href]`) ||
|
||||
// `innerText` is much slower than `textContent`, but produces much better
|
||||
// (i.e. less unexpected) results
|
||||
selectLast(win, 'a[href]', lnk => pattern.test(lnk.innerText));
|
||||
|
||||
if (link) {
|
||||
link.click();
|
||||
}
|
||||
};
|
||||
|
||||
const linkPrev = (win) => {
|
||||
linkRel(win, 'prev');
|
||||
};
|
||||
|
||||
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();
|
||||
}
|
||||
linkRel(win, 'next');
|
||||
};
|
||||
|
||||
const parent = (win) => {
|
||||
let loc = win.location;
|
||||
const loc = win.location;
|
||||
if (loc.hash !== '') {
|
||||
loc.hash = '';
|
||||
return;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import actions from 'content/actions';
|
||||
|
||||
const defaultState = {
|
||||
keyword: '',
|
||||
keyword: null,
|
||||
found: false,
|
||||
};
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import actions from 'content/actions';
|
|||
const defaultState = {
|
||||
enabled: false,
|
||||
newTab: false,
|
||||
background: false,
|
||||
keys: '',
|
||||
};
|
||||
|
||||
|
@ -12,6 +13,7 @@ export default function reducer(state = defaultState, action = {}) {
|
|||
return Object.assign({}, state, {
|
||||
enabled: true,
|
||||
newTab: action.newTab,
|
||||
background: action.background,
|
||||
keys: '',
|
||||
});
|
||||
case actions.FOLLOW_CONTROLLER_DISABLE:
|
||||
|
|
|
@ -1,27 +1,14 @@
|
|||
import * as doms from 'shared/utils/dom';
|
||||
|
||||
const SCROLL_DELTA_X = 48;
|
||||
const SCROLL_DELTA_Y = 48;
|
||||
const SMOOTH_SCROLL_DURATION = 150;
|
||||
|
||||
const isVisible = (win, element) => {
|
||||
let rect = element.getBoundingClientRect();
|
||||
if (rect.width === 0 || rect.height === 0) {
|
||||
return false;
|
||||
}
|
||||
if (rect.right < 0 && rect.bottom < 0) {
|
||||
return false;
|
||||
}
|
||||
if (win.innerWidth < rect.left && win.innerHeight < rect.top) {
|
||||
return false;
|
||||
}
|
||||
// dirty way to store scrolling state on globally
|
||||
let scrolling = [false];
|
||||
|
||||
let { display, visibility } = win.getComputedStyle(element);
|
||||
if (display === 'none' || visibility === 'hidden') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const isScrollableStyle = (win, element) => {
|
||||
let { overflowX, overflowY } = win.getComputedStyle(element);
|
||||
const isScrollableStyle = (element) => {
|
||||
let { overflowX, overflowY } = window.getComputedStyle(element);
|
||||
return !(overflowX !== 'scroll' && overflowX !== 'auto' &&
|
||||
overflowY !== 'scroll' && overflowY !== 'auto');
|
||||
};
|
||||
|
@ -35,15 +22,14 @@ const isOverflowed = (element) => {
|
|||
// this method is called by each scrolling, and the returned value of this
|
||||
// method is not cached. That does not cause performance issue because in the
|
||||
// most pages, the window is root element i,e, documentElement.
|
||||
const findScrollable = (win, element) => {
|
||||
if (isScrollableStyle(win, element) && isOverflowed(element)) {
|
||||
const findScrollable = (element) => {
|
||||
if (isScrollableStyle(element) && isOverflowed(element)) {
|
||||
return element;
|
||||
}
|
||||
|
||||
let children = Array.prototype
|
||||
.filter.call(element.children, e => isVisible(win, e));
|
||||
let children = Array.from(element.children).filter(doms.isVisible);
|
||||
for (let child of children) {
|
||||
let scrollable = findScrollable(win, child);
|
||||
let scrollable = findScrollable(child);
|
||||
if (scrollable) {
|
||||
return scrollable;
|
||||
}
|
||||
|
@ -51,68 +37,153 @@ const findScrollable = (win, element) => {
|
|||
return null;
|
||||
};
|
||||
|
||||
const scrollTarget = (win) => {
|
||||
if (isOverflowed(win.document.documentElement)) {
|
||||
return win.document.documentElement;
|
||||
const scrollTarget = () => {
|
||||
if (isOverflowed(window.document.documentElement)) {
|
||||
return window.document.documentElement;
|
||||
}
|
||||
if (isOverflowed(win.document.body)) {
|
||||
return win.document.body;
|
||||
if (isOverflowed(window.document.body)) {
|
||||
return window.document.body;
|
||||
}
|
||||
let target = findScrollable(win, win.document.documentElement);
|
||||
let target = findScrollable(window.document.documentElement);
|
||||
if (target) {
|
||||
return target;
|
||||
}
|
||||
return win.document.documentElement;
|
||||
return window.document.documentElement;
|
||||
};
|
||||
|
||||
const scrollVertically = (win, count) => {
|
||||
let target = scrollTarget(win);
|
||||
class SmoothScroller {
|
||||
constructor(element, repeat) {
|
||||
this.element = element;
|
||||
this.repeat = repeat;
|
||||
this.scrolling = scrolling;
|
||||
if (repeat) {
|
||||
this.easing = SmoothScroller.linearEasing;
|
||||
} else {
|
||||
this.easing = SmoothScroller.inOutQuadEasing;
|
||||
}
|
||||
}
|
||||
|
||||
scroll(x, y) {
|
||||
if (this.scrolling[0]) {
|
||||
return;
|
||||
}
|
||||
scrolling[0] = true;
|
||||
|
||||
this.startX = this.element.scrollLeft;
|
||||
this.startY = this.element.scrollTop;
|
||||
|
||||
this.targetX = x;
|
||||
this.targetY = y;
|
||||
this.distanceX = x - this.startX;
|
||||
this.distanceY = y - this.startY;
|
||||
this.timeStart = 0;
|
||||
|
||||
window.requestAnimationFrame(this.loop.bind(this));
|
||||
}
|
||||
|
||||
loop(time) {
|
||||
if (!this.timeStart) {
|
||||
this.timeStart = time;
|
||||
}
|
||||
|
||||
let elapsed = time - this.timeStart;
|
||||
let v = this.easing(elapsed / SMOOTH_SCROLL_DURATION);
|
||||
let nextX = this.startX + this.distanceX * v;
|
||||
let nextY = this.startY + this.distanceY * v;
|
||||
|
||||
window.scrollTo(nextX, nextY);
|
||||
|
||||
if (elapsed < SMOOTH_SCROLL_DURATION) {
|
||||
window.requestAnimationFrame(this.loop.bind(this));
|
||||
} else {
|
||||
scrolling[0] = false;
|
||||
this.element.scrollTo(this.targetX, this.targetY);
|
||||
}
|
||||
}
|
||||
|
||||
static inOutQuadEasing(t) {
|
||||
if (t < 1) {
|
||||
return t * t;
|
||||
}
|
||||
return -(t - 1) * (t - 1) + 1;
|
||||
}
|
||||
|
||||
static linearEasing(t) {
|
||||
return t;
|
||||
}
|
||||
}
|
||||
|
||||
class RoughtScroller {
|
||||
constructor(element) {
|
||||
this.element = element;
|
||||
}
|
||||
|
||||
scroll(x, y) {
|
||||
this.element.scrollTo(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
const scroller = (element, smooth, repeat) => {
|
||||
if (smooth) {
|
||||
return new SmoothScroller(element, repeat);
|
||||
}
|
||||
return new RoughtScroller(element);
|
||||
};
|
||||
|
||||
const scrollVertically = (count, smooth, repeat) => {
|
||||
let target = scrollTarget();
|
||||
let x = target.scrollLeft;
|
||||
let y = target.scrollTop + SCROLL_DELTA_Y * count;
|
||||
target.scrollTo(x, y);
|
||||
if (repeat && smooth) {
|
||||
y = target.scrollTop + SCROLL_DELTA_Y * count * 4;
|
||||
}
|
||||
scroller(target, smooth, repeat).scroll(x, y);
|
||||
};
|
||||
|
||||
const scrollHorizonally = (win, count) => {
|
||||
let target = scrollTarget(win);
|
||||
const scrollHorizonally = (count, smooth, repeat) => {
|
||||
let target = scrollTarget();
|
||||
let x = target.scrollLeft + SCROLL_DELTA_X * count;
|
||||
let y = target.scrollTop;
|
||||
target.scrollTo(x, y);
|
||||
if (repeat && smooth) {
|
||||
y = target.scrollTop + SCROLL_DELTA_Y * count * 4;
|
||||
}
|
||||
scroller(target, smooth, repeat).scroll(x, y);
|
||||
};
|
||||
|
||||
const scrollPages = (win, count) => {
|
||||
let target = scrollTarget(win);
|
||||
const scrollPages = (count, smooth, repeat) => {
|
||||
let target = scrollTarget();
|
||||
let height = target.clientHeight;
|
||||
let x = target.scrollLeft;
|
||||
let y = target.scrollTop + height * count;
|
||||
target.scrollTo(x, y);
|
||||
scroller(target, smooth, repeat).scroll(x, y);
|
||||
};
|
||||
|
||||
const scrollTop = (win) => {
|
||||
let target = scrollTarget(win);
|
||||
const scrollTop = (smooth, repeat) => {
|
||||
let target = scrollTarget();
|
||||
let x = target.scrollLeft;
|
||||
let y = 0;
|
||||
target.scrollTo(x, y);
|
||||
scroller(target, smooth, repeat).scroll(x, y);
|
||||
};
|
||||
|
||||
const scrollBottom = (win) => {
|
||||
let target = scrollTarget(win);
|
||||
const scrollBottom = (smooth, repeat) => {
|
||||
let target = scrollTarget();
|
||||
let x = target.scrollLeft;
|
||||
let y = target.scrollHeight;
|
||||
target.scrollTo(x, y);
|
||||
scroller(target, smooth, repeat).scroll(x, y);
|
||||
};
|
||||
|
||||
const scrollHome = (win) => {
|
||||
let target = scrollTarget(win);
|
||||
const scrollHome = (smooth, repeat) => {
|
||||
let target = scrollTarget();
|
||||
let x = 0;
|
||||
let y = target.scrollTop;
|
||||
target.scrollTo(x, y);
|
||||
scroller(target, smooth, repeat).scroll(x, y);
|
||||
};
|
||||
|
||||
const scrollEnd = (win) => {
|
||||
let target = scrollTarget(win);
|
||||
const scrollEnd = (smooth, repeat) => {
|
||||
let target = scrollTarget();
|
||||
let x = target.scrollWidth;
|
||||
let y = target.scrollTop;
|
||||
target.scrollTo(x, y);
|
||||
scroller(target, smooth, repeat).scroll(x, y);
|
||||
};
|
||||
|
||||
export {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import messages from 'shared/messages';
|
||||
|
||||
const yank = (win) => {
|
||||
let input = win.document.createElement('input');
|
||||
win.document.body.append(input);
|
||||
|
@ -12,4 +14,26 @@ const yank = (win) => {
|
|||
input.remove();
|
||||
};
|
||||
|
||||
export { yank };
|
||||
const paste = (win, newTab) => {
|
||||
let textarea = win.document.createElement('textarea');
|
||||
win.document.body.append(textarea);
|
||||
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.top = '-100px';
|
||||
textarea.contentEditable = 'true';
|
||||
textarea.focus();
|
||||
|
||||
if (win.document.execCommand('paste')) {
|
||||
if (/^(https?|ftp):\/\//.test(textarea.textContent)) {
|
||||
browser.runtime.sendMessage({
|
||||
type: messages.OPEN_URL,
|
||||
url: textarea.textContent,
|
||||
newTab: newTab ? newTab : false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
textarea.remove();
|
||||
};
|
||||
|
||||
export { yank, paste };
|
||||
|
|
|
@ -1,34 +1,39 @@
|
|||
import actions from 'settings/actions';
|
||||
import messages from 'shared/messages';
|
||||
import DefaultSettings from 'shared/default-settings';
|
||||
import DefaultSettings from 'shared/settings/default';
|
||||
import * as settingsStorage from 'shared/settings/storage';
|
||||
import * as settingsValues from 'shared/settings/values';
|
||||
|
||||
const load = () => {
|
||||
return browser.storage.local.get('settings').then(({ settings }) => {
|
||||
if (settings) {
|
||||
return set(settings);
|
||||
}
|
||||
return set(DefaultSettings);
|
||||
}, console.error);
|
||||
return settingsStorage.loadRaw().then((settings) => {
|
||||
return set(settings);
|
||||
});
|
||||
};
|
||||
|
||||
const save = (settings) => {
|
||||
return browser.storage.local.set({
|
||||
settings,
|
||||
}).then(() => {
|
||||
return settingsStorage.save(settings).then(() => {
|
||||
return browser.runtime.sendMessage({
|
||||
type: messages.SETTINGS_RELOAD
|
||||
}).then(() => {
|
||||
return set(settings);
|
||||
});
|
||||
}).then(() => {
|
||||
return set(settings);
|
||||
});
|
||||
};
|
||||
|
||||
const set = (settings) => {
|
||||
let value = JSON.parse(DefaultSettings.json);
|
||||
if (settings.source === 'json') {
|
||||
value = settingsValues.valueFromJson(settings.json);
|
||||
} else if (settings.source === 'form') {
|
||||
value = settingsValues.valueFromForm(settings.form);
|
||||
}
|
||||
|
||||
return {
|
||||
type: actions.SETTING_SET_SETTINGS,
|
||||
source: settings.source,
|
||||
json: settings.json,
|
||||
value: JSON.parse(settings.json),
|
||||
form: settings.form,
|
||||
value,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
52
src/settings/components/form/blacklist-form.jsx
Normal file
52
src/settings/components/form/blacklist-form.jsx
Normal file
|
@ -0,0 +1,52 @@
|
|||
import './blacklist-form.scss';
|
||||
import AddButton from '../ui/add-button';
|
||||
import DeleteButton from '../ui/delete-button';
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
class BlacklistForm extends Component {
|
||||
|
||||
render() {
|
||||
let value = this.props.value;
|
||||
if (!value) {
|
||||
value = [];
|
||||
}
|
||||
|
||||
return <div className='form-blacklist-form'>
|
||||
{
|
||||
value.map((url, index) => {
|
||||
return <div key={index} className='form-blacklist-form-row'>
|
||||
<input data-index={index} type='text' name='url'
|
||||
className='column-url' value={url}
|
||||
onChange={this.bindValue.bind(this)} />
|
||||
<DeleteButton data-index={index} name='delete'
|
||||
onClick={this.bindValue.bind(this)} />
|
||||
</div>;
|
||||
})
|
||||
}
|
||||
<AddButton name='add' style='float:right'
|
||||
onClick={this.bindValue.bind(this)} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
bindValue(e) {
|
||||
if (!this.props.onChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
let name = e.target.name;
|
||||
let index = e.target.getAttribute('data-index');
|
||||
let next = this.props.value ? this.props.value.slice() : [];
|
||||
|
||||
if (name === 'url') {
|
||||
next[index] = e.target.value;
|
||||
} else if (name === 'add') {
|
||||
next.push('');
|
||||
} else if (name === 'delete') {
|
||||
next.splice(index, 1);
|
||||
}
|
||||
|
||||
this.props.onChange(next);
|
||||
}
|
||||
}
|
||||
|
||||
export default BlacklistForm;
|
9
src/settings/components/form/blacklist-form.scss
Normal file
9
src/settings/components/form/blacklist-form.scss
Normal file
|
@ -0,0 +1,9 @@
|
|||
.form-blacklist-form {
|
||||
&-row {
|
||||
display: flex;
|
||||
|
||||
.column-url {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
109
src/settings/components/form/keymaps-form.jsx
Normal file
109
src/settings/components/form/keymaps-form.jsx
Normal file
|
@ -0,0 +1,109 @@
|
|||
import './keymaps-form.scss';
|
||||
import { h, Component } from 'preact';
|
||||
import Input from '../ui/input';
|
||||
|
||||
const KeyMapFields = [
|
||||
[
|
||||
['scroll.vertically?{"count":1}', 'Scroll down'],
|
||||
['scroll.vertically?{"count":-1}', 'Scroll up'],
|
||||
['scroll.horizonally?{"count":-1}', 'Scroll left'],
|
||||
['scroll.horizonally?{"count":1}', 'Scroll right'],
|
||||
['scroll.home', 'Scroll to leftmost'],
|
||||
['scroll.end', 'Scroll to rightmost'],
|
||||
['scroll.top', 'Scroll to top'],
|
||||
['scroll.bottom', 'Scroll to bottom'],
|
||||
['scroll.pages?{"count":-0.5}', 'Scroll up by half of screen'],
|
||||
['scroll.pages?{"count":0.5}', 'Scroll down by half of screen'],
|
||||
['scroll.pages?{"count":-1}', 'Scroll up by a screen'],
|
||||
['scroll.pages?{"count":1}', 'Scroll down by a screen'],
|
||||
], [
|
||||
['tabs.close', 'Close a tab'],
|
||||
['tabs.reopen', 'Reopen closed tab'],
|
||||
['tabs.next?{"count":1}', 'Select next Tab'],
|
||||
['tabs.prev?{"count":1}', 'Select prev Tab'],
|
||||
['tabs.first', 'Select first tab'],
|
||||
['tabs.last', 'Select last tab'],
|
||||
['tabs.reload?{"cache":false}', 'Reload current tab'],
|
||||
['tabs.reload?{"cache":true}', 'Reload with no caches'],
|
||||
['tabs.pin.toggle', 'Toggle pinned state'],
|
||||
['tabs.duplicate', 'Duplicate a tab'],
|
||||
], [
|
||||
['follow.start?{"newTab":false}', 'Follow a link'],
|
||||
['follow.start?{"newTab":true}', 'Follow a link in new tab'],
|
||||
['navigate.history.prev', 'Go back in histories'],
|
||||
['navigate.history.next', 'Go forward in histories'],
|
||||
['navigate.link.next', 'Open next link'],
|
||||
['navigate.link.prev', 'Open previous link'],
|
||||
['navigate.parent', 'Go to parent directory'],
|
||||
['navigate.root', 'Go to root directory'],
|
||||
['focus.input', 'Focus input'],
|
||||
], [
|
||||
['find.start', 'Start find mode'],
|
||||
['find.next', 'Find next word'],
|
||||
['find.prev', 'Find previous word'],
|
||||
], [
|
||||
['command.show', 'Open console'],
|
||||
['command.show.open?{"alter":false}', 'Open URL'],
|
||||
['command.show.open?{"alter":true}', 'Alter URL'],
|
||||
['command.show.tabopen?{"alter":false}', 'Open URL in new Tab'],
|
||||
['command.show.tabopen?{"alter":true}', 'Alter URL in new Tab'],
|
||||
['command.show.winopen?{"alter":false}', 'Open URL in new window'],
|
||||
['command.show.winopen?{"alter":true}', 'Alter URL in new window'],
|
||||
['command.show.buffer', 'Open buffer command'],
|
||||
], [
|
||||
['addon.toggle.enabled', 'Enable or disable'],
|
||||
['urls.yank', 'Copy current URL'],
|
||||
['urls.paste?{"newTab":false}', 'Open clipboard\'s URL in current tab'],
|
||||
['urls.paste?{"newTab":true}', 'Open clipboard\'s URL in new tab'],
|
||||
['zoom.in', 'Zoom-in'],
|
||||
['zoom.out', 'Zoom-out'],
|
||||
['zoom.neutral', 'Reset zoom level'],
|
||||
]
|
||||
];
|
||||
|
||||
const AllowdOps = [].concat(...KeyMapFields.map(group => group.map(e => e[0])));
|
||||
|
||||
class KeymapsForm extends Component {
|
||||
|
||||
render() {
|
||||
let values = this.props.value;
|
||||
if (!values) {
|
||||
values = {};
|
||||
}
|
||||
return <div className='form-keymaps-form'>
|
||||
{
|
||||
KeyMapFields.map((group, index) => {
|
||||
return <div key={index} className='form-keymaps-form-field-group'>
|
||||
{
|
||||
group.map((field) => {
|
||||
let name = field[0];
|
||||
let label = field[1];
|
||||
let value = values[name];
|
||||
return <Input
|
||||
type='text' id={name} name={name} key={name}
|
||||
label={label} value={value}
|
||||
onChange={this.bindValue.bind(this)}
|
||||
/>;
|
||||
})
|
||||
}
|
||||
</div>;
|
||||
})
|
||||
}
|
||||
</div>;
|
||||
}
|
||||
|
||||
bindValue(e) {
|
||||
if (!this.props.onChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
let next = Object.assign({}, this.props.value);
|
||||
next[e.target.name] = e.target.value;
|
||||
|
||||
this.props.onChange(next);
|
||||
}
|
||||
}
|
||||
|
||||
KeymapsForm.AllowdOps = AllowdOps;
|
||||
|
||||
export default KeymapsForm;
|
11
src/settings/components/form/keymaps-form.scss
Normal file
11
src/settings/components/form/keymaps-form.scss
Normal file
|
@ -0,0 +1,11 @@
|
|||
.form-keymaps-form {
|
||||
column-count: 3;
|
||||
|
||||
&-field-group {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
&-field-group:first-of-type {
|
||||
margin-top: 24px;
|
||||
}
|
||||
}
|
60
src/settings/components/form/properties-form.jsx
Normal file
60
src/settings/components/form/properties-form.jsx
Normal file
|
@ -0,0 +1,60 @@
|
|||
import './properties-form.scss';
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
class PropertiesForm extends Component {
|
||||
|
||||
render() {
|
||||
let types = this.props.types;
|
||||
let value = this.props.value;
|
||||
if (!value) {
|
||||
value = {};
|
||||
}
|
||||
|
||||
return <div className='form-properties-form'>
|
||||
{
|
||||
Object.keys(types).map((name) => {
|
||||
let type = types[name];
|
||||
let inputType = null;
|
||||
if (type === 'string') {
|
||||
inputType = 'text';
|
||||
} else if (type === 'number') {
|
||||
inputType = 'number';
|
||||
} else if (type === 'boolean') {
|
||||
inputType = 'checkbox';
|
||||
}
|
||||
return <div key={name} className='form-properties-form-row'>
|
||||
<label>
|
||||
<span className='column-name'>{name}</span>
|
||||
<input type={inputType} name={name}
|
||||
className='column-input'
|
||||
value={value[name] ? value[name] : ''}
|
||||
onChange={this.bindValue.bind(this)}
|
||||
checked={value[name]}
|
||||
/>
|
||||
</label>
|
||||
</div>;
|
||||
})
|
||||
}
|
||||
</div>;
|
||||
}
|
||||
|
||||
bindValue(e) {
|
||||
if (!this.props.onChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
let name = e.target.name;
|
||||
let next = Object.assign({}, this.props.value);
|
||||
if (e.target.type.toLowerCase() === 'checkbox') {
|
||||
next[name] = e.target.checked;
|
||||
} else if (e.target.type.toLowerCase() === 'number') {
|
||||
next[name] = Number(e.target.value);
|
||||
} else {
|
||||
next[name] = e.target.value;
|
||||
}
|
||||
|
||||
this.props.onChange(next);
|
||||
}
|
||||
}
|
||||
|
||||
export default PropertiesForm;
|
12
src/settings/components/form/properties-form.scss
Normal file
12
src/settings/components/form/properties-form.scss
Normal file
|
@ -0,0 +1,12 @@
|
|||
.form-properties-form {
|
||||
&-row {
|
||||
.column-name {
|
||||
display: inline-block;
|
||||
min-width: 5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
.column-input {
|
||||
line-height: 2.2rem;
|
||||
}
|
||||
}
|
||||
}
|
78
src/settings/components/form/search-form.jsx
Normal file
78
src/settings/components/form/search-form.jsx
Normal file
|
@ -0,0 +1,78 @@
|
|||
import './search-form.scss';
|
||||
import { h, Component } from 'preact';
|
||||
import AddButton from '../ui/add-button';
|
||||
import DeleteButton from '../ui/delete-button';
|
||||
|
||||
class SearchForm extends Component {
|
||||
|
||||
render() {
|
||||
let value = this.props.value;
|
||||
if (!value) {
|
||||
value = { default: '', engines: []};
|
||||
}
|
||||
if (!value.engines) {
|
||||
value.engines = [];
|
||||
}
|
||||
|
||||
return <div className='form-search-form'>
|
||||
<div className='form-search-form-header'>
|
||||
<div className='column-name'>Name</div>
|
||||
<div className='column-url'>URL</div>
|
||||
<div className='column-option'>Default</div>
|
||||
</div>
|
||||
{
|
||||
value.engines.map((engine, index) => {
|
||||
return <div key={index} className='form-search-form-row'>
|
||||
<input data-index={index} type='text' name='name'
|
||||
className='column-name' value={engine[0]}
|
||||
onChange={this.bindValue.bind(this)} />
|
||||
<input data-index={index} type='text' name='url'
|
||||
placeholder='http://example.com/?q={}'
|
||||
className='column-url' value={engine[1]}
|
||||
onChange={this.bindValue.bind(this)} />
|
||||
<div className='column-option'>
|
||||
<input data-index={index} type='radio' name='default'
|
||||
checked={value.default === engine[0]}
|
||||
onChange={this.bindValue.bind(this)} />
|
||||
<DeleteButton data-index={index} name='delete'
|
||||
onClick={this.bindValue.bind(this)} />
|
||||
</div>
|
||||
</div>;
|
||||
})
|
||||
}
|
||||
<AddButton name='add' style='float:right'
|
||||
onClick={this.bindValue.bind(this)} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
bindValue(e) {
|
||||
if (!this.props.onChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
let value = this.props.value;
|
||||
let name = e.target.name;
|
||||
let index = e.target.getAttribute('data-index');
|
||||
let next = Object.assign({}, {
|
||||
default: value.default,
|
||||
engines: value.engines ? value.engines.slice() : [],
|
||||
});
|
||||
|
||||
if (name === 'name') {
|
||||
next.engines[index][0] = e.target.value;
|
||||
next.default = this.props.value.engines[index][0];
|
||||
} else if (name === 'url') {
|
||||
next.engines[index][1] = e.target.value;
|
||||
} else if (name === 'default') {
|
||||
next.default = this.props.value.engines[index][0];
|
||||
} else if (name === 'add') {
|
||||
next.engines.push(['', '']);
|
||||
} else if (name === 'delete') {
|
||||
next.engines.splice(index, 1);
|
||||
}
|
||||
|
||||
this.props.onChange(next);
|
||||
}
|
||||
}
|
||||
|
||||
export default SearchForm;
|
28
src/settings/components/form/search-form.scss
Normal file
28
src/settings/components/form/search-form.scss
Normal file
|
@ -0,0 +1,28 @@
|
|||
.form-search-form {
|
||||
@mixin row-base {
|
||||
display: flex;
|
||||
|
||||
.column-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.column-url {
|
||||
flex: 5;
|
||||
min-width: 0;
|
||||
}
|
||||
.column-option {
|
||||
text-align: right;
|
||||
flex-basis: 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
&-header {
|
||||
@include row-base;
|
||||
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&-row {
|
||||
@include row-base;
|
||||
}
|
||||
}
|
|
@ -1,16 +1,29 @@
|
|||
import './site.scss';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { h, Component } from 'preact';
|
||||
import Input from './ui/input';
|
||||
import SearchForm from './form/search-form';
|
||||
import KeymapsForm from './form/keymaps-form';
|
||||
import BlacklistForm from './form/blacklist-form';
|
||||
import PropertiesForm from './form/properties-form';
|
||||
import * as properties from 'shared/settings/properties';
|
||||
import * as settingActions from 'settings/actions/setting';
|
||||
import * as validator from 'shared/validators/setting';
|
||||
import * as validator from 'shared/settings/validator';
|
||||
import * as settingsValues from 'shared/settings/values';
|
||||
|
||||
class SettingsComponent extends React.Component {
|
||||
const DO_YOU_WANT_TO_CONTINUE =
|
||||
'Some settings in JSON can be lost when migrating. ' +
|
||||
'Do you want to continue?';
|
||||
|
||||
class SettingsComponent extends Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
settings: {
|
||||
json: '',
|
||||
},
|
||||
errors: {
|
||||
json: '',
|
||||
}
|
||||
};
|
||||
this.context.store.subscribe(this.stateChanged.bind(this));
|
||||
|
@ -26,66 +39,187 @@ class SettingsComponent extends React.Component {
|
|||
settings: {
|
||||
source: settings.source,
|
||||
json: settings.json,
|
||||
form: settings.form,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
renderFormFields() {
|
||||
return <div>
|
||||
<fieldset>
|
||||
<legend>Keybindings</legend>
|
||||
<KeymapsForm
|
||||
value={this.state.settings.form.keymaps}
|
||||
onChange={value => this.bindForm('keymaps', value)}
|
||||
/>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Search Engines</legend>
|
||||
<SearchForm
|
||||
value={this.state.settings.form.search}
|
||||
onChange={value => this.bindForm('search', value)}
|
||||
/>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Blacklist</legend>
|
||||
<BlacklistForm
|
||||
value={this.state.settings.form.blacklist}
|
||||
onChange={value => this.bindForm('blacklist', value)}
|
||||
/>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Properties</legend>
|
||||
<PropertiesForm
|
||||
types={properties.types}
|
||||
value={this.state.settings.form.properties}
|
||||
onChange={value => this.bindForm('properties', value)}
|
||||
/>
|
||||
</fieldset>
|
||||
</div>;
|
||||
}
|
||||
|
||||
renderJsonFields() {
|
||||
return <div>
|
||||
<Input
|
||||
type='textarea'
|
||||
name='json'
|
||||
label='Plane JSON'
|
||||
spellCheck='false'
|
||||
error={this.state.errors.json}
|
||||
onChange={this.bindValue.bind(this)}
|
||||
value={this.state.settings.json}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
render() {
|
||||
let fields = null;
|
||||
if (this.state.settings.source === 'form') {
|
||||
fields = this.renderFormFields();
|
||||
} else if (this.state.settings.source === 'json') {
|
||||
fields = this.renderJsonFields();
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<h1>Configure Vim-Vixen</h1>
|
||||
<form className='vimvixen-settings-form'>
|
||||
|
||||
<p>Load settings from:</p>
|
||||
<input type='radio' id='setting-source-json'
|
||||
<Input
|
||||
type='radio'
|
||||
id='setting-source-form'
|
||||
name='source'
|
||||
value='json'
|
||||
onChange={this.bindAndSave.bind(this)}
|
||||
checked={this.state.settings.source === 'json'} />
|
||||
<label htmlFor='settings-source-json'>JSON</label>
|
||||
label='Use form'
|
||||
checked={this.state.settings.source === 'form'}
|
||||
value='form'
|
||||
onChange={this.bindSource.bind(this)} />
|
||||
|
||||
<textarea name='json' spellCheck='false'
|
||||
onInput={this.validate.bind(this)}
|
||||
onChange={this.bindValue.bind(this)}
|
||||
onBlur={this.bindAndSave.bind(this)}
|
||||
value={this.state.settings.json} />
|
||||
<Input
|
||||
type='radio'
|
||||
name='source'
|
||||
label='Use plain JSON'
|
||||
checked={this.state.settings.source === 'json'}
|
||||
value='json'
|
||||
onChange={this.bindSource.bind(this)} />
|
||||
|
||||
{ fields }
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
validate(e) {
|
||||
try {
|
||||
let settings = JSON.parse(e.target.value);
|
||||
validate(target) {
|
||||
if (target.name === 'json') {
|
||||
let settings = JSON.parse(target.value);
|
||||
validator.validate(settings);
|
||||
e.target.setCustomValidity('');
|
||||
} catch (err) {
|
||||
e.target.setCustomValidity(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
bindValue(e) {
|
||||
let nextSettings = Object.assign({}, this.state.settings);
|
||||
nextSettings[e.target.name] = e.target.value;
|
||||
validateValue(e) {
|
||||
let next = Object.assign({}, this.state);
|
||||
|
||||
this.setState({ settings: nextSettings });
|
||||
next.errors.json = '';
|
||||
try {
|
||||
this.validate(e.target);
|
||||
} catch (err) {
|
||||
next.errors.json = err.message;
|
||||
}
|
||||
next.settings[e.target.name] = e.target.value;
|
||||
}
|
||||
|
||||
bindAndSave(e) {
|
||||
this.bindValue(e);
|
||||
bindForm(name, value) {
|
||||
let next = Object.assign({}, this.state, {
|
||||
settings: Object.assign({}, this.state.settings, {
|
||||
form: Object.assign({}, this.state.settings.form)
|
||||
})
|
||||
});
|
||||
next.settings.form[name] = value;
|
||||
this.setState(next);
|
||||
this.context.store.dispatch(settingActions.save(next.settings));
|
||||
}
|
||||
|
||||
bindValue(e) {
|
||||
let next = Object.assign({}, this.state);
|
||||
let error = false;
|
||||
|
||||
next.errors.json = '';
|
||||
try {
|
||||
let json = this.state.settings.json;
|
||||
validator.validate(JSON.parse(json));
|
||||
this.context.store.dispatch(settingActions.save(this.state.settings));
|
||||
this.validate(e.target);
|
||||
} catch (err) {
|
||||
// error already shown
|
||||
next.errors.json = err.message;
|
||||
error = true;
|
||||
}
|
||||
next.settings[e.target.name] = e.target.value;
|
||||
|
||||
this.setState(this.state);
|
||||
if (!error) {
|
||||
this.context.store.dispatch(settingActions.save(next.settings));
|
||||
}
|
||||
}
|
||||
|
||||
migrateToForm() {
|
||||
let b = window.confirm(DO_YOU_WANT_TO_CONTINUE);
|
||||
if (!b) {
|
||||
this.setState(this.state);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
validator.validate(JSON.parse(this.state.settings.json));
|
||||
} catch (err) {
|
||||
this.setState(this.state);
|
||||
return;
|
||||
}
|
||||
|
||||
let form = settingsValues.formFromJson(
|
||||
this.state.settings.json, KeymapsForm.AllowdOps);
|
||||
let next = Object.assign({}, this.state);
|
||||
next.settings.form = form;
|
||||
next.settings.source = 'form';
|
||||
next.errors.json = '';
|
||||
|
||||
this.setState(next);
|
||||
this.context.store.dispatch(settingActions.save(next.settings));
|
||||
}
|
||||
|
||||
migrateToJson() {
|
||||
let json = settingsValues.jsonFromForm(this.state.settings.form);
|
||||
let next = Object.assign({}, this.state);
|
||||
next.settings.json = json;
|
||||
next.settings.source = 'json';
|
||||
next.errors.json = '';
|
||||
|
||||
this.setState(next);
|
||||
this.context.store.dispatch(settingActions.save(next.settings));
|
||||
}
|
||||
|
||||
bindSource(e) {
|
||||
let from = this.state.settings.source;
|
||||
let to = e.target.value;
|
||||
|
||||
if (from === 'form' && to === 'json') {
|
||||
this.migrateToJson();
|
||||
} else if (from === 'json' && to === 'form') {
|
||||
this.migrateToForm();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsComponent.contextTypes = {
|
||||
store: PropTypes.any,
|
||||
};
|
||||
|
||||
export default SettingsComponent;
|
||||
|
|
|
@ -1,8 +1,27 @@
|
|||
.vimvixen-settings-form {
|
||||
padding: 2px;
|
||||
|
||||
textarea[name=json] {
|
||||
font-family: monospace;
|
||||
width: 100%;
|
||||
min-height: 64ex;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
margin-top: 1rem;
|
||||
|
||||
fieldset:first-of-type {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
legend {
|
||||
font-size: 1.5rem;
|
||||
padding: .5rem 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
12
src/settings/components/ui/add-button.jsx
Normal file
12
src/settings/components/ui/add-button.jsx
Normal file
|
@ -0,0 +1,12 @@
|
|||
import './add-button.scss';
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
class AddButton extends Component {
|
||||
render() {
|
||||
return <input
|
||||
className='ui-add-button' type='button' value='✚'
|
||||
{...this.props} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default AddButton;
|
13
src/settings/components/ui/add-button.scss
Normal file
13
src/settings/components/ui/add-button.scss
Normal file
|
@ -0,0 +1,13 @@
|
|||
.ui-add-button {
|
||||
border: none;
|
||||
padding: 4;
|
||||
display: inline;
|
||||
background: none;
|
||||
font-weight: bold;
|
||||
color: green;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: darkgreen;
|
||||
}
|
||||
}
|
12
src/settings/components/ui/delete-button.jsx
Normal file
12
src/settings/components/ui/delete-button.jsx
Normal file
|
@ -0,0 +1,12 @@
|
|||
import './delete-button.scss';
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
class DeleteButton extends Component {
|
||||
render() {
|
||||
return <input
|
||||
className='ui-delete-button' type='button' value='✖'
|
||||
{...this.props} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default DeleteButton;
|
13
src/settings/components/ui/delete-button.scss
Normal file
13
src/settings/components/ui/delete-button.scss
Normal file
|
@ -0,0 +1,13 @@
|
|||
|
||||
.ui-delete-button {
|
||||
border: none;
|
||||
padding: 4;
|
||||
display: inline;
|
||||
background: none;
|
||||
color: red;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: darkred;
|
||||
}
|
||||
}
|
52
src/settings/components/ui/input.jsx
Normal file
52
src/settings/components/ui/input.jsx
Normal file
|
@ -0,0 +1,52 @@
|
|||
import { h, Component } from 'preact';
|
||||
import './input.scss';
|
||||
|
||||
class Input extends Component {
|
||||
|
||||
renderText(props) {
|
||||
let inputClassName = props.error ? 'input-error' : '';
|
||||
return <div className='settings-ui-input'>
|
||||
<label htmlFor={props.id}>{ props.label }</label>
|
||||
<input type='text' className={inputClassName} {...props} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
renderRadio(props) {
|
||||
let inputClassName = props.error ? 'input-error' : '';
|
||||
return <div className='settings-ui-input'>
|
||||
<label>
|
||||
<input type='radio' className={inputClassName} {...props} />
|
||||
{ props.label }
|
||||
</label>
|
||||
</div>;
|
||||
}
|
||||
|
||||
renderTextArea(props) {
|
||||
let inputClassName = props.error ? 'input-error' : '';
|
||||
return <div className='settings-ui-input'>
|
||||
<label
|
||||
htmlFor={props.id}
|
||||
>{ props.label }</label>
|
||||
<textarea className={inputClassName} {...props} />
|
||||
<p className='settings-ui-input-error'>{ this.props.error }</p>
|
||||
</div>;
|
||||
}
|
||||
|
||||
render() {
|
||||
let { type } = this.props;
|
||||
|
||||
switch (this.props.type) {
|
||||
case 'text':
|
||||
return this.renderText(this.props);
|
||||
case 'radio':
|
||||
return this.renderRadio(this.props);
|
||||
case 'textarea':
|
||||
return this.renderTextArea(this.props);
|
||||
default:
|
||||
console.warn(`Unsupported input type ${type}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default Input;
|
29
src/settings/components/ui/input.scss
Normal file
29
src/settings/components/ui/input.scss
Normal file
|
@ -0,0 +1,29 @@
|
|||
.settings-ui-input {
|
||||
page-break-inside: avoid;
|
||||
|
||||
* {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: bold;
|
||||
min-width: 14rem;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
input[type='text'] {
|
||||
padding: 4px;
|
||||
width: 8rem;
|
||||
}
|
||||
|
||||
input.input-crror,
|
||||
textarea.input-error {
|
||||
box-shadow: 0 0 2px red;
|
||||
}
|
||||
|
||||
&-error {
|
||||
font-weight: bold;
|
||||
color: red;
|
||||
min-height: 1.5em;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { h, render } from 'preact';
|
||||
import SettingsComponent from './components';
|
||||
import reducer from 'settings/reducers/setting';
|
||||
import Provider from 'shared/store/provider';
|
||||
|
@ -9,7 +8,7 @@ const store = createStore(reducer);
|
|||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
let wrapper = document.getElementById('vimvixen-settings');
|
||||
ReactDOM.render(
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<SettingsComponent />
|
||||
</Provider>,
|
||||
|
|
|
@ -3,6 +3,7 @@ import actions from 'settings/actions';
|
|||
const defaultState = {
|
||||
source: '',
|
||||
json: '',
|
||||
form: null,
|
||||
value: {}
|
||||
};
|
||||
|
||||
|
@ -12,6 +13,7 @@ export default function reducer(state = defaultState, action = {}) {
|
|||
return {
|
||||
source: action.source,
|
||||
json: action.json,
|
||||
form: action.form,
|
||||
value: action.value,
|
||||
};
|
||||
default:
|
||||
|
|
|
@ -1,169 +0,0 @@
|
|||
import * as tabs from 'background/tabs';
|
||||
import * as histories from 'background/histories';
|
||||
|
||||
const normalizeUrl = (args, searchConfig) => {
|
||||
let concat = args.join(' ');
|
||||
try {
|
||||
return new URL(concat).href;
|
||||
} catch (e) {
|
||||
if (concat.includes('.') && !concat.includes(' ')) {
|
||||
return 'http://' + concat;
|
||||
}
|
||||
let query = encodeURI(concat);
|
||||
let template = searchConfig.engines[
|
||||
searchConfig.default
|
||||
];
|
||||
for (let key in searchConfig.engines) {
|
||||
if (args[0] === key) {
|
||||
query = args.slice(1).join(' ');
|
||||
template = searchConfig.engines[key];
|
||||
}
|
||||
}
|
||||
return template.replace('{}', query);
|
||||
}
|
||||
};
|
||||
|
||||
const openCommand = (url) => {
|
||||
return browser.tabs.query({
|
||||
active: true, currentWindow: true
|
||||
}).then((gotTabs) => {
|
||||
if (gotTabs.length > 0) {
|
||||
return browser.tabs.update(gotTabs[0].id, { url: url });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const tabopenCommand = (url) => {
|
||||
return browser.tabs.create({ url: url });
|
||||
};
|
||||
|
||||
const winopenCommand = (url) => {
|
||||
return browser.windows.create({ url });
|
||||
};
|
||||
|
||||
const bufferCommand = (keywords) => {
|
||||
if (keywords.length === 0) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
let keywordsStr = keywords.join(' ');
|
||||
return browser.tabs.query({
|
||||
active: true, currentWindow: true
|
||||
}).then((gotTabs) => {
|
||||
if (gotTabs.length > 0) {
|
||||
if (isNaN(keywordsStr)) {
|
||||
return tabs.selectByKeyword(gotTabs[0], keywordsStr);
|
||||
}
|
||||
let index = parseInt(keywordsStr, 10) - 1;
|
||||
return tabs.selectAt(index);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getOpenCompletions = (command, keywords, searchConfig) => {
|
||||
return histories.getCompletions(keywords).then((pages) => {
|
||||
let historyItems = pages.map((page) => {
|
||||
return {
|
||||
caption: page.title,
|
||||
content: command + ' ' + page.url,
|
||||
url: page.url
|
||||
};
|
||||
});
|
||||
let engineNames = Object.keys(searchConfig.engines);
|
||||
let engineItems = engineNames.filter(name => name.startsWith(keywords))
|
||||
.map(name => ({
|
||||
caption: name,
|
||||
content: command + ' ' + name
|
||||
}));
|
||||
|
||||
let completions = [];
|
||||
if (engineItems.length > 0) {
|
||||
completions.push({
|
||||
name: 'Search Engines',
|
||||
items: engineItems
|
||||
});
|
||||
}
|
||||
if (historyItems.length > 0) {
|
||||
completions.push({
|
||||
name: 'History',
|
||||
items: historyItems
|
||||
});
|
||||
}
|
||||
return completions;
|
||||
});
|
||||
};
|
||||
|
||||
const doCommand = (line, settings) => {
|
||||
let words = line.trim().split(/ +/);
|
||||
let name = words.shift();
|
||||
|
||||
switch (name) {
|
||||
case 'o':
|
||||
case 'open':
|
||||
return openCommand(normalizeUrl(words, settings.search));
|
||||
case 't':
|
||||
case 'tabopen':
|
||||
return tabopenCommand(normalizeUrl(words, settings.search));
|
||||
case 'w':
|
||||
case 'winopen':
|
||||
return winopenCommand(normalizeUrl(words, settings.search));
|
||||
case 'b':
|
||||
case 'buffer':
|
||||
return bufferCommand(words);
|
||||
case '':
|
||||
return Promise.resolve();
|
||||
}
|
||||
throw new Error(name + ' command is not defined');
|
||||
};
|
||||
|
||||
const getCompletions = (line, settings) => {
|
||||
let typedWords = line.trim().split(/ +/);
|
||||
let typing = '';
|
||||
if (!line.endsWith(' ')) {
|
||||
typing = typedWords.pop();
|
||||
}
|
||||
|
||||
if (typedWords.length === 0) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
let name = typedWords.shift();
|
||||
let keywords = typedWords.concat(typing).join(' ');
|
||||
|
||||
switch (name) {
|
||||
case 'o':
|
||||
case 'open':
|
||||
case 't':
|
||||
case 'tabopen':
|
||||
case 'w':
|
||||
case 'winopen':
|
||||
return getOpenCompletions(name, keywords, settings.search);
|
||||
case 'b':
|
||||
case 'buffer':
|
||||
return tabs.getCompletions(keywords).then((gotTabs) => {
|
||||
let items = gotTabs.map((tab) => {
|
||||
return {
|
||||
caption: tab.title,
|
||||
content: name + ' ' + tab.title,
|
||||
url: tab.url,
|
||||
icon: tab.favIconUrl
|
||||
};
|
||||
});
|
||||
return [
|
||||
{
|
||||
name: 'Buffers',
|
||||
items: items
|
||||
}
|
||||
];
|
||||
});
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
};
|
||||
|
||||
const exec = (line, settings) => {
|
||||
return doCommand(line, settings);
|
||||
};
|
||||
|
||||
const complete = (line, settings) => {
|
||||
return getCompletions(line, settings);
|
||||
};
|
||||
|
||||
export { exec, complete };
|
84
src/shared/commands/complete.js
Normal file
84
src/shared/commands/complete.js
Normal file
|
@ -0,0 +1,84 @@
|
|||
import * as tabs from 'background/tabs';
|
||||
import * as histories from 'background/histories';
|
||||
|
||||
const getOpenCompletions = (command, keywords, searchConfig) => {
|
||||
return histories.getCompletions(keywords).then((pages) => {
|
||||
let historyItems = pages.map((page) => {
|
||||
return {
|
||||
caption: page.title,
|
||||
content: command + ' ' + page.url,
|
||||
url: page.url
|
||||
};
|
||||
});
|
||||
let engineNames = Object.keys(searchConfig.engines);
|
||||
let engineItems = engineNames.filter(name => name.startsWith(keywords))
|
||||
.map(name => ({
|
||||
caption: name,
|
||||
content: command + ' ' + name
|
||||
}));
|
||||
|
||||
let completions = [];
|
||||
if (engineItems.length > 0) {
|
||||
completions.push({
|
||||
name: 'Search Engines',
|
||||
items: engineItems
|
||||
});
|
||||
}
|
||||
if (historyItems.length > 0) {
|
||||
completions.push({
|
||||
name: 'History',
|
||||
items: historyItems
|
||||
});
|
||||
}
|
||||
return completions;
|
||||
});
|
||||
};
|
||||
|
||||
const getCompletions = (line, settings) => {
|
||||
let typedWords = line.trim().split(/ +/);
|
||||
let typing = '';
|
||||
if (!line.endsWith(' ')) {
|
||||
typing = typedWords.pop();
|
||||
}
|
||||
|
||||
if (typedWords.length === 0) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
let name = typedWords.shift();
|
||||
let keywords = typedWords.concat(typing).join(' ');
|
||||
|
||||
switch (name) {
|
||||
case 'o':
|
||||
case 'open':
|
||||
case 't':
|
||||
case 'tabopen':
|
||||
case 'w':
|
||||
case 'winopen':
|
||||
return getOpenCompletions(name, keywords, settings.search);
|
||||
case 'b':
|
||||
case 'buffer':
|
||||
return tabs.getCompletions(keywords).then((gotTabs) => {
|
||||
let items = gotTabs.map((tab) => {
|
||||
return {
|
||||
caption: tab.title,
|
||||
content: name + ' ' + tab.title,
|
||||
url: tab.url,
|
||||
icon: tab.favIconUrl
|
||||
};
|
||||
});
|
||||
return [
|
||||
{
|
||||
name: 'Buffers',
|
||||
items: items
|
||||
}
|
||||
];
|
||||
});
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
};
|
||||
|
||||
const complete = (line, settings) => {
|
||||
return getCompletions(line, settings);
|
||||
};
|
||||
|
||||
export default complete;
|
3
src/shared/commands/index.js
Normal file
3
src/shared/commands/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import complete from './complete';
|
||||
|
||||
export { complete };
|
59
src/shared/commands/parsers.js
Normal file
59
src/shared/commands/parsers.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
const normalizeUrl = (args, searchConfig) => {
|
||||
let concat = args.join(' ');
|
||||
try {
|
||||
return new URL(concat).href;
|
||||
} catch (e) {
|
||||
if (concat.includes('.') && !concat.includes(' ')) {
|
||||
return 'http://' + concat;
|
||||
}
|
||||
let query = concat;
|
||||
let template = searchConfig.engines[
|
||||
searchConfig.default
|
||||
];
|
||||
for (let key in searchConfig.engines) {
|
||||
if (args[0] === key) {
|
||||
query = args.slice(1).join(' ');
|
||||
template = searchConfig.engines[key];
|
||||
}
|
||||
}
|
||||
return template.replace('{}', encodeURIComponent(query));
|
||||
}
|
||||
};
|
||||
|
||||
const mustNumber = (v) => {
|
||||
let num = Number(v);
|
||||
if (isNaN(num)) {
|
||||
throw new Error('Not number: ' + v);
|
||||
}
|
||||
return num;
|
||||
};
|
||||
|
||||
const parseSetOption = (word, types) => {
|
||||
let [key, value] = word.split('=');
|
||||
if (value === undefined) {
|
||||
value = !key.startsWith('no');
|
||||
key = value ? key : key.slice(2);
|
||||
}
|
||||
let type = types[key];
|
||||
if (!type) {
|
||||
throw new Error('Unknown property: ' + key);
|
||||
}
|
||||
if (type === 'boolean' && typeof value !== 'boolean' ||
|
||||
type !== 'boolean' && typeof value === 'boolean') {
|
||||
throw new Error('Invalid argument: ' + word);
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'string': return [key, value];
|
||||
case 'number': return [key, mustNumber(value)];
|
||||
case 'boolean': return [key, value];
|
||||
}
|
||||
};
|
||||
|
||||
const parseCommandLine = (line) => {
|
||||
let words = line.trim().split(/ +/);
|
||||
let name = words.shift();
|
||||
return [name, words];
|
||||
};
|
||||
|
||||
export { normalizeUrl, parseCommandLine, parseSetOption };
|
|
@ -32,6 +32,7 @@ export default {
|
|||
CONSOLE_SHOW_ERROR: 'console.show.error',
|
||||
CONSOLE_SHOW_INFO: 'console.show.info',
|
||||
CONSOLE_SHOW_FIND: 'console.show.find',
|
||||
CONSOLE_HIDE: 'console.hide',
|
||||
|
||||
FOLLOW_START: 'follow.start',
|
||||
FOLLOW_REQUEST_COUNT_TARGETS: 'follow.request.count.targets',
|
||||
|
@ -44,6 +45,8 @@ export default {
|
|||
|
||||
FIND_NEXT: 'find.next',
|
||||
FIND_PREV: 'find.prev',
|
||||
FIND_GET_KEYWORD: 'find.get.keyword',
|
||||
FIND_SET_KEYWORD: 'find.set.keyword',
|
||||
|
||||
OPEN_URL: 'open.url',
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
export default {
|
||||
// Hide console, or cancel some user actions
|
||||
CANCEL: 'cancel',
|
||||
|
||||
// Addons
|
||||
ADDON_ENABLE: 'addon.enable',
|
||||
ADDON_DISABLE: 'addon.disable',
|
||||
|
@ -31,13 +34,18 @@ export default {
|
|||
NAVIGATE_PARENT: 'navigate.parent',
|
||||
NAVIGATE_ROOT: 'navigate.root',
|
||||
|
||||
// Focus
|
||||
FOCUS_INPUT: 'focus.input',
|
||||
|
||||
// Tabs
|
||||
TAB_CLOSE: 'tabs.close',
|
||||
TAB_CLOSE_FORCE: 'tabs.close.force',
|
||||
TAB_REOPEN: 'tabs.reopen',
|
||||
TAB_PREV: 'tabs.prev',
|
||||
TAB_NEXT: 'tabs.next',
|
||||
TAB_FIRST: 'tabs.first',
|
||||
TAB_LAST: 'tabs.last',
|
||||
TAB_PREV_SEL: 'tabs.prevsel',
|
||||
TAB_RELOAD: 'tabs.reload',
|
||||
TAB_PIN: 'tabs.pin',
|
||||
TAB_UNPIN: 'tabs.unpin',
|
||||
|
@ -49,8 +57,9 @@ export default {
|
|||
ZOOM_OUT: 'zoom.out',
|
||||
ZOOM_NEUTRAL: 'zoom.neutral',
|
||||
|
||||
// Url yank
|
||||
// Url yank/paste
|
||||
URLS_YANK: 'urls.yank',
|
||||
URLS_PASTE: 'urls.paste',
|
||||
|
||||
// Find
|
||||
FIND_START: 'find.start',
|
||||
|
|
|
@ -15,8 +15,6 @@ export default {
|
|||
"j": { "type": "scroll.vertically", "count": 1 },
|
||||
"h": { "type": "scroll.horizonally", "count": -1 },
|
||||
"l": { "type": "scroll.horizonally", "count": 1 },
|
||||
"<C-Y>": { "type": "scroll.vertically", "count": -1 },
|
||||
"<C-E>": { "type": "scroll.vertically", "count": 1 },
|
||||
"<C-U>": { "type": "scroll.pages", "count": -0.5 },
|
||||
"<C-D>": { "type": "scroll.pages", "count": 0.5 },
|
||||
"<C-B>": { "type": "scroll.pages", "count": -1 },
|
||||
|
@ -25,6 +23,7 @@ export default {
|
|||
"G": { "type": "scroll.bottom" },
|
||||
"$": { "type": "scroll.end" },
|
||||
"d": { "type": "tabs.close" },
|
||||
"!d": { "type": "tabs.close.force" },
|
||||
"u": { "type": "tabs.reopen" },
|
||||
"K": { "type": "tabs.prev", "count": 1 },
|
||||
"J": { "type": "tabs.next", "count": 1 },
|
||||
|
@ -32,6 +31,7 @@ export default {
|
|||
"gt": { "type": "tabs.next", "count": 1 },
|
||||
"g0": { "type": "tabs.first" },
|
||||
"g$": { "type": "tabs.last" },
|
||||
"<C-6>": { "type": "tabs.prevsel" },
|
||||
"r": { "type": "tabs.reload", "cache": false },
|
||||
"R": { "type": "tabs.reload", "cache": true },
|
||||
"zp": { "type": "tabs.pin.toggle" },
|
||||
|
@ -39,15 +39,18 @@ export default {
|
|||
"zi": { "type": "zoom.in" },
|
||||
"zo": { "type": "zoom.out" },
|
||||
"zz": { "type": "zoom.neutral" },
|
||||
"f": { "type": "follow.start", "newTab": false },
|
||||
"F": { "type": "follow.start", "newTab": true },
|
||||
"f": { "type": "follow.start", "newTab": false, "background": false },
|
||||
"F": { "type": "follow.start", "newTab": true, "background": false },
|
||||
"H": { "type": "navigate.history.prev" },
|
||||
"L": { "type": "navigate.history.next" },
|
||||
"[[": { "type": "navigate.link.prev" },
|
||||
"]]": { "type": "navigate.link.next" },
|
||||
"gu": { "type": "navigate.parent" },
|
||||
"gU": { "type": "navigate.root" },
|
||||
"gi": { "type": "focus.input" },
|
||||
"y": { "type": "urls.yank" },
|
||||
"p": { "type": "urls.paste", "newTab": false },
|
||||
"P": { "type": "urls.paste", "newTab": true },
|
||||
"/": { "type": "find.start" },
|
||||
"n": { "type": "find.next" },
|
||||
"N": { "type": "find.prev" },
|
||||
|
@ -63,6 +66,8 @@ export default {
|
|||
"twitter": "https://twitter.com/search?q={}",
|
||||
"wikipedia": "https://en.wikipedia.org/w/index.php?search={}"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
}
|
||||
}`
|
||||
}`,
|
||||
};
|
18
src/shared/settings/properties.js
Normal file
18
src/shared/settings/properties.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
// describe types of a propety as:
|
||||
// mystr: 'string',
|
||||
// mynum: 'number',
|
||||
// mybool: 'boolean',
|
||||
const types = {
|
||||
hintchars: 'string',
|
||||
smoothscroll: 'boolean',
|
||||
adjacenttab: 'boolean',
|
||||
};
|
||||
|
||||
// describe default values of a property
|
||||
const defaults = {
|
||||
hintchars: 'abcdefghijklmnopqrstuvwxyz',
|
||||
smoothscroll: false,
|
||||
adjacenttab: true,
|
||||
};
|
||||
|
||||
export { types, defaults };
|
36
src/shared/settings/storage.js
Normal file
36
src/shared/settings/storage.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
import DefaultSettings from './default';
|
||||
import * as settingsValues from './values';
|
||||
|
||||
const loadRaw = () => {
|
||||
return browser.storage.local.get('settings').then(({ settings }) => {
|
||||
if (!settings) {
|
||||
return DefaultSettings;
|
||||
}
|
||||
return Object.assign({}, DefaultSettings, settings);
|
||||
});
|
||||
};
|
||||
|
||||
const loadValue = () => {
|
||||
return loadRaw().then((settings) => {
|
||||
let value = JSON.parse(DefaultSettings.json);
|
||||
if (settings.source === 'json') {
|
||||
value = settingsValues.valueFromJson(settings.json);
|
||||
} else if (settings.source === 'form') {
|
||||
value = settingsValues.valueFromForm(settings.form);
|
||||
}
|
||||
if (!value.properties) {
|
||||
value.properties = {};
|
||||
}
|
||||
return Object.assign({},
|
||||
settingsValues.valueFromJson(DefaultSettings.json),
|
||||
value);
|
||||
});
|
||||
};
|
||||
|
||||
const save = (settings) => {
|
||||
return browser.storage.local.set({
|
||||
settings,
|
||||
});
|
||||
};
|
||||
|
||||
export { loadRaw, loadValue, save };
|
|
@ -1,6 +1,7 @@
|
|||
import operations from 'shared/operations';
|
||||
import * as properties from './properties';
|
||||
|
||||
const VALID_TOP_KEYS = ['keymaps', 'search', 'blacklist'];
|
||||
const VALID_TOP_KEYS = ['keymaps', 'search', 'blacklist', 'properties'];
|
||||
const VALID_OPERATION_VALUES = Object.keys(operations).map((key) => {
|
||||
return operations[key];
|
||||
});
|
||||
|
@ -48,6 +49,17 @@ const validateSearch = (search) => {
|
|||
}
|
||||
};
|
||||
|
||||
const validateProperties = (props) => {
|
||||
for (let name of Object.keys(props)) {
|
||||
if (!properties.types[name]) {
|
||||
throw new Error(`Unknown property name: "${name}"`);
|
||||
}
|
||||
if (typeof props[name] !== properties.types[name]) {
|
||||
throw new Error(`Invalid type for property: "${name}"`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const validate = (settings) => {
|
||||
validateInvalidTopKeys(settings);
|
||||
if (settings.keymaps) {
|
||||
|
@ -56,6 +68,9 @@ const validate = (settings) => {
|
|||
if (settings.search) {
|
||||
validateSearch(settings.search);
|
||||
}
|
||||
if (settings.properties) {
|
||||
validateProperties(settings.properties);
|
||||
}
|
||||
};
|
||||
|
||||
export { validate };
|
108
src/shared/settings/values.js
Normal file
108
src/shared/settings/values.js
Normal file
|
@ -0,0 +1,108 @@
|
|||
import * as properties from './properties';
|
||||
|
||||
const operationFromFormName = (name) => {
|
||||
let [type, argStr] = name.split('?');
|
||||
let args = {};
|
||||
if (argStr) {
|
||||
args = JSON.parse(argStr);
|
||||
}
|
||||
return Object.assign({ type }, args);
|
||||
};
|
||||
|
||||
const operationToFormName = (op) => {
|
||||
let type = op.type;
|
||||
let args = Object.assign({}, op);
|
||||
delete args.type;
|
||||
|
||||
if (Object.keys(args).length === 0) {
|
||||
return type;
|
||||
}
|
||||
return op.type + '?' + JSON.stringify(args);
|
||||
};
|
||||
|
||||
const valueFromJson = (json) => {
|
||||
return JSON.parse(json);
|
||||
};
|
||||
|
||||
const valueFromForm = (form) => {
|
||||
let keymaps = undefined;
|
||||
if (form.keymaps) {
|
||||
keymaps = {};
|
||||
for (let name of Object.keys(form.keymaps)) {
|
||||
let keys = form.keymaps[name];
|
||||
keymaps[keys] = operationFromFormName(name);
|
||||
}
|
||||
}
|
||||
|
||||
let search = undefined;
|
||||
if (form.search) {
|
||||
search = { default: form.search.default };
|
||||
|
||||
if (form.search.engines) {
|
||||
search.engines = {};
|
||||
for (let [name, url] of form.search.engines) {
|
||||
search.engines[name] = url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
keymaps,
|
||||
search,
|
||||
blacklist: form.blacklist,
|
||||
properties: form.properties
|
||||
};
|
||||
};
|
||||
|
||||
const jsonFromValue = (value) => {
|
||||
return JSON.stringify(value, undefined, 2);
|
||||
};
|
||||
|
||||
const formFromValue = (value, allowedOps) => {
|
||||
let keymaps = undefined;
|
||||
|
||||
if (value.keymaps) {
|
||||
let allowedSet = new Set(allowedOps);
|
||||
|
||||
keymaps = {};
|
||||
for (let keys of Object.keys(value.keymaps)) {
|
||||
let op = operationToFormName(value.keymaps[keys]);
|
||||
if (allowedSet.has(op)) {
|
||||
keymaps[op] = keys;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let search = undefined;
|
||||
if (value.search) {
|
||||
search = { default: value.search.default };
|
||||
if (value.search.engines) {
|
||||
search.engines = Object.keys(value.search.engines).map((name) => {
|
||||
return [name, value.search.engines[name]];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let formProperties = Object.assign({}, properties.defaults, value.properties);
|
||||
|
||||
return {
|
||||
keymaps,
|
||||
search,
|
||||
blacklist: value.blacklist,
|
||||
properties: formProperties,
|
||||
};
|
||||
};
|
||||
|
||||
const jsonFromForm = (form) => {
|
||||
return jsonFromValue(valueFromForm(form));
|
||||
};
|
||||
|
||||
const formFromJson = (json, allowedOps) => {
|
||||
let value = valueFromJson(json);
|
||||
return formFromValue(value, allowedOps);
|
||||
};
|
||||
|
||||
export {
|
||||
valueFromJson, valueFromForm, jsonFromValue, formFromValue,
|
||||
jsonFromForm, formFromJson
|
||||
};
|
|
@ -1,18 +1,15 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
class Provider extends React.PureComponent {
|
||||
class Provider extends Component {
|
||||
getChildContext() {
|
||||
return { store: this.props.store };
|
||||
}
|
||||
|
||||
render() {
|
||||
return React.Children.only(this.props.children);
|
||||
return <div>
|
||||
{ this.props.children }
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
Provider.childContextTypes = {
|
||||
store: PropTypes.any,
|
||||
};
|
||||
|
||||
export default Provider;
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue