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/ |
/node_modules/ |
||||||
/build/ |
/build/ |
||||||
|
/e2e/ambassador/build/ |
||||||
*.zip |
*.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) => { |
const openNewTab = (url, openerTabId, background = false, adjacent = false) => { |
||||||
return browser.tabs.create({ url: url }); |
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) => { |
const openToTab = (url, tab) => { |
||||||
return browser.tabs.update(tab.id, { url: url }); |
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
|
// Make setting reducer instead of re-use
|
||||||
const defaultState = { |
const defaultState = { |
||||||
setting: settingReducer(undefined, {}), |
setting: settingReducer(undefined, {}), |
||||||
|
find: findReducer(undefined, {}), |
||||||
}; |
}; |
||||||
|
|
||||||
export default function reducer(state = defaultState, action = {}) { |
export default function reducer(state = defaultState, action = {}) { |
||||||
return Object.assign({}, state, { |
return Object.assign({}, state, { |
||||||
setting: settingReducer(state.setting, action), |
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 { |
.vimvixen-settings-form { |
||||||
|
padding: 2px; |
||||||
|
|
||||||
textarea[name=json] { |
textarea[name=json] { |
||||||
font-family: monospace; |
font-family: monospace; |
||||||
width: 100%; |
width: 100%; |
||||||
min-height: 64ex; |
min-height: 64ex; |
||||||
resize: vertical; |
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 { h, Component } from 'preact'; |
||||||
import PropTypes from 'prop-types'; |
|
||||||
|
|
||||||
class Provider extends React.PureComponent { |
class Provider extends Component { |
||||||
getChildContext() { |
getChildContext() { |
||||||
return { store: this.props.store }; |
return { store: this.props.store }; |
||||||
} |
} |
||||||
|
|
||||||
render() { |
render() { |
||||||
return React.Children.only(this.props.children); |
return <div> |
||||||
|
{ this.props.children } |
||||||
|
</div>; |
||||||
} |
} |
||||||
} |
} |
||||||
|
|
||||||
Provider.childContextTypes = { |
|
||||||
store: PropTypes.any, |
|
||||||
}; |
|
||||||
|
|
||||||
export default Provider; |
export default Provider; |
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in new issue