commit
9da2f5fd78
119 changed files with 19769 additions and 6761 deletions
@ -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 |
@ -1,3 +1,4 @@ |
||||
/node_modules/ |
||||
/build/ |
||||
/e2e/ambassador/build/ |
||||
*.zip |
||||
|
@ -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 |
@ -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" |
||||
] |
||||
} |
@ -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 |
||||
); |
||||
} |
||||
}); |
@ -0,0 +1,7 @@ |
||||
const receiveContentMessage = (func) => { |
||||
browser.runtime.onMessage.addListener((message) => { |
||||
return func(message); |
||||
}); |
||||
}; |
||||
|
||||
export { receiveContentMessage }; |
@ -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 |
||||
}; |
@ -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 }; |
@ -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 }; |
@ -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 }; |
@ -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 }; |
@ -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 }; |
@ -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 }; |
@ -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({}); |
||||
}); |
@ -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, |
||||
}; |
@ -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 }; |
@ -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, |
||||
}; |
@ -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 |
||||
|
@ -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); |
||||
}); |
||||
}); |
||||
}); |
@ -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); |
||||
}); |
||||
}); |
||||
}); |
@ -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); |
||||
}); |
||||
}); |
||||
}); |
@ -0,0 +1,10 @@ |
||||
'use strict'; |
||||
|
||||
window.__karma__.start = (function(start){ |
||||
return function(){ |
||||
var args = arguments |
||||
setTimeout(() => { |
||||
start(args) |
||||
}, 3000); |
||||
}; |
||||
}(window.__karma__.start)); |
@ -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], |
||||
} |
||||
|
@ -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 |
||||
} |
||||
} |
||||
}) |
||||
} |
@ -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'); |
File diff suppressed because it is too large
Load Diff
@ -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); |
@ -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 }; |
@ -0,0 +1,10 @@ |
||||
import actions from './index'; |
||||
|
||||
const setKeyword = (keyword) => { |
||||
return { |
||||
type: actions.FIND_SET_KEYWORD, |
||||
keyword, |
||||
}; |
||||
}; |
||||
|
||||
export { setKeyword }; |
@ -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', |
||||
}; |
@ -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 }; |
||||
|
@ -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), |
||||
}); |
||||
} |
||||
|
@ -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; |
||||
} |
||||
} |
||||
|
@ -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 }; |
@ -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; |
@ -0,0 +1,9 @@ |
||||
.form-blacklist-form { |
||||
&-row { |
||||
display: flex; |
||||
|
||||
.column-url { |
||||
flex: 1; |
||||
} |
||||
} |
||||
} |
@ -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; |
@ -0,0 +1,11 @@ |
||||
.form-keymaps-form { |
||||
column-count: 3; |
||||
|
||||
&-field-group { |
||||
margin-top: 24px; |
||||
} |
||||
|
||||
&-field-group:first-of-type { |
||||
margin-top: 24px; |
||||
} |
||||
} |
@ -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; |
@ -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; |
||||
} |
||||
} |
||||
} |
@ -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; |
@ -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,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; |
||||
} |
||||
} |
||||
} |
||||
|
@ -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; |
@ -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; |
||||
} |
||||
} |
@ -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; |
@ -0,0 +1,13 @@ |
||||
|
||||
.ui-delete-button { |
||||
border: none; |
||||
padding: 4; |
||||
display: inline; |
||||
background: none; |
||||
color: red; |
||||
cursor: pointer; |
||||
|
||||
&:hover { |
||||
color: darkred; |
||||
} |
||||
} |
@ -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; |
@ -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,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 }; |
@ -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; |
@ -0,0 +1,3 @@ |
||||
import complete from './complete'; |
||||
|
||||
export { complete }; |
@ -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 }; |
@ -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 }; |
@ -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 }; |
@ -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 new issue