Merge remote-tracking branch 'origin/master' into patch-1
This commit is contained in:
commit
9da2f5fd78
119 changed files with 14640 additions and 1632 deletions
79
src/background/actions/command.js
Normal file
79
src/background/actions/command.js
Normal file
|
@ -0,0 +1,79 @@
|
|||
import actions from '../actions';
|
||||
import * as tabs from 'background/tabs';
|
||||
import * as parsers from 'shared/commands/parsers';
|
||||
import * as properties from 'shared/settings/properties';
|
||||
|
||||
const openCommand = (url) => {
|
||||
return browser.tabs.query({
|
||||
active: true, currentWindow: true
|
||||
}).then((gotTabs) => {
|
||||
if (gotTabs.length > 0) {
|
||||
return browser.tabs.update(gotTabs[0].id, { url: url });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const tabopenCommand = (url) => {
|
||||
return browser.tabs.create({ url: url });
|
||||
};
|
||||
|
||||
const winopenCommand = (url) => {
|
||||
return browser.windows.create({ url });
|
||||
};
|
||||
|
||||
const bufferCommand = (keywords) => {
|
||||
if (keywords.length === 0) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
let keywordsStr = keywords.join(' ');
|
||||
return browser.tabs.query({
|
||||
active: true, currentWindow: true
|
||||
}).then((gotTabs) => {
|
||||
if (gotTabs.length > 0) {
|
||||
if (isNaN(keywordsStr)) {
|
||||
return tabs.selectByKeyword(gotTabs[0], keywordsStr);
|
||||
}
|
||||
let index = parseInt(keywordsStr, 10) - 1;
|
||||
return tabs.selectAt(index);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const setCommand = (args) => {
|
||||
if (!args[0]) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
let [name, value] = parsers.parseSetOption(args[0], properties.types);
|
||||
return {
|
||||
type: actions.SETTING_SET_PROPERTY,
|
||||
name,
|
||||
value
|
||||
};
|
||||
};
|
||||
|
||||
const exec = (line, settings) => {
|
||||
let [name, args] = parsers.parseCommandLine(line);
|
||||
|
||||
switch (name) {
|
||||
case 'o':
|
||||
case 'open':
|
||||
return openCommand(parsers.normalizeUrl(args, settings.search));
|
||||
case 't':
|
||||
case 'tabopen':
|
||||
return tabopenCommand(parsers.normalizeUrl(args, settings.search));
|
||||
case 'w':
|
||||
case 'winopen':
|
||||
return winopenCommand(parsers.normalizeUrl(args, settings.search));
|
||||
case 'b':
|
||||
case 'buffer':
|
||||
return bufferCommand(args);
|
||||
case 'set':
|
||||
return setCommand(args);
|
||||
case '':
|
||||
return Promise.resolve();
|
||||
}
|
||||
throw new Error(name + ' command is not defined');
|
||||
};
|
||||
|
||||
export { exec };
|
10
src/background/actions/find.js
Normal file
10
src/background/actions/find.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
import actions from './index';
|
||||
|
||||
const setKeyword = (keyword) => {
|
||||
return {
|
||||
type: actions.FIND_SET_KEYWORD,
|
||||
keyword,
|
||||
};
|
||||
};
|
||||
|
||||
export { setKeyword };
|
8
src/background/actions/index.js
Normal file
8
src/background/actions/index.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
export default {
|
||||
// Settings
|
||||
SETTING_SET_SETTINGS: 'setting.set.settings',
|
||||
SETTING_SET_PROPERTY: 'setting.set.property',
|
||||
|
||||
// Find
|
||||
FIND_SET_KEYWORD: 'find.set.keyword',
|
||||
};
|
|
@ -17,6 +17,8 @@ const exec = (operation, tab) => {
|
|||
switch (operation.type) {
|
||||
case operations.TAB_CLOSE:
|
||||
return tabs.closeTab(tab.id);
|
||||
case operations.TAB_CLOSE_FORCE:
|
||||
return tabs.closeTabForce(tab.id);
|
||||
case operations.TAB_REOPEN:
|
||||
return tabs.reopenTab();
|
||||
case operations.TAB_PREV:
|
||||
|
@ -27,6 +29,8 @@ const exec = (operation, tab) => {
|
|||
return tabs.selectFirstTab();
|
||||
case operations.TAB_LAST:
|
||||
return tabs.selectLastTab();
|
||||
case operations.TAB_PREV_SEL:
|
||||
return tabs.selectPrevSelTab();
|
||||
case operations.TAB_RELOAD:
|
||||
return tabs.reload(tab, operation.cache);
|
||||
case operations.TAB_PIN:
|
||||
|
@ -69,6 +73,10 @@ const exec = (operation, tab) => {
|
|||
return browser.tabs.sendMessage(tab.id, {
|
||||
type: messages.CONSOLE_SHOW_FIND
|
||||
});
|
||||
case operations.CANCEL:
|
||||
return browser.tabs.sendMessage(tab.id, {
|
||||
type: messages.CONSOLE_HIDE,
|
||||
});
|
||||
default:
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
|
21
src/background/actions/setting.js
Normal file
21
src/background/actions/setting.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
import actions from '../actions';
|
||||
import * as settingsStorage from 'shared/settings/storage';
|
||||
|
||||
const load = () => {
|
||||
return settingsStorage.loadValue().then((value) => {
|
||||
return {
|
||||
type: actions.SETTING_SET_SETTINGS,
|
||||
value,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const setProperty = (name, value) => {
|
||||
return {
|
||||
type: actions.SETTING_SET_PROPERTY,
|
||||
name,
|
||||
value,
|
||||
};
|
||||
};
|
||||
|
||||
export { load, setProperty };
|
|
@ -1,9 +1,21 @@
|
|||
const openNewTab = (url) => {
|
||||
return browser.tabs.create({ url: url });
|
||||
const openNewTab = (url, openerTabId, background = false, adjacent = false) => {
|
||||
if (adjacent) {
|
||||
return browser.tabs.query({
|
||||
active: true, currentWindow: true
|
||||
}).then((tabs) => {
|
||||
return browser.tabs.create({
|
||||
url,
|
||||
openerTabId,
|
||||
active: !background,
|
||||
index: tabs[0].index + 1
|
||||
});
|
||||
});
|
||||
}
|
||||
return browser.tabs.create({ url, active: !background });
|
||||
};
|
||||
|
||||
const openToTab = (url, tab) => {
|
||||
return browser.tabs.update(tab.id, { url: url });
|
||||
};
|
||||
|
||||
export { openToTab, openNewTab };
|
||||
export { openNewTab, openToTab };
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import messages from 'shared/messages';
|
||||
import * as operationActions from 'background/actions/operation';
|
||||
import * as settingsActions from 'settings/actions/setting';
|
||||
import * as commandActions from 'background/actions/command';
|
||||
import * as settingActions from 'background/actions/setting';
|
||||
import * as findActions from 'background/actions/find';
|
||||
import * as tabActions from 'background/actions/tab';
|
||||
import * as commands from 'shared/commands';
|
||||
|
||||
|
@ -22,6 +24,8 @@ export default class BackgroundComponent {
|
|||
|
||||
onMessage(message, sender) {
|
||||
let settings = this.store.getState().setting;
|
||||
let find = this.store.getState().find;
|
||||
|
||||
switch (message.type) {
|
||||
case messages.BACKGROUND_OPERATION:
|
||||
return this.store.dispatch(
|
||||
|
@ -29,25 +33,31 @@ export default class BackgroundComponent {
|
|||
sender);
|
||||
case messages.OPEN_URL:
|
||||
if (message.newTab) {
|
||||
return this.store.dispatch(
|
||||
tabActions.openNewTab(message.url), sender);
|
||||
let action = tabActions.openNewTab(
|
||||
message.url, sender.tab.id, message.background,
|
||||
settings.value.properties.adjacenttab);
|
||||
return this.store.dispatch(action, sender);
|
||||
}
|
||||
return this.store.dispatch(
|
||||
tabActions.openToTab(message.url, sender.tab), sender);
|
||||
case messages.CONSOLE_ENTER_COMMAND:
|
||||
return commands.exec(message.text, settings.value).catch((e) => {
|
||||
return browser.tabs.sendMessage(sender.tab.id, {
|
||||
type: messages.CONSOLE_SHOW_ERROR,
|
||||
text: e.message,
|
||||
});
|
||||
});
|
||||
this.store.dispatch(
|
||||
commandActions.exec(message.text, settings.value),
|
||||
sender
|
||||
);
|
||||
return this.broadcastSettingsChanged();
|
||||
case messages.SETTINGS_QUERY:
|
||||
return Promise.resolve(this.store.getState().setting.value);
|
||||
case messages.CONSOLE_QUERY_COMPLETIONS:
|
||||
return commands.complete(message.text, settings.value);
|
||||
case messages.SETTINGS_RELOAD:
|
||||
this.store.dispatch(settingsActions.load());
|
||||
this.store.dispatch(settingActions.load());
|
||||
return this.broadcastSettingsChanged();
|
||||
case messages.FIND_GET_KEYWORD:
|
||||
return Promise.resolve(find.keyword);
|
||||
case messages.FIND_SET_KEYWORD:
|
||||
this.store.dispatch(findActions.setKeyword(message.keyword));
|
||||
return Promise.resolve({});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -76,7 +76,6 @@ const getCompletions = (keyword) => {
|
|||
.sort((x, y) => x[0].visitCount < y[0].visitCount)
|
||||
.slice(0, 10)
|
||||
.map(item => item[0])
|
||||
.sort((x, y) => x.url > y.url)
|
||||
)[0];
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import * as settingsActions from 'settings/actions/setting';
|
||||
import * as settingActions from 'background/actions/setting';
|
||||
import messages from 'shared/messages';
|
||||
import BackgroundComponent from 'background/components/background';
|
||||
import reducers from 'background/reducers';
|
||||
|
@ -16,4 +16,4 @@ const store = createStore(reducers, (e, sender) => {
|
|||
// eslint-disable-next-line no-unused-vars
|
||||
const backgroundComponent = new BackgroundComponent(store);
|
||||
|
||||
store.dispatch(settingsActions.load());
|
||||
store.dispatch(settingActions.load());
|
||||
|
|
16
src/background/reducers/find.js
Normal file
16
src/background/reducers/find.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
import actions from 'content/actions';
|
||||
|
||||
const defaultState = {
|
||||
keyword: null,
|
||||
};
|
||||
|
||||
export default function reducer(state = defaultState, action = {}) {
|
||||
switch (action.type) {
|
||||
case actions.FIND_SET_KEYWORD:
|
||||
return Object.assign({}, state, {
|
||||
keyword: action.keyword,
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -1,12 +1,15 @@
|
|||
import settingReducer from 'settings/reducers/setting';
|
||||
import settingReducer from './setting';
|
||||
import findReducer from './find';
|
||||
|
||||
// Make setting reducer instead of re-use
|
||||
const defaultState = {
|
||||
setting: settingReducer(undefined, {}),
|
||||
find: findReducer(undefined, {}),
|
||||
};
|
||||
|
||||
export default function reducer(state = defaultState, action = {}) {
|
||||
return Object.assign({}, state, {
|
||||
setting: settingReducer(state.setting, action),
|
||||
find: findReducer(state.find, action),
|
||||
});
|
||||
}
|
||||
|
|
24
src/background/reducers/setting.js
Normal file
24
src/background/reducers/setting.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import actions from 'background/actions';
|
||||
|
||||
const defaultState = {
|
||||
value: {},
|
||||
};
|
||||
|
||||
export default function reducer(state = defaultState, action = {}) {
|
||||
switch (action.type) {
|
||||
case actions.SETTING_SET_SETTINGS:
|
||||
return {
|
||||
value: action.value,
|
||||
};
|
||||
case actions.SETTING_SET_PROPERTY:
|
||||
return {
|
||||
value: Object.assign({}, state.value, {
|
||||
properties: Object.assign({}, state.value.properties,
|
||||
{ [action.name]: action.value })
|
||||
})
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,22 @@
|
|||
let prevSelTab = 1;
|
||||
let currSelTab = 1;
|
||||
|
||||
browser.tabs.onActivated.addListener((activeInfo) => {
|
||||
return browser.tabs.query({ currentWindow: true }).then(() => {
|
||||
prevSelTab = currSelTab;
|
||||
currSelTab = activeInfo.tabId;
|
||||
});
|
||||
});
|
||||
|
||||
const closeTab = (id) => {
|
||||
return browser.tabs.get(id).then((tab) => {
|
||||
if (!tab.pinned) {
|
||||
return browser.tabs.remove(id);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const closeTabForce = (id) => {
|
||||
return browser.tabs.remove(id);
|
||||
};
|
||||
|
||||
|
@ -51,7 +69,7 @@ const selectByKeyword = (current, keyword) => {
|
|||
const getCompletions = (keyword) => {
|
||||
return browser.tabs.query({ currentWindow: true }).then((tabs) => {
|
||||
let matched = tabs.filter((t) => {
|
||||
return t.url.includes(keyword) || t.title.includes(keyword);
|
||||
return t.url.includes(keyword) || t.title && t.title.includes(keyword);
|
||||
});
|
||||
return matched;
|
||||
});
|
||||
|
@ -93,6 +111,10 @@ const selectLastTab = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const selectPrevSelTab = () => {
|
||||
return browser.tabs.update(prevSelTab, { active: true });
|
||||
};
|
||||
|
||||
const reload = (current, cache) => {
|
||||
return browser.tabs.reload(
|
||||
current.id,
|
||||
|
@ -116,7 +138,8 @@ const duplicate = (id) => {
|
|||
};
|
||||
|
||||
export {
|
||||
closeTab, reopenTab, selectAt, selectByKeyword, getCompletions,
|
||||
selectPrevTab, selectNextTab, selectFirstTab, selectLastTab, reload,
|
||||
updateTabPinned, toggleTabPinned, duplicate
|
||||
closeTab, closeTabForce, reopenTab, selectAt, selectByKeyword,
|
||||
getCompletions, selectPrevTab, selectNextTab, selectFirstTab,
|
||||
selectLastTab, selectPrevSelTab, reload, updateTabPinned,
|
||||
toggleTabPinned, duplicate
|
||||
};
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
import actions from 'console/actions';
|
||||
|
||||
const hide = () => {
|
||||
return {
|
||||
type: actions.CONSOLE_HIDE,
|
||||
};
|
||||
};
|
||||
|
||||
const showCommand = (text) => {
|
||||
return {
|
||||
type: actions.CONSOLE_SHOW_COMMAND,
|
||||
|
@ -61,6 +67,6 @@ const completionPrev = () => {
|
|||
};
|
||||
|
||||
export {
|
||||
showCommand, showFind, showError, showInfo, hideCommand, setConsoleText,
|
||||
hide, showCommand, showFind, showError, showInfo, hideCommand, setConsoleText,
|
||||
setCompletions, completionNext, completionPrev
|
||||
};
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
export default {
|
||||
// console commands
|
||||
CONSOLE_HIDE: 'console.hide',
|
||||
CONSOLE_SHOW_COMMAND: 'console.show.command',
|
||||
CONSOLE_SHOW_ERROR: 'console.show.error',
|
||||
CONSOLE_SHOW_INFO: 'console.show.info',
|
||||
|
|
|
@ -31,14 +31,33 @@ export default class ConsoleComponent {
|
|||
}
|
||||
}
|
||||
|
||||
doEnter(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
return this.onEntered(e.target.value);
|
||||
}
|
||||
|
||||
selectNext(e) {
|
||||
this.store.dispatch(consoleActions.completionNext());
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
selectPrev(e) {
|
||||
this.store.dispatch(consoleActions.completionPrev());
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
onKeyDown(e) {
|
||||
if (e.keyCode === KeyboardEvent.DOM_VK_ESCAPE && e.ctrlKey) {
|
||||
return this.hideCommand();
|
||||
}
|
||||
switch (e.keyCode) {
|
||||
case KeyboardEvent.DOM_VK_ESCAPE:
|
||||
return this.hideCommand();
|
||||
case KeyboardEvent.DOM_VK_RETURN:
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
return this.onEntered(e.target.value);
|
||||
return this.doEnter(e);
|
||||
case KeyboardEvent.DOM_VK_TAB:
|
||||
if (e.shiftKey) {
|
||||
this.store.dispatch(consoleActions.completionPrev());
|
||||
|
@ -48,6 +67,26 @@ export default class ConsoleComponent {
|
|||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
break;
|
||||
case KeyboardEvent.DOM_VK_OPEN_BRACKET:
|
||||
if (e.ctrlKey) {
|
||||
return this.hideCommand();
|
||||
}
|
||||
break;
|
||||
case KeyboardEvent.DOM_VK_M:
|
||||
if (e.ctrlKey) {
|
||||
return this.doEnter(e);
|
||||
}
|
||||
break;
|
||||
case KeyboardEvent.DOM_VK_N:
|
||||
if (e.ctrlKey) {
|
||||
this.selectNext(e);
|
||||
}
|
||||
break;
|
||||
case KeyboardEvent.DOM_VK_P:
|
||||
if (e.ctrlKey) {
|
||||
this.selectPrev(e);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,8 @@ const onMessage = (message) => {
|
|||
return store.dispatch(consoleActions.showError(message.text));
|
||||
case messages.CONSOLE_SHOW_INFO:
|
||||
return store.dispatch(consoleActions.showInfo(message.text));
|
||||
case messages.CONSOLE_HIDE:
|
||||
return store.dispatch(consoleActions.hide());
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -53,6 +53,10 @@ const nextConsoleText = (completions, group, item, defaults) => {
|
|||
|
||||
export default function reducer(state = defaultState, action = {}) {
|
||||
switch (action.type) {
|
||||
case actions.CONSOLE_HIDE:
|
||||
return Object.assign({}, state, {
|
||||
mode: '',
|
||||
});
|
||||
case actions.CONSOLE_SHOW_COMMAND:
|
||||
return Object.assign({}, state, {
|
||||
mode: 'command',
|
||||
|
|
|
@ -69,6 +69,8 @@ body {
|
|||
|
||||
&-message {
|
||||
@include consoole-font;
|
||||
|
||||
border-top: 1px solid gray;
|
||||
}
|
||||
|
||||
&-error {
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
// NOTE: window.find is not standard API
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Window/find
|
||||
|
||||
import messages from 'shared/messages';
|
||||
import actions from 'content/actions';
|
||||
import * as consoleFrames from '../console-frames';
|
||||
|
||||
|
@ -14,6 +15,13 @@ const postPatternNotFound = (pattern) => {
|
|||
'Pattern not found: ' + pattern);
|
||||
};
|
||||
|
||||
const postPatternFound = (pattern) => {
|
||||
return consoleFrames.postInfo(
|
||||
window.document,
|
||||
'Pattern found: ' + pattern,
|
||||
);
|
||||
};
|
||||
|
||||
const find = (string, backwards) => {
|
||||
let caseSensitive = false;
|
||||
let wrapScan = true;
|
||||
|
@ -24,32 +32,49 @@ const find = (string, backwards) => {
|
|||
return window.find(string, caseSensitive, backwards, wrapScan);
|
||||
};
|
||||
|
||||
const findNext = (keyword, reset, backwards) => {
|
||||
const findNext = (currentKeyword, reset, backwards) => {
|
||||
if (reset) {
|
||||
window.getSelection().removeAllRanges();
|
||||
}
|
||||
|
||||
let found = find(keyword, backwards);
|
||||
if (!found) {
|
||||
window.getSelection().removeAllRanges();
|
||||
found = find(keyword, backwards);
|
||||
let promise = Promise.resolve(currentKeyword);
|
||||
if (currentKeyword) {
|
||||
browser.runtime.sendMessage({
|
||||
type: messages.FIND_SET_KEYWORD,
|
||||
keyword: currentKeyword,
|
||||
});
|
||||
} else {
|
||||
promise = browser.runtime.sendMessage({
|
||||
type: messages.FIND_GET_KEYWORD,
|
||||
});
|
||||
}
|
||||
if (!found) {
|
||||
postPatternNotFound(keyword);
|
||||
}
|
||||
return {
|
||||
type: actions.FIND_SET_KEYWORD,
|
||||
keyword,
|
||||
found,
|
||||
};
|
||||
|
||||
return promise.then((keyword) => {
|
||||
let found = find(keyword, backwards);
|
||||
if (!found) {
|
||||
window.getSelection().removeAllRanges();
|
||||
found = find(keyword, backwards);
|
||||
}
|
||||
if (found) {
|
||||
postPatternFound(keyword);
|
||||
} else {
|
||||
postPatternNotFound(keyword);
|
||||
}
|
||||
|
||||
return {
|
||||
type: actions.FIND_SET_KEYWORD,
|
||||
keyword,
|
||||
found,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const next = (keyword, reset) => {
|
||||
return findNext(keyword, reset, false);
|
||||
const next = (currentKeyword, reset) => {
|
||||
return findNext(currentKeyword, reset, false);
|
||||
};
|
||||
|
||||
const prev = (keyword, reset) => {
|
||||
return findNext(keyword, reset, true);
|
||||
const prev = (currentKeyword, reset) => {
|
||||
return findNext(currentKeyword, reset, true);
|
||||
};
|
||||
|
||||
export { next, prev };
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import actions from 'content/actions';
|
||||
|
||||
const enable = (newTab) => {
|
||||
const enable = (newTab, background) => {
|
||||
return {
|
||||
type: actions.FOLLOW_CONTROLLER_ENABLE,
|
||||
newTab,
|
||||
background,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -2,12 +2,16 @@ import operations from 'shared/operations';
|
|||
import messages from 'shared/messages';
|
||||
import * as scrolls from 'content/scrolls';
|
||||
import * as navigates from 'content/navigates';
|
||||
import * as focuses from 'content/focuses';
|
||||
import * as urls from 'content/urls';
|
||||
import * as consoleFrames from 'content/console-frames';
|
||||
import * as addonActions from './addon';
|
||||
import * as properties from 'shared/settings/properties';
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
const exec = (operation) => {
|
||||
const exec = (operation, repeat, settings) => {
|
||||
let smoothscroll = settings.properties.smoothscroll ||
|
||||
properties.defaults.smoothscroll;
|
||||
switch (operation.type) {
|
||||
case operations.ADDON_ENABLE:
|
||||
return addonActions.enable();
|
||||
|
@ -24,23 +28,24 @@ const exec = (operation) => {
|
|||
type: messages.FIND_PREV,
|
||||
}), '*');
|
||||
case operations.SCROLL_VERTICALLY:
|
||||
return scrolls.scrollVertically(window, operation.count);
|
||||
return scrolls.scrollVertically(operation.count, smoothscroll, repeat);
|
||||
case operations.SCROLL_HORIZONALLY:
|
||||
return scrolls.scrollHorizonally(window, operation.count);
|
||||
return scrolls.scrollHorizonally(operation.count, smoothscroll, repeat);
|
||||
case operations.SCROLL_PAGES:
|
||||
return scrolls.scrollPages(window, operation.count);
|
||||
return scrolls.scrollPages(operation.count, smoothscroll, repeat);
|
||||
case operations.SCROLL_TOP:
|
||||
return scrolls.scrollTop(window);
|
||||
return scrolls.scrollTop(smoothscroll, repeat);
|
||||
case operations.SCROLL_BOTTOM:
|
||||
return scrolls.scrollBottom(window);
|
||||
return scrolls.scrollBottom(smoothscroll, repeat);
|
||||
case operations.SCROLL_HOME:
|
||||
return scrolls.scrollHome(window);
|
||||
return scrolls.scrollHome(smoothscroll, repeat);
|
||||
case operations.SCROLL_END:
|
||||
return scrolls.scrollEnd(window);
|
||||
return scrolls.scrollEnd(smoothscroll, repeat);
|
||||
case operations.FOLLOW_START:
|
||||
return window.top.postMessage(JSON.stringify({
|
||||
type: messages.FOLLOW_START,
|
||||
newTab: operation.newTab
|
||||
newTab: operation.newTab,
|
||||
background: operation.background,
|
||||
}), '*');
|
||||
case operations.NAVIGATE_HISTORY_PREV:
|
||||
return navigates.historyPrev(window);
|
||||
|
@ -54,12 +59,13 @@ const exec = (operation) => {
|
|||
return navigates.parent(window);
|
||||
case operations.NAVIGATE_ROOT:
|
||||
return navigates.root(window);
|
||||
case operations.FOCUS_INPUT:
|
||||
return focuses.focusInput();
|
||||
case operations.URLS_YANK:
|
||||
urls.yank(window);
|
||||
return consoleFrames.postMessage(window.document, {
|
||||
type: messages.CONSOLE_SHOW_INFO,
|
||||
text: 'Current url yanked',
|
||||
});
|
||||
return consoleFrames.postInfo(window.document, 'Current url yanked');
|
||||
case operations.URLS_PASTE:
|
||||
return urls.paste(window, operation.newTab ? operation.newTab : false);
|
||||
default:
|
||||
browser.runtime.sendMessage({
|
||||
type: messages.BACKGROUND_OPERATION,
|
||||
|
|
|
@ -1,10 +1,17 @@
|
|||
import actions from 'content/actions';
|
||||
import * as keyUtils from 'shared/utils/keys';
|
||||
import operations from 'shared/operations';
|
||||
|
||||
const reservedKeymaps = {
|
||||
'<Esc>': { type: operations.CANCEL },
|
||||
'<C-[>': { type: operations.CANCEL },
|
||||
};
|
||||
|
||||
const set = (value) => {
|
||||
let entries = [];
|
||||
if (value.keymaps) {
|
||||
entries = Object.entries(value.keymaps).map((entry) => {
|
||||
let keymaps = Object.assign({}, value.keymaps, reservedKeymaps);
|
||||
entries = Object.entries(keymaps).map((entry) => {
|
||||
return [
|
||||
keyUtils.fromMapKeys(entry[0]),
|
||||
entry[1],
|
||||
|
|
|
@ -4,7 +4,8 @@ import * as dom from 'shared/utils/dom';
|
|||
|
||||
const TARGET_SELECTOR = [
|
||||
'a', 'button', 'input', 'textarea', 'area',
|
||||
'[contenteditable=true]', '[contenteditable=""]', '[tabindex]'
|
||||
'[contenteditable=true]', '[contenteditable=""]', '[tabindex]',
|
||||
'[role="button"]'
|
||||
].join(',');
|
||||
|
||||
|
||||
|
@ -29,11 +30,27 @@ const inViewport = (win, element, viewSize, framePosition) => {
|
|||
return true;
|
||||
};
|
||||
|
||||
const isAriaHiddenOrAriaDisabled = (win, element) => {
|
||||
if (!element || win.document.documentElement === element) {
|
||||
return false;
|
||||
}
|
||||
for (let attr of ['aria-hidden', 'aria-disabled']) {
|
||||
if (element.hasAttribute(attr)) {
|
||||
let hidden = element.getAttribute(attr).toLowerCase();
|
||||
if (hidden === '' || hidden === 'true') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return isAriaHiddenOrAriaDisabled(win, element.parentNode);
|
||||
};
|
||||
|
||||
export default class Follow {
|
||||
constructor(win, store) {
|
||||
this.win = win;
|
||||
this.store = store;
|
||||
this.newTab = false;
|
||||
this.background = false;
|
||||
this.hints = {};
|
||||
this.targets = [];
|
||||
|
||||
|
@ -47,6 +64,7 @@ export default class Follow {
|
|||
this.win.parent.postMessage(JSON.stringify({
|
||||
type: messages.FOLLOW_KEY_PRESS,
|
||||
key: key.key,
|
||||
ctrlKey: key.ctrlKey,
|
||||
}), '*');
|
||||
return true;
|
||||
}
|
||||
|
@ -68,6 +86,7 @@ export default class Follow {
|
|||
type: messages.OPEN_URL,
|
||||
url: element.href,
|
||||
newTab: true,
|
||||
background: this.background,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -79,12 +98,13 @@ export default class Follow {
|
|||
}), '*');
|
||||
}
|
||||
|
||||
createHints(keysArray, newTab) {
|
||||
createHints(keysArray, newTab, background) {
|
||||
if (keysArray.length !== this.targets.length) {
|
||||
throw new Error('illegal hint count');
|
||||
}
|
||||
|
||||
this.newTab = newTab;
|
||||
this.background = background;
|
||||
this.hints = {};
|
||||
for (let i = 0; i < keysArray.length; ++i) {
|
||||
let keys = keysArray[i];
|
||||
|
@ -150,7 +170,8 @@ export default class Follow {
|
|||
case messages.FOLLOW_REQUEST_COUNT_TARGETS:
|
||||
return this.countHints(sender, message.viewSize, message.framePosition);
|
||||
case messages.FOLLOW_CREATE_HINTS:
|
||||
return this.createHints(message.keysArray, message.newTab);
|
||||
return this.createHints(
|
||||
message.keysArray, message.newTab, message.background);
|
||||
case messages.FOLLOW_SHOW_HINTS:
|
||||
return this.showHints(message.keys);
|
||||
case messages.FOLLOW_ACTIVATE:
|
||||
|
@ -170,6 +191,7 @@ export default class Follow {
|
|||
style.visibility !== 'hidden' &&
|
||||
element.type !== 'hidden' &&
|
||||
element.offsetHeight > 0 &&
|
||||
!isAriaHiddenOrAriaDisabled(win, element) &&
|
||||
inViewport(win, element, viewSize, framePosition);
|
||||
});
|
||||
return filtered;
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import * as dom from 'shared/utils/dom';
|
||||
import * as keys from 'shared/utils/keys';
|
||||
|
||||
const cancelKey = (e) => {
|
||||
return e.key === 'Escape' || e.key === '[' && e.ctrlKey;
|
||||
};
|
||||
|
||||
export default class InputComponent {
|
||||
constructor(target) {
|
||||
this.pressed = {};
|
||||
|
@ -37,7 +41,7 @@ export default class InputComponent {
|
|||
|
||||
capture(e) {
|
||||
if (this.fromInput(e)) {
|
||||
if (e.key === 'Escape' && e.target.blur) {
|
||||
if (cancelKey(e) && e.target.blur) {
|
||||
e.target.blur();
|
||||
}
|
||||
return;
|
||||
|
|
|
@ -47,7 +47,8 @@ export default class KeymapperComponent {
|
|||
return true;
|
||||
}
|
||||
let operation = keymaps.get(matched[0]);
|
||||
this.store.dispatch(operationActions.exec(operation));
|
||||
this.store.dispatch(operationActions.exec(
|
||||
operation, key.repeat, state.setting));
|
||||
this.store.dispatch(inputActions.clearKeys());
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import * as findActions from 'content/actions/find';
|
||||
import messages from 'shared/messages';
|
||||
import * as consoleFrames from '../../console-frames';
|
||||
|
||||
export default class FindComponent {
|
||||
constructor(win, store) {
|
||||
|
@ -32,23 +31,11 @@ export default class FindComponent {
|
|||
|
||||
next() {
|
||||
let state = this.store.getState().find;
|
||||
|
||||
if (!state.found) {
|
||||
return consoleFrames.postError(
|
||||
window.document,
|
||||
'Pattern not found: ' + state.keyword);
|
||||
}
|
||||
return this.store.dispatch(findActions.next(state.keyword, false));
|
||||
}
|
||||
|
||||
prev() {
|
||||
let state = this.store.getState().find;
|
||||
|
||||
if (!state.found) {
|
||||
return consoleFrames.postError(
|
||||
window.document,
|
||||
'Pattern not found: ' + state.keyword);
|
||||
}
|
||||
return this.store.dispatch(findActions.prev(state.keyword, false));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import * as followControllerActions from 'content/actions/follow-controller';
|
||||
import messages from 'shared/messages';
|
||||
import HintKeyProducer from 'content/hint-key-producer';
|
||||
|
||||
const DEFAULT_HINT_CHARSET = 'abcdefghijklmnopqrstuvwxyz';
|
||||
import * as properties from 'shared/settings/properties';
|
||||
|
||||
const broadcastMessage = (win, message) => {
|
||||
let json = JSON.stringify(message);
|
||||
|
@ -29,11 +28,11 @@ export default class FollowController {
|
|||
switch (message.type) {
|
||||
case messages.FOLLOW_START:
|
||||
return this.store.dispatch(
|
||||
followControllerActions.enable(message.newTab));
|
||||
followControllerActions.enable(message.newTab, message.background));
|
||||
case messages.FOLLOW_RESPONSE_COUNT_TARGETS:
|
||||
return this.create(message.count, sender);
|
||||
case messages.FOLLOW_KEY_PRESS:
|
||||
return this.keyPress(message.key);
|
||||
return this.keyPress(message.key, message.ctrlKey);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -70,7 +69,11 @@ export default class FollowController {
|
|||
});
|
||||
}
|
||||
|
||||
keyPress(key) {
|
||||
keyPress(key, ctrlKey) {
|
||||
if (key === '[' && ctrlKey) {
|
||||
this.store.dispatch(followControllerActions.disable());
|
||||
return true;
|
||||
}
|
||||
switch (key) {
|
||||
case 'Enter':
|
||||
this.activate();
|
||||
|
@ -84,7 +87,7 @@ export default class FollowController {
|
|||
this.store.dispatch(followControllerActions.backspace());
|
||||
break;
|
||||
default:
|
||||
if (DEFAULT_HINT_CHARSET.includes(key)) {
|
||||
if (this.hintchars().includes(key)) {
|
||||
this.store.dispatch(followControllerActions.keyPress(key));
|
||||
}
|
||||
break;
|
||||
|
@ -93,7 +96,7 @@ export default class FollowController {
|
|||
}
|
||||
|
||||
count() {
|
||||
this.producer = new HintKeyProducer(DEFAULT_HINT_CHARSET);
|
||||
this.producer = new HintKeyProducer(this.hintchars());
|
||||
let doc = this.win.document;
|
||||
let viewWidth = this.win.innerWidth || doc.documentElement.clientWidth;
|
||||
let viewHeight = this.win.innerHeight || doc.documentElement.clientHeight;
|
||||
|
@ -126,6 +129,7 @@ export default class FollowController {
|
|||
type: messages.FOLLOW_CREATE_HINTS,
|
||||
keysArray: produced,
|
||||
newTab: this.state.newTab,
|
||||
background: this.state.background,
|
||||
}), '*');
|
||||
}
|
||||
|
||||
|
@ -135,4 +139,9 @@ export default class FollowController {
|
|||
type: messages.FOLLOW_REMOVE_HINTS,
|
||||
});
|
||||
}
|
||||
|
||||
hintchars() {
|
||||
return this.store.getState().setting.properties.hintchars ||
|
||||
properties.defaults.hintchars;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,4 +28,11 @@ const postError = (doc, message) => {
|
|||
});
|
||||
};
|
||||
|
||||
export { initialize, blur, postMessage, postError };
|
||||
const postInfo = (doc, message) => {
|
||||
return postMessage(doc, {
|
||||
type: messages.CONSOLE_SHOW_INFO,
|
||||
text: message,
|
||||
});
|
||||
};
|
||||
|
||||
export { initialize, blur, postError, postInfo };
|
||||
|
|
13
src/content/focuses.js
Normal file
13
src/content/focuses.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
import * as doms from 'shared/utils/dom';
|
||||
|
||||
const focusInput = () => {
|
||||
let inputTypes = ['email', 'number', 'search', 'tel', 'text', 'url'];
|
||||
let inputSelector = inputTypes.map(type => `input[type=${type}]`).join(',');
|
||||
let targets = window.document.querySelectorAll(inputSelector + ',textarea');
|
||||
let target = Array.from(targets).find(doms.isVisible);
|
||||
if (target) {
|
||||
target.focus();
|
||||
}
|
||||
};
|
||||
|
||||
export { focusInput };
|
|
@ -1,17 +1,18 @@
|
|||
const PREV_LINK_PATTERNS = [
|
||||
/\bprev\b/i, /\bprevious\b/i, /\bback\b/i,
|
||||
/</, /\u2039/, /\u2190/, /\xab/, /\u226a/, /<</
|
||||
];
|
||||
const NEXT_LINK_PATTERNS = [
|
||||
/\bnext\b/i,
|
||||
/>/, /\u203a/, /\u2192/, /\xbb/, /\u226b/, />>/
|
||||
];
|
||||
const REL_PATTERN = {
|
||||
prev: /^(?:prev(?:ious)?|older)\b|\u2039|\u2190|\xab|\u226a|<</i,
|
||||
next: /^(?:next|newer)\b|\u203a|\u2192|\xbb|\u226b|>>/i,
|
||||
};
|
||||
|
||||
const findLinkByPatterns = (win, patterns) => {
|
||||
let links = win.document.getElementsByTagName('a');
|
||||
return Array.prototype.find.call(links, (link) => {
|
||||
return patterns.some(ptn => ptn.test(link.textContent));
|
||||
});
|
||||
// Return the last element in the document matching the supplied selector
|
||||
// and the optional filter, or null if there are no matches.
|
||||
const selectLast = (win, selector, filter) => {
|
||||
let nodes = win.document.querySelectorAll(selector);
|
||||
|
||||
if (filter) {
|
||||
nodes = Array.from(nodes).filter(filter);
|
||||
}
|
||||
|
||||
return nodes.length ? nodes[nodes.length - 1] : null;
|
||||
};
|
||||
|
||||
const historyPrev = (win) => {
|
||||
|
@ -22,30 +23,37 @@ const historyNext = (win) => {
|
|||
win.history.forward();
|
||||
};
|
||||
|
||||
const linkPrev = (win) => {
|
||||
let link = win.document.querySelector('a[rel=prev]');
|
||||
// Code common to linkPrev and linkNext which navigates to the specified page.
|
||||
const linkRel = (win, rel) => {
|
||||
let link = selectLast(win, `link[rel~=${rel}][href]`);
|
||||
|
||||
if (link) {
|
||||
return link.click();
|
||||
win.location = link.href;
|
||||
return;
|
||||
}
|
||||
link = findLinkByPatterns(win, PREV_LINK_PATTERNS);
|
||||
|
||||
const pattern = REL_PATTERN[rel];
|
||||
|
||||
link = selectLast(win, `a[rel~=${rel}][href]`) ||
|
||||
// `innerText` is much slower than `textContent`, but produces much better
|
||||
// (i.e. less unexpected) results
|
||||
selectLast(win, 'a[href]', lnk => pattern.test(lnk.innerText));
|
||||
|
||||
if (link) {
|
||||
link.click();
|
||||
}
|
||||
};
|
||||
|
||||
const linkPrev = (win) => {
|
||||
linkRel(win, 'prev');
|
||||
};
|
||||
|
||||
const linkNext = (win) => {
|
||||
let link = win.document.querySelector('a[rel=next]');
|
||||
if (link) {
|
||||
return link.click();
|
||||
}
|
||||
link = findLinkByPatterns(win, NEXT_LINK_PATTERNS);
|
||||
if (link) {
|
||||
link.click();
|
||||
}
|
||||
linkRel(win, 'next');
|
||||
};
|
||||
|
||||
const parent = (win) => {
|
||||
let loc = win.location;
|
||||
const loc = win.location;
|
||||
if (loc.hash !== '') {
|
||||
loc.hash = '';
|
||||
return;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import actions from 'content/actions';
|
||||
|
||||
const defaultState = {
|
||||
keyword: '',
|
||||
keyword: null,
|
||||
found: false,
|
||||
};
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import actions from 'content/actions';
|
|||
const defaultState = {
|
||||
enabled: false,
|
||||
newTab: false,
|
||||
background: false,
|
||||
keys: '',
|
||||
};
|
||||
|
||||
|
@ -12,6 +13,7 @@ export default function reducer(state = defaultState, action = {}) {
|
|||
return Object.assign({}, state, {
|
||||
enabled: true,
|
||||
newTab: action.newTab,
|
||||
background: action.background,
|
||||
keys: '',
|
||||
});
|
||||
case actions.FOLLOW_CONTROLLER_DISABLE:
|
||||
|
|
|
@ -1,27 +1,14 @@
|
|||
import * as doms from 'shared/utils/dom';
|
||||
|
||||
const SCROLL_DELTA_X = 48;
|
||||
const SCROLL_DELTA_Y = 48;
|
||||
const SMOOTH_SCROLL_DURATION = 150;
|
||||
|
||||
const isVisible = (win, element) => {
|
||||
let rect = element.getBoundingClientRect();
|
||||
if (rect.width === 0 || rect.height === 0) {
|
||||
return false;
|
||||
}
|
||||
if (rect.right < 0 && rect.bottom < 0) {
|
||||
return false;
|
||||
}
|
||||
if (win.innerWidth < rect.left && win.innerHeight < rect.top) {
|
||||
return false;
|
||||
}
|
||||
// dirty way to store scrolling state on globally
|
||||
let scrolling = [false];
|
||||
|
||||
let { display, visibility } = win.getComputedStyle(element);
|
||||
if (display === 'none' || visibility === 'hidden') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const isScrollableStyle = (win, element) => {
|
||||
let { overflowX, overflowY } = win.getComputedStyle(element);
|
||||
const isScrollableStyle = (element) => {
|
||||
let { overflowX, overflowY } = window.getComputedStyle(element);
|
||||
return !(overflowX !== 'scroll' && overflowX !== 'auto' &&
|
||||
overflowY !== 'scroll' && overflowY !== 'auto');
|
||||
};
|
||||
|
@ -35,15 +22,14 @@ const isOverflowed = (element) => {
|
|||
// this method is called by each scrolling, and the returned value of this
|
||||
// method is not cached. That does not cause performance issue because in the
|
||||
// most pages, the window is root element i,e, documentElement.
|
||||
const findScrollable = (win, element) => {
|
||||
if (isScrollableStyle(win, element) && isOverflowed(element)) {
|
||||
const findScrollable = (element) => {
|
||||
if (isScrollableStyle(element) && isOverflowed(element)) {
|
||||
return element;
|
||||
}
|
||||
|
||||
let children = Array.prototype
|
||||
.filter.call(element.children, e => isVisible(win, e));
|
||||
let children = Array.from(element.children).filter(doms.isVisible);
|
||||
for (let child of children) {
|
||||
let scrollable = findScrollable(win, child);
|
||||
let scrollable = findScrollable(child);
|
||||
if (scrollable) {
|
||||
return scrollable;
|
||||
}
|
||||
|
@ -51,68 +37,153 @@ const findScrollable = (win, element) => {
|
|||
return null;
|
||||
};
|
||||
|
||||
const scrollTarget = (win) => {
|
||||
if (isOverflowed(win.document.documentElement)) {
|
||||
return win.document.documentElement;
|
||||
const scrollTarget = () => {
|
||||
if (isOverflowed(window.document.documentElement)) {
|
||||
return window.document.documentElement;
|
||||
}
|
||||
if (isOverflowed(win.document.body)) {
|
||||
return win.document.body;
|
||||
if (isOverflowed(window.document.body)) {
|
||||
return window.document.body;
|
||||
}
|
||||
let target = findScrollable(win, win.document.documentElement);
|
||||
let target = findScrollable(window.document.documentElement);
|
||||
if (target) {
|
||||
return target;
|
||||
}
|
||||
return win.document.documentElement;
|
||||
return window.document.documentElement;
|
||||
};
|
||||
|
||||
const scrollVertically = (win, count) => {
|
||||
let target = scrollTarget(win);
|
||||
class SmoothScroller {
|
||||
constructor(element, repeat) {
|
||||
this.element = element;
|
||||
this.repeat = repeat;
|
||||
this.scrolling = scrolling;
|
||||
if (repeat) {
|
||||
this.easing = SmoothScroller.linearEasing;
|
||||
} else {
|
||||
this.easing = SmoothScroller.inOutQuadEasing;
|
||||
}
|
||||
}
|
||||
|
||||
scroll(x, y) {
|
||||
if (this.scrolling[0]) {
|
||||
return;
|
||||
}
|
||||
scrolling[0] = true;
|
||||
|
||||
this.startX = this.element.scrollLeft;
|
||||
this.startY = this.element.scrollTop;
|
||||
|
||||
this.targetX = x;
|
||||
this.targetY = y;
|
||||
this.distanceX = x - this.startX;
|
||||
this.distanceY = y - this.startY;
|
||||
this.timeStart = 0;
|
||||
|
||||
window.requestAnimationFrame(this.loop.bind(this));
|
||||
}
|
||||
|
||||
loop(time) {
|
||||
if (!this.timeStart) {
|
||||
this.timeStart = time;
|
||||
}
|
||||
|
||||
let elapsed = time - this.timeStart;
|
||||
let v = this.easing(elapsed / SMOOTH_SCROLL_DURATION);
|
||||
let nextX = this.startX + this.distanceX * v;
|
||||
let nextY = this.startY + this.distanceY * v;
|
||||
|
||||
window.scrollTo(nextX, nextY);
|
||||
|
||||
if (elapsed < SMOOTH_SCROLL_DURATION) {
|
||||
window.requestAnimationFrame(this.loop.bind(this));
|
||||
} else {
|
||||
scrolling[0] = false;
|
||||
this.element.scrollTo(this.targetX, this.targetY);
|
||||
}
|
||||
}
|
||||
|
||||
static inOutQuadEasing(t) {
|
||||
if (t < 1) {
|
||||
return t * t;
|
||||
}
|
||||
return -(t - 1) * (t - 1) + 1;
|
||||
}
|
||||
|
||||
static linearEasing(t) {
|
||||
return t;
|
||||
}
|
||||
}
|
||||
|
||||
class RoughtScroller {
|
||||
constructor(element) {
|
||||
this.element = element;
|
||||
}
|
||||
|
||||
scroll(x, y) {
|
||||
this.element.scrollTo(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
const scroller = (element, smooth, repeat) => {
|
||||
if (smooth) {
|
||||
return new SmoothScroller(element, repeat);
|
||||
}
|
||||
return new RoughtScroller(element);
|
||||
};
|
||||
|
||||
const scrollVertically = (count, smooth, repeat) => {
|
||||
let target = scrollTarget();
|
||||
let x = target.scrollLeft;
|
||||
let y = target.scrollTop + SCROLL_DELTA_Y * count;
|
||||
target.scrollTo(x, y);
|
||||
if (repeat && smooth) {
|
||||
y = target.scrollTop + SCROLL_DELTA_Y * count * 4;
|
||||
}
|
||||
scroller(target, smooth, repeat).scroll(x, y);
|
||||
};
|
||||
|
||||
const scrollHorizonally = (win, count) => {
|
||||
let target = scrollTarget(win);
|
||||
const scrollHorizonally = (count, smooth, repeat) => {
|
||||
let target = scrollTarget();
|
||||
let x = target.scrollLeft + SCROLL_DELTA_X * count;
|
||||
let y = target.scrollTop;
|
||||
target.scrollTo(x, y);
|
||||
if (repeat && smooth) {
|
||||
y = target.scrollTop + SCROLL_DELTA_Y * count * 4;
|
||||
}
|
||||
scroller(target, smooth, repeat).scroll(x, y);
|
||||
};
|
||||
|
||||
const scrollPages = (win, count) => {
|
||||
let target = scrollTarget(win);
|
||||
const scrollPages = (count, smooth, repeat) => {
|
||||
let target = scrollTarget();
|
||||
let height = target.clientHeight;
|
||||
let x = target.scrollLeft;
|
||||
let y = target.scrollTop + height * count;
|
||||
target.scrollTo(x, y);
|
||||
scroller(target, smooth, repeat).scroll(x, y);
|
||||
};
|
||||
|
||||
const scrollTop = (win) => {
|
||||
let target = scrollTarget(win);
|
||||
const scrollTop = (smooth, repeat) => {
|
||||
let target = scrollTarget();
|
||||
let x = target.scrollLeft;
|
||||
let y = 0;
|
||||
target.scrollTo(x, y);
|
||||
scroller(target, smooth, repeat).scroll(x, y);
|
||||
};
|
||||
|
||||
const scrollBottom = (win) => {
|
||||
let target = scrollTarget(win);
|
||||
const scrollBottom = (smooth, repeat) => {
|
||||
let target = scrollTarget();
|
||||
let x = target.scrollLeft;
|
||||
let y = target.scrollHeight;
|
||||
target.scrollTo(x, y);
|
||||
scroller(target, smooth, repeat).scroll(x, y);
|
||||
};
|
||||
|
||||
const scrollHome = (win) => {
|
||||
let target = scrollTarget(win);
|
||||
const scrollHome = (smooth, repeat) => {
|
||||
let target = scrollTarget();
|
||||
let x = 0;
|
||||
let y = target.scrollTop;
|
||||
target.scrollTo(x, y);
|
||||
scroller(target, smooth, repeat).scroll(x, y);
|
||||
};
|
||||
|
||||
const scrollEnd = (win) => {
|
||||
let target = scrollTarget(win);
|
||||
const scrollEnd = (smooth, repeat) => {
|
||||
let target = scrollTarget();
|
||||
let x = target.scrollWidth;
|
||||
let y = target.scrollTop;
|
||||
target.scrollTo(x, y);
|
||||
scroller(target, smooth, repeat).scroll(x, y);
|
||||
};
|
||||
|
||||
export {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import messages from 'shared/messages';
|
||||
|
||||
const yank = (win) => {
|
||||
let input = win.document.createElement('input');
|
||||
win.document.body.append(input);
|
||||
|
@ -12,4 +14,26 @@ const yank = (win) => {
|
|||
input.remove();
|
||||
};
|
||||
|
||||
export { yank };
|
||||
const paste = (win, newTab) => {
|
||||
let textarea = win.document.createElement('textarea');
|
||||
win.document.body.append(textarea);
|
||||
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.top = '-100px';
|
||||
textarea.contentEditable = 'true';
|
||||
textarea.focus();
|
||||
|
||||
if (win.document.execCommand('paste')) {
|
||||
if (/^(https?|ftp):\/\//.test(textarea.textContent)) {
|
||||
browser.runtime.sendMessage({
|
||||
type: messages.OPEN_URL,
|
||||
url: textarea.textContent,
|
||||
newTab: newTab ? newTab : false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
textarea.remove();
|
||||
};
|
||||
|
||||
export { yank, paste };
|
||||
|
|
|
@ -1,34 +1,39 @@
|
|||
import actions from 'settings/actions';
|
||||
import messages from 'shared/messages';
|
||||
import DefaultSettings from 'shared/default-settings';
|
||||
import DefaultSettings from 'shared/settings/default';
|
||||
import * as settingsStorage from 'shared/settings/storage';
|
||||
import * as settingsValues from 'shared/settings/values';
|
||||
|
||||
const load = () => {
|
||||
return browser.storage.local.get('settings').then(({ settings }) => {
|
||||
if (settings) {
|
||||
return set(settings);
|
||||
}
|
||||
return set(DefaultSettings);
|
||||
}, console.error);
|
||||
return settingsStorage.loadRaw().then((settings) => {
|
||||
return set(settings);
|
||||
});
|
||||
};
|
||||
|
||||
const save = (settings) => {
|
||||
return browser.storage.local.set({
|
||||
settings,
|
||||
}).then(() => {
|
||||
return settingsStorage.save(settings).then(() => {
|
||||
return browser.runtime.sendMessage({
|
||||
type: messages.SETTINGS_RELOAD
|
||||
}).then(() => {
|
||||
return set(settings);
|
||||
});
|
||||
}).then(() => {
|
||||
return set(settings);
|
||||
});
|
||||
};
|
||||
|
||||
const set = (settings) => {
|
||||
let value = JSON.parse(DefaultSettings.json);
|
||||
if (settings.source === 'json') {
|
||||
value = settingsValues.valueFromJson(settings.json);
|
||||
} else if (settings.source === 'form') {
|
||||
value = settingsValues.valueFromForm(settings.form);
|
||||
}
|
||||
|
||||
return {
|
||||
type: actions.SETTING_SET_SETTINGS,
|
||||
source: settings.source,
|
||||
json: settings.json,
|
||||
value: JSON.parse(settings.json),
|
||||
form: settings.form,
|
||||
value,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
52
src/settings/components/form/blacklist-form.jsx
Normal file
52
src/settings/components/form/blacklist-form.jsx
Normal file
|
@ -0,0 +1,52 @@
|
|||
import './blacklist-form.scss';
|
||||
import AddButton from '../ui/add-button';
|
||||
import DeleteButton from '../ui/delete-button';
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
class BlacklistForm extends Component {
|
||||
|
||||
render() {
|
||||
let value = this.props.value;
|
||||
if (!value) {
|
||||
value = [];
|
||||
}
|
||||
|
||||
return <div className='form-blacklist-form'>
|
||||
{
|
||||
value.map((url, index) => {
|
||||
return <div key={index} className='form-blacklist-form-row'>
|
||||
<input data-index={index} type='text' name='url'
|
||||
className='column-url' value={url}
|
||||
onChange={this.bindValue.bind(this)} />
|
||||
<DeleteButton data-index={index} name='delete'
|
||||
onClick={this.bindValue.bind(this)} />
|
||||
</div>;
|
||||
})
|
||||
}
|
||||
<AddButton name='add' style='float:right'
|
||||
onClick={this.bindValue.bind(this)} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
bindValue(e) {
|
||||
if (!this.props.onChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
let name = e.target.name;
|
||||
let index = e.target.getAttribute('data-index');
|
||||
let next = this.props.value ? this.props.value.slice() : [];
|
||||
|
||||
if (name === 'url') {
|
||||
next[index] = e.target.value;
|
||||
} else if (name === 'add') {
|
||||
next.push('');
|
||||
} else if (name === 'delete') {
|
||||
next.splice(index, 1);
|
||||
}
|
||||
|
||||
this.props.onChange(next);
|
||||
}
|
||||
}
|
||||
|
||||
export default BlacklistForm;
|
9
src/settings/components/form/blacklist-form.scss
Normal file
9
src/settings/components/form/blacklist-form.scss
Normal file
|
@ -0,0 +1,9 @@
|
|||
.form-blacklist-form {
|
||||
&-row {
|
||||
display: flex;
|
||||
|
||||
.column-url {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
109
src/settings/components/form/keymaps-form.jsx
Normal file
109
src/settings/components/form/keymaps-form.jsx
Normal file
|
@ -0,0 +1,109 @@
|
|||
import './keymaps-form.scss';
|
||||
import { h, Component } from 'preact';
|
||||
import Input from '../ui/input';
|
||||
|
||||
const KeyMapFields = [
|
||||
[
|
||||
['scroll.vertically?{"count":1}', 'Scroll down'],
|
||||
['scroll.vertically?{"count":-1}', 'Scroll up'],
|
||||
['scroll.horizonally?{"count":-1}', 'Scroll left'],
|
||||
['scroll.horizonally?{"count":1}', 'Scroll right'],
|
||||
['scroll.home', 'Scroll to leftmost'],
|
||||
['scroll.end', 'Scroll to rightmost'],
|
||||
['scroll.top', 'Scroll to top'],
|
||||
['scroll.bottom', 'Scroll to bottom'],
|
||||
['scroll.pages?{"count":-0.5}', 'Scroll up by half of screen'],
|
||||
['scroll.pages?{"count":0.5}', 'Scroll down by half of screen'],
|
||||
['scroll.pages?{"count":-1}', 'Scroll up by a screen'],
|
||||
['scroll.pages?{"count":1}', 'Scroll down by a screen'],
|
||||
], [
|
||||
['tabs.close', 'Close a tab'],
|
||||
['tabs.reopen', 'Reopen closed tab'],
|
||||
['tabs.next?{"count":1}', 'Select next Tab'],
|
||||
['tabs.prev?{"count":1}', 'Select prev Tab'],
|
||||
['tabs.first', 'Select first tab'],
|
||||
['tabs.last', 'Select last tab'],
|
||||
['tabs.reload?{"cache":false}', 'Reload current tab'],
|
||||
['tabs.reload?{"cache":true}', 'Reload with no caches'],
|
||||
['tabs.pin.toggle', 'Toggle pinned state'],
|
||||
['tabs.duplicate', 'Duplicate a tab'],
|
||||
], [
|
||||
['follow.start?{"newTab":false}', 'Follow a link'],
|
||||
['follow.start?{"newTab":true}', 'Follow a link in new tab'],
|
||||
['navigate.history.prev', 'Go back in histories'],
|
||||
['navigate.history.next', 'Go forward in histories'],
|
||||
['navigate.link.next', 'Open next link'],
|
||||
['navigate.link.prev', 'Open previous link'],
|
||||
['navigate.parent', 'Go to parent directory'],
|
||||
['navigate.root', 'Go to root directory'],
|
||||
['focus.input', 'Focus input'],
|
||||
], [
|
||||
['find.start', 'Start find mode'],
|
||||
['find.next', 'Find next word'],
|
||||
['find.prev', 'Find previous word'],
|
||||
], [
|
||||
['command.show', 'Open console'],
|
||||
['command.show.open?{"alter":false}', 'Open URL'],
|
||||
['command.show.open?{"alter":true}', 'Alter URL'],
|
||||
['command.show.tabopen?{"alter":false}', 'Open URL in new Tab'],
|
||||
['command.show.tabopen?{"alter":true}', 'Alter URL in new Tab'],
|
||||
['command.show.winopen?{"alter":false}', 'Open URL in new window'],
|
||||
['command.show.winopen?{"alter":true}', 'Alter URL in new window'],
|
||||
['command.show.buffer', 'Open buffer command'],
|
||||
], [
|
||||
['addon.toggle.enabled', 'Enable or disable'],
|
||||
['urls.yank', 'Copy current URL'],
|
||||
['urls.paste?{"newTab":false}', 'Open clipboard\'s URL in current tab'],
|
||||
['urls.paste?{"newTab":true}', 'Open clipboard\'s URL in new tab'],
|
||||
['zoom.in', 'Zoom-in'],
|
||||
['zoom.out', 'Zoom-out'],
|
||||
['zoom.neutral', 'Reset zoom level'],
|
||||
]
|
||||
];
|
||||
|
||||
const AllowdOps = [].concat(...KeyMapFields.map(group => group.map(e => e[0])));
|
||||
|
||||
class KeymapsForm extends Component {
|
||||
|
||||
render() {
|
||||
let values = this.props.value;
|
||||
if (!values) {
|
||||
values = {};
|
||||
}
|
||||
return <div className='form-keymaps-form'>
|
||||
{
|
||||
KeyMapFields.map((group, index) => {
|
||||
return <div key={index} className='form-keymaps-form-field-group'>
|
||||
{
|
||||
group.map((field) => {
|
||||
let name = field[0];
|
||||
let label = field[1];
|
||||
let value = values[name];
|
||||
return <Input
|
||||
type='text' id={name} name={name} key={name}
|
||||
label={label} value={value}
|
||||
onChange={this.bindValue.bind(this)}
|
||||
/>;
|
||||
})
|
||||
}
|
||||
</div>;
|
||||
})
|
||||
}
|
||||
</div>;
|
||||
}
|
||||
|
||||
bindValue(e) {
|
||||
if (!this.props.onChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
let next = Object.assign({}, this.props.value);
|
||||
next[e.target.name] = e.target.value;
|
||||
|
||||
this.props.onChange(next);
|
||||
}
|
||||
}
|
||||
|
||||
KeymapsForm.AllowdOps = AllowdOps;
|
||||
|
||||
export default KeymapsForm;
|
11
src/settings/components/form/keymaps-form.scss
Normal file
11
src/settings/components/form/keymaps-form.scss
Normal file
|
@ -0,0 +1,11 @@
|
|||
.form-keymaps-form {
|
||||
column-count: 3;
|
||||
|
||||
&-field-group {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
&-field-group:first-of-type {
|
||||
margin-top: 24px;
|
||||
}
|
||||
}
|
60
src/settings/components/form/properties-form.jsx
Normal file
60
src/settings/components/form/properties-form.jsx
Normal file
|
@ -0,0 +1,60 @@
|
|||
import './properties-form.scss';
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
class PropertiesForm extends Component {
|
||||
|
||||
render() {
|
||||
let types = this.props.types;
|
||||
let value = this.props.value;
|
||||
if (!value) {
|
||||
value = {};
|
||||
}
|
||||
|
||||
return <div className='form-properties-form'>
|
||||
{
|
||||
Object.keys(types).map((name) => {
|
||||
let type = types[name];
|
||||
let inputType = null;
|
||||
if (type === 'string') {
|
||||
inputType = 'text';
|
||||
} else if (type === 'number') {
|
||||
inputType = 'number';
|
||||
} else if (type === 'boolean') {
|
||||
inputType = 'checkbox';
|
||||
}
|
||||
return <div key={name} className='form-properties-form-row'>
|
||||
<label>
|
||||
<span className='column-name'>{name}</span>
|
||||
<input type={inputType} name={name}
|
||||
className='column-input'
|
||||
value={value[name] ? value[name] : ''}
|
||||
onChange={this.bindValue.bind(this)}
|
||||
checked={value[name]}
|
||||
/>
|
||||
</label>
|
||||
</div>;
|
||||
})
|
||||
}
|
||||
</div>;
|
||||
}
|
||||
|
||||
bindValue(e) {
|
||||
if (!this.props.onChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
let name = e.target.name;
|
||||
let next = Object.assign({}, this.props.value);
|
||||
if (e.target.type.toLowerCase() === 'checkbox') {
|
||||
next[name] = e.target.checked;
|
||||
} else if (e.target.type.toLowerCase() === 'number') {
|
||||
next[name] = Number(e.target.value);
|
||||
} else {
|
||||
next[name] = e.target.value;
|
||||
}
|
||||
|
||||
this.props.onChange(next);
|
||||
}
|
||||
}
|
||||
|
||||
export default PropertiesForm;
|
12
src/settings/components/form/properties-form.scss
Normal file
12
src/settings/components/form/properties-form.scss
Normal file
|
@ -0,0 +1,12 @@
|
|||
.form-properties-form {
|
||||
&-row {
|
||||
.column-name {
|
||||
display: inline-block;
|
||||
min-width: 5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
.column-input {
|
||||
line-height: 2.2rem;
|
||||
}
|
||||
}
|
||||
}
|
78
src/settings/components/form/search-form.jsx
Normal file
78
src/settings/components/form/search-form.jsx
Normal file
|
@ -0,0 +1,78 @@
|
|||
import './search-form.scss';
|
||||
import { h, Component } from 'preact';
|
||||
import AddButton from '../ui/add-button';
|
||||
import DeleteButton from '../ui/delete-button';
|
||||
|
||||
class SearchForm extends Component {
|
||||
|
||||
render() {
|
||||
let value = this.props.value;
|
||||
if (!value) {
|
||||
value = { default: '', engines: []};
|
||||
}
|
||||
if (!value.engines) {
|
||||
value.engines = [];
|
||||
}
|
||||
|
||||
return <div className='form-search-form'>
|
||||
<div className='form-search-form-header'>
|
||||
<div className='column-name'>Name</div>
|
||||
<div className='column-url'>URL</div>
|
||||
<div className='column-option'>Default</div>
|
||||
</div>
|
||||
{
|
||||
value.engines.map((engine, index) => {
|
||||
return <div key={index} className='form-search-form-row'>
|
||||
<input data-index={index} type='text' name='name'
|
||||
className='column-name' value={engine[0]}
|
||||
onChange={this.bindValue.bind(this)} />
|
||||
<input data-index={index} type='text' name='url'
|
||||
placeholder='http://example.com/?q={}'
|
||||
className='column-url' value={engine[1]}
|
||||
onChange={this.bindValue.bind(this)} />
|
||||
<div className='column-option'>
|
||||
<input data-index={index} type='radio' name='default'
|
||||
checked={value.default === engine[0]}
|
||||
onChange={this.bindValue.bind(this)} />
|
||||
<DeleteButton data-index={index} name='delete'
|
||||
onClick={this.bindValue.bind(this)} />
|
||||
</div>
|
||||
</div>;
|
||||
})
|
||||
}
|
||||
<AddButton name='add' style='float:right'
|
||||
onClick={this.bindValue.bind(this)} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
bindValue(e) {
|
||||
if (!this.props.onChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
let value = this.props.value;
|
||||
let name = e.target.name;
|
||||
let index = e.target.getAttribute('data-index');
|
||||
let next = Object.assign({}, {
|
||||
default: value.default,
|
||||
engines: value.engines ? value.engines.slice() : [],
|
||||
});
|
||||
|
||||
if (name === 'name') {
|
||||
next.engines[index][0] = e.target.value;
|
||||
next.default = this.props.value.engines[index][0];
|
||||
} else if (name === 'url') {
|
||||
next.engines[index][1] = e.target.value;
|
||||
} else if (name === 'default') {
|
||||
next.default = this.props.value.engines[index][0];
|
||||
} else if (name === 'add') {
|
||||
next.engines.push(['', '']);
|
||||
} else if (name === 'delete') {
|
||||
next.engines.splice(index, 1);
|
||||
}
|
||||
|
||||
this.props.onChange(next);
|
||||
}
|
||||
}
|
||||
|
||||
export default SearchForm;
|
28
src/settings/components/form/search-form.scss
Normal file
28
src/settings/components/form/search-form.scss
Normal file
|
@ -0,0 +1,28 @@
|
|||
.form-search-form {
|
||||
@mixin row-base {
|
||||
display: flex;
|
||||
|
||||
.column-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.column-url {
|
||||
flex: 5;
|
||||
min-width: 0;
|
||||
}
|
||||
.column-option {
|
||||
text-align: right;
|
||||
flex-basis: 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
&-header {
|
||||
@include row-base;
|
||||
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&-row {
|
||||
@include row-base;
|
||||
}
|
||||
}
|
|
@ -1,16 +1,29 @@
|
|||
import './site.scss';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { h, Component } from 'preact';
|
||||
import Input from './ui/input';
|
||||
import SearchForm from './form/search-form';
|
||||
import KeymapsForm from './form/keymaps-form';
|
||||
import BlacklistForm from './form/blacklist-form';
|
||||
import PropertiesForm from './form/properties-form';
|
||||
import * as properties from 'shared/settings/properties';
|
||||
import * as settingActions from 'settings/actions/setting';
|
||||
import * as validator from 'shared/validators/setting';
|
||||
import * as validator from 'shared/settings/validator';
|
||||
import * as settingsValues from 'shared/settings/values';
|
||||
|
||||
class SettingsComponent extends React.Component {
|
||||
const DO_YOU_WANT_TO_CONTINUE =
|
||||
'Some settings in JSON can be lost when migrating. ' +
|
||||
'Do you want to continue?';
|
||||
|
||||
class SettingsComponent extends Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
settings: {
|
||||
json: '',
|
||||
},
|
||||
errors: {
|
||||
json: '',
|
||||
}
|
||||
};
|
||||
this.context.store.subscribe(this.stateChanged.bind(this));
|
||||
|
@ -26,66 +39,187 @@ class SettingsComponent extends React.Component {
|
|||
settings: {
|
||||
source: settings.source,
|
||||
json: settings.json,
|
||||
form: settings.form,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
renderFormFields() {
|
||||
return <div>
|
||||
<fieldset>
|
||||
<legend>Keybindings</legend>
|
||||
<KeymapsForm
|
||||
value={this.state.settings.form.keymaps}
|
||||
onChange={value => this.bindForm('keymaps', value)}
|
||||
/>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Search Engines</legend>
|
||||
<SearchForm
|
||||
value={this.state.settings.form.search}
|
||||
onChange={value => this.bindForm('search', value)}
|
||||
/>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Blacklist</legend>
|
||||
<BlacklistForm
|
||||
value={this.state.settings.form.blacklist}
|
||||
onChange={value => this.bindForm('blacklist', value)}
|
||||
/>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Properties</legend>
|
||||
<PropertiesForm
|
||||
types={properties.types}
|
||||
value={this.state.settings.form.properties}
|
||||
onChange={value => this.bindForm('properties', value)}
|
||||
/>
|
||||
</fieldset>
|
||||
</div>;
|
||||
}
|
||||
|
||||
renderJsonFields() {
|
||||
return <div>
|
||||
<Input
|
||||
type='textarea'
|
||||
name='json'
|
||||
label='Plane JSON'
|
||||
spellCheck='false'
|
||||
error={this.state.errors.json}
|
||||
onChange={this.bindValue.bind(this)}
|
||||
value={this.state.settings.json}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
render() {
|
||||
let fields = null;
|
||||
if (this.state.settings.source === 'form') {
|
||||
fields = this.renderFormFields();
|
||||
} else if (this.state.settings.source === 'json') {
|
||||
fields = this.renderJsonFields();
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<h1>Configure Vim-Vixen</h1>
|
||||
<form className='vimvixen-settings-form'>
|
||||
|
||||
<p>Load settings from:</p>
|
||||
<input type='radio' id='setting-source-json'
|
||||
<Input
|
||||
type='radio'
|
||||
id='setting-source-form'
|
||||
name='source'
|
||||
value='json'
|
||||
onChange={this.bindAndSave.bind(this)}
|
||||
checked={this.state.settings.source === 'json'} />
|
||||
<label htmlFor='settings-source-json'>JSON</label>
|
||||
label='Use form'
|
||||
checked={this.state.settings.source === 'form'}
|
||||
value='form'
|
||||
onChange={this.bindSource.bind(this)} />
|
||||
|
||||
<textarea name='json' spellCheck='false'
|
||||
onInput={this.validate.bind(this)}
|
||||
onChange={this.bindValue.bind(this)}
|
||||
onBlur={this.bindAndSave.bind(this)}
|
||||
value={this.state.settings.json} />
|
||||
<Input
|
||||
type='radio'
|
||||
name='source'
|
||||
label='Use plain JSON'
|
||||
checked={this.state.settings.source === 'json'}
|
||||
value='json'
|
||||
onChange={this.bindSource.bind(this)} />
|
||||
|
||||
{ fields }
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
validate(e) {
|
||||
try {
|
||||
let settings = JSON.parse(e.target.value);
|
||||
validate(target) {
|
||||
if (target.name === 'json') {
|
||||
let settings = JSON.parse(target.value);
|
||||
validator.validate(settings);
|
||||
e.target.setCustomValidity('');
|
||||
} catch (err) {
|
||||
e.target.setCustomValidity(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
bindValue(e) {
|
||||
let nextSettings = Object.assign({}, this.state.settings);
|
||||
nextSettings[e.target.name] = e.target.value;
|
||||
validateValue(e) {
|
||||
let next = Object.assign({}, this.state);
|
||||
|
||||
this.setState({ settings: nextSettings });
|
||||
next.errors.json = '';
|
||||
try {
|
||||
this.validate(e.target);
|
||||
} catch (err) {
|
||||
next.errors.json = err.message;
|
||||
}
|
||||
next.settings[e.target.name] = e.target.value;
|
||||
}
|
||||
|
||||
bindAndSave(e) {
|
||||
this.bindValue(e);
|
||||
bindForm(name, value) {
|
||||
let next = Object.assign({}, this.state, {
|
||||
settings: Object.assign({}, this.state.settings, {
|
||||
form: Object.assign({}, this.state.settings.form)
|
||||
})
|
||||
});
|
||||
next.settings.form[name] = value;
|
||||
this.setState(next);
|
||||
this.context.store.dispatch(settingActions.save(next.settings));
|
||||
}
|
||||
|
||||
bindValue(e) {
|
||||
let next = Object.assign({}, this.state);
|
||||
let error = false;
|
||||
|
||||
next.errors.json = '';
|
||||
try {
|
||||
let json = this.state.settings.json;
|
||||
validator.validate(JSON.parse(json));
|
||||
this.context.store.dispatch(settingActions.save(this.state.settings));
|
||||
this.validate(e.target);
|
||||
} catch (err) {
|
||||
// error already shown
|
||||
next.errors.json = err.message;
|
||||
error = true;
|
||||
}
|
||||
next.settings[e.target.name] = e.target.value;
|
||||
|
||||
this.setState(this.state);
|
||||
if (!error) {
|
||||
this.context.store.dispatch(settingActions.save(next.settings));
|
||||
}
|
||||
}
|
||||
|
||||
migrateToForm() {
|
||||
let b = window.confirm(DO_YOU_WANT_TO_CONTINUE);
|
||||
if (!b) {
|
||||
this.setState(this.state);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
validator.validate(JSON.parse(this.state.settings.json));
|
||||
} catch (err) {
|
||||
this.setState(this.state);
|
||||
return;
|
||||
}
|
||||
|
||||
let form = settingsValues.formFromJson(
|
||||
this.state.settings.json, KeymapsForm.AllowdOps);
|
||||
let next = Object.assign({}, this.state);
|
||||
next.settings.form = form;
|
||||
next.settings.source = 'form';
|
||||
next.errors.json = '';
|
||||
|
||||
this.setState(next);
|
||||
this.context.store.dispatch(settingActions.save(next.settings));
|
||||
}
|
||||
|
||||
migrateToJson() {
|
||||
let json = settingsValues.jsonFromForm(this.state.settings.form);
|
||||
let next = Object.assign({}, this.state);
|
||||
next.settings.json = json;
|
||||
next.settings.source = 'json';
|
||||
next.errors.json = '';
|
||||
|
||||
this.setState(next);
|
||||
this.context.store.dispatch(settingActions.save(next.settings));
|
||||
}
|
||||
|
||||
bindSource(e) {
|
||||
let from = this.state.settings.source;
|
||||
let to = e.target.value;
|
||||
|
||||
if (from === 'form' && to === 'json') {
|
||||
this.migrateToJson();
|
||||
} else if (from === 'json' && to === 'form') {
|
||||
this.migrateToForm();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsComponent.contextTypes = {
|
||||
store: PropTypes.any,
|
||||
};
|
||||
|
||||
export default SettingsComponent;
|
||||
|
|
|
@ -1,8 +1,27 @@
|
|||
.vimvixen-settings-form {
|
||||
padding: 2px;
|
||||
|
||||
textarea[name=json] {
|
||||
font-family: monospace;
|
||||
width: 100%;
|
||||
min-height: 64ex;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
margin-top: 1rem;
|
||||
|
||||
fieldset:first-of-type {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
legend {
|
||||
font-size: 1.5rem;
|
||||
padding: .5rem 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
12
src/settings/components/ui/add-button.jsx
Normal file
12
src/settings/components/ui/add-button.jsx
Normal file
|
@ -0,0 +1,12 @@
|
|||
import './add-button.scss';
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
class AddButton extends Component {
|
||||
render() {
|
||||
return <input
|
||||
className='ui-add-button' type='button' value='✚'
|
||||
{...this.props} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default AddButton;
|
13
src/settings/components/ui/add-button.scss
Normal file
13
src/settings/components/ui/add-button.scss
Normal file
|
@ -0,0 +1,13 @@
|
|||
.ui-add-button {
|
||||
border: none;
|
||||
padding: 4;
|
||||
display: inline;
|
||||
background: none;
|
||||
font-weight: bold;
|
||||
color: green;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: darkgreen;
|
||||
}
|
||||
}
|
12
src/settings/components/ui/delete-button.jsx
Normal file
12
src/settings/components/ui/delete-button.jsx
Normal file
|
@ -0,0 +1,12 @@
|
|||
import './delete-button.scss';
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
class DeleteButton extends Component {
|
||||
render() {
|
||||
return <input
|
||||
className='ui-delete-button' type='button' value='✖'
|
||||
{...this.props} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default DeleteButton;
|
13
src/settings/components/ui/delete-button.scss
Normal file
13
src/settings/components/ui/delete-button.scss
Normal file
|
@ -0,0 +1,13 @@
|
|||
|
||||
.ui-delete-button {
|
||||
border: none;
|
||||
padding: 4;
|
||||
display: inline;
|
||||
background: none;
|
||||
color: red;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: darkred;
|
||||
}
|
||||
}
|
52
src/settings/components/ui/input.jsx
Normal file
52
src/settings/components/ui/input.jsx
Normal file
|
@ -0,0 +1,52 @@
|
|||
import { h, Component } from 'preact';
|
||||
import './input.scss';
|
||||
|
||||
class Input extends Component {
|
||||
|
||||
renderText(props) {
|
||||
let inputClassName = props.error ? 'input-error' : '';
|
||||
return <div className='settings-ui-input'>
|
||||
<label htmlFor={props.id}>{ props.label }</label>
|
||||
<input type='text' className={inputClassName} {...props} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
renderRadio(props) {
|
||||
let inputClassName = props.error ? 'input-error' : '';
|
||||
return <div className='settings-ui-input'>
|
||||
<label>
|
||||
<input type='radio' className={inputClassName} {...props} />
|
||||
{ props.label }
|
||||
</label>
|
||||
</div>;
|
||||
}
|
||||
|
||||
renderTextArea(props) {
|
||||
let inputClassName = props.error ? 'input-error' : '';
|
||||
return <div className='settings-ui-input'>
|
||||
<label
|
||||
htmlFor={props.id}
|
||||
>{ props.label }</label>
|
||||
<textarea className={inputClassName} {...props} />
|
||||
<p className='settings-ui-input-error'>{ this.props.error }</p>
|
||||
</div>;
|
||||
}
|
||||
|
||||
render() {
|
||||
let { type } = this.props;
|
||||
|
||||
switch (this.props.type) {
|
||||
case 'text':
|
||||
return this.renderText(this.props);
|
||||
case 'radio':
|
||||
return this.renderRadio(this.props);
|
||||
case 'textarea':
|
||||
return this.renderTextArea(this.props);
|
||||
default:
|
||||
console.warn(`Unsupported input type ${type}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default Input;
|
29
src/settings/components/ui/input.scss
Normal file
29
src/settings/components/ui/input.scss
Normal file
|
@ -0,0 +1,29 @@
|
|||
.settings-ui-input {
|
||||
page-break-inside: avoid;
|
||||
|
||||
* {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: bold;
|
||||
min-width: 14rem;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
input[type='text'] {
|
||||
padding: 4px;
|
||||
width: 8rem;
|
||||
}
|
||||
|
||||
input.input-crror,
|
||||
textarea.input-error {
|
||||
box-shadow: 0 0 2px red;
|
||||
}
|
||||
|
||||
&-error {
|
||||
font-weight: bold;
|
||||
color: red;
|
||||
min-height: 1.5em;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { h, render } from 'preact';
|
||||
import SettingsComponent from './components';
|
||||
import reducer from 'settings/reducers/setting';
|
||||
import Provider from 'shared/store/provider';
|
||||
|
@ -9,7 +8,7 @@ const store = createStore(reducer);
|
|||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
let wrapper = document.getElementById('vimvixen-settings');
|
||||
ReactDOM.render(
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<SettingsComponent />
|
||||
</Provider>,
|
||||
|
|
|
@ -3,6 +3,7 @@ import actions from 'settings/actions';
|
|||
const defaultState = {
|
||||
source: '',
|
||||
json: '',
|
||||
form: null,
|
||||
value: {}
|
||||
};
|
||||
|
||||
|
@ -12,6 +13,7 @@ export default function reducer(state = defaultState, action = {}) {
|
|||
return {
|
||||
source: action.source,
|
||||
json: action.json,
|
||||
form: action.form,
|
||||
value: action.value,
|
||||
};
|
||||
default:
|
||||
|
|
|
@ -1,169 +0,0 @@
|
|||
import * as tabs from 'background/tabs';
|
||||
import * as histories from 'background/histories';
|
||||
|
||||
const normalizeUrl = (args, searchConfig) => {
|
||||
let concat = args.join(' ');
|
||||
try {
|
||||
return new URL(concat).href;
|
||||
} catch (e) {
|
||||
if (concat.includes('.') && !concat.includes(' ')) {
|
||||
return 'http://' + concat;
|
||||
}
|
||||
let query = encodeURI(concat);
|
||||
let template = searchConfig.engines[
|
||||
searchConfig.default
|
||||
];
|
||||
for (let key in searchConfig.engines) {
|
||||
if (args[0] === key) {
|
||||
query = args.slice(1).join(' ');
|
||||
template = searchConfig.engines[key];
|
||||
}
|
||||
}
|
||||
return template.replace('{}', query);
|
||||
}
|
||||
};
|
||||
|
||||
const openCommand = (url) => {
|
||||
return browser.tabs.query({
|
||||
active: true, currentWindow: true
|
||||
}).then((gotTabs) => {
|
||||
if (gotTabs.length > 0) {
|
||||
return browser.tabs.update(gotTabs[0].id, { url: url });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const tabopenCommand = (url) => {
|
||||
return browser.tabs.create({ url: url });
|
||||
};
|
||||
|
||||
const winopenCommand = (url) => {
|
||||
return browser.windows.create({ url });
|
||||
};
|
||||
|
||||
const bufferCommand = (keywords) => {
|
||||
if (keywords.length === 0) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
let keywordsStr = keywords.join(' ');
|
||||
return browser.tabs.query({
|
||||
active: true, currentWindow: true
|
||||
}).then((gotTabs) => {
|
||||
if (gotTabs.length > 0) {
|
||||
if (isNaN(keywordsStr)) {
|
||||
return tabs.selectByKeyword(gotTabs[0], keywordsStr);
|
||||
}
|
||||
let index = parseInt(keywordsStr, 10) - 1;
|
||||
return tabs.selectAt(index);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getOpenCompletions = (command, keywords, searchConfig) => {
|
||||
return histories.getCompletions(keywords).then((pages) => {
|
||||
let historyItems = pages.map((page) => {
|
||||
return {
|
||||
caption: page.title,
|
||||
content: command + ' ' + page.url,
|
||||
url: page.url
|
||||
};
|
||||
});
|
||||
let engineNames = Object.keys(searchConfig.engines);
|
||||
let engineItems = engineNames.filter(name => name.startsWith(keywords))
|
||||
.map(name => ({
|
||||
caption: name,
|
||||
content: command + ' ' + name
|
||||
}));
|
||||
|
||||
let completions = [];
|
||||
if (engineItems.length > 0) {
|
||||
completions.push({
|
||||
name: 'Search Engines',
|
||||
items: engineItems
|
||||
});
|
||||
}
|
||||
if (historyItems.length > 0) {
|
||||
completions.push({
|
||||
name: 'History',
|
||||
items: historyItems
|
||||
});
|
||||
}
|
||||
return completions;
|
||||
});
|
||||
};
|
||||
|
||||
const doCommand = (line, settings) => {
|
||||
let words = line.trim().split(/ +/);
|
||||
let name = words.shift();
|
||||
|
||||
switch (name) {
|
||||
case 'o':
|
||||
case 'open':
|
||||
return openCommand(normalizeUrl(words, settings.search));
|
||||
case 't':
|
||||
case 'tabopen':
|
||||
return tabopenCommand(normalizeUrl(words, settings.search));
|
||||
case 'w':
|
||||
case 'winopen':
|
||||
return winopenCommand(normalizeUrl(words, settings.search));
|
||||
case 'b':
|
||||
case 'buffer':
|
||||
return bufferCommand(words);
|
||||
case '':
|
||||
return Promise.resolve();
|
||||
}
|
||||
throw new Error(name + ' command is not defined');
|
||||
};
|
||||
|
||||
const getCompletions = (line, settings) => {
|
||||
let typedWords = line.trim().split(/ +/);
|
||||
let typing = '';
|
||||
if (!line.endsWith(' ')) {
|
||||
typing = typedWords.pop();
|
||||
}
|
||||
|
||||
if (typedWords.length === 0) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
let name = typedWords.shift();
|
||||
let keywords = typedWords.concat(typing).join(' ');
|
||||
|
||||
switch (name) {
|
||||
case 'o':
|
||||
case 'open':
|
||||
case 't':
|
||||
case 'tabopen':
|
||||
case 'w':
|
||||
case 'winopen':
|
||||
return getOpenCompletions(name, keywords, settings.search);
|
||||
case 'b':
|
||||
case 'buffer':
|
||||
return tabs.getCompletions(keywords).then((gotTabs) => {
|
||||
let items = gotTabs.map((tab) => {
|
||||
return {
|
||||
caption: tab.title,
|
||||
content: name + ' ' + tab.title,
|
||||
url: tab.url,
|
||||
icon: tab.favIconUrl
|
||||
};
|
||||
});
|
||||
return [
|
||||
{
|
||||
name: 'Buffers',
|
||||
items: items
|
||||
}
|
||||
];
|
||||
});
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
};
|
||||
|
||||
const exec = (line, settings) => {
|
||||
return doCommand(line, settings);
|
||||
};
|
||||
|
||||
const complete = (line, settings) => {
|
||||
return getCompletions(line, settings);
|
||||
};
|
||||
|
||||
export { exec, complete };
|
84
src/shared/commands/complete.js
Normal file
84
src/shared/commands/complete.js
Normal file
|
@ -0,0 +1,84 @@
|
|||
import * as tabs from 'background/tabs';
|
||||
import * as histories from 'background/histories';
|
||||
|
||||
const getOpenCompletions = (command, keywords, searchConfig) => {
|
||||
return histories.getCompletions(keywords).then((pages) => {
|
||||
let historyItems = pages.map((page) => {
|
||||
return {
|
||||
caption: page.title,
|
||||
content: command + ' ' + page.url,
|
||||
url: page.url
|
||||
};
|
||||
});
|
||||
let engineNames = Object.keys(searchConfig.engines);
|
||||
let engineItems = engineNames.filter(name => name.startsWith(keywords))
|
||||
.map(name => ({
|
||||
caption: name,
|
||||
content: command + ' ' + name
|
||||
}));
|
||||
|
||||
let completions = [];
|
||||
if (engineItems.length > 0) {
|
||||
completions.push({
|
||||
name: 'Search Engines',
|
||||
items: engineItems
|
||||
});
|
||||
}
|
||||
if (historyItems.length > 0) {
|
||||
completions.push({
|
||||
name: 'History',
|
||||
items: historyItems
|
||||
});
|
||||
}
|
||||
return completions;
|
||||
});
|
||||
};
|
||||
|
||||
const getCompletions = (line, settings) => {
|
||||
let typedWords = line.trim().split(/ +/);
|
||||
let typing = '';
|
||||
if (!line.endsWith(' ')) {
|
||||
typing = typedWords.pop();
|
||||
}
|
||||
|
||||
if (typedWords.length === 0) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
let name = typedWords.shift();
|
||||
let keywords = typedWords.concat(typing).join(' ');
|
||||
|
||||
switch (name) {
|
||||
case 'o':
|
||||
case 'open':
|
||||
case 't':
|
||||
case 'tabopen':
|
||||
case 'w':
|
||||
case 'winopen':
|
||||
return getOpenCompletions(name, keywords, settings.search);
|
||||
case 'b':
|
||||
case 'buffer':
|
||||
return tabs.getCompletions(keywords).then((gotTabs) => {
|
||||
let items = gotTabs.map((tab) => {
|
||||
return {
|
||||
caption: tab.title,
|
||||
content: name + ' ' + tab.title,
|
||||
url: tab.url,
|
||||
icon: tab.favIconUrl
|
||||
};
|
||||
});
|
||||
return [
|
||||
{
|
||||
name: 'Buffers',
|
||||
items: items
|
||||
}
|
||||
];
|
||||
});
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
};
|
||||
|
||||
const complete = (line, settings) => {
|
||||
return getCompletions(line, settings);
|
||||
};
|
||||
|
||||
export default complete;
|
3
src/shared/commands/index.js
Normal file
3
src/shared/commands/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import complete from './complete';
|
||||
|
||||
export { complete };
|
59
src/shared/commands/parsers.js
Normal file
59
src/shared/commands/parsers.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
const normalizeUrl = (args, searchConfig) => {
|
||||
let concat = args.join(' ');
|
||||
try {
|
||||
return new URL(concat).href;
|
||||
} catch (e) {
|
||||
if (concat.includes('.') && !concat.includes(' ')) {
|
||||
return 'http://' + concat;
|
||||
}
|
||||
let query = concat;
|
||||
let template = searchConfig.engines[
|
||||
searchConfig.default
|
||||
];
|
||||
for (let key in searchConfig.engines) {
|
||||
if (args[0] === key) {
|
||||
query = args.slice(1).join(' ');
|
||||
template = searchConfig.engines[key];
|
||||
}
|
||||
}
|
||||
return template.replace('{}', encodeURIComponent(query));
|
||||
}
|
||||
};
|
||||
|
||||
const mustNumber = (v) => {
|
||||
let num = Number(v);
|
||||
if (isNaN(num)) {
|
||||
throw new Error('Not number: ' + v);
|
||||
}
|
||||
return num;
|
||||
};
|
||||
|
||||
const parseSetOption = (word, types) => {
|
||||
let [key, value] = word.split('=');
|
||||
if (value === undefined) {
|
||||
value = !key.startsWith('no');
|
||||
key = value ? key : key.slice(2);
|
||||
}
|
||||
let type = types[key];
|
||||
if (!type) {
|
||||
throw new Error('Unknown property: ' + key);
|
||||
}
|
||||
if (type === 'boolean' && typeof value !== 'boolean' ||
|
||||
type !== 'boolean' && typeof value === 'boolean') {
|
||||
throw new Error('Invalid argument: ' + word);
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'string': return [key, value];
|
||||
case 'number': return [key, mustNumber(value)];
|
||||
case 'boolean': return [key, value];
|
||||
}
|
||||
};
|
||||
|
||||
const parseCommandLine = (line) => {
|
||||
let words = line.trim().split(/ +/);
|
||||
let name = words.shift();
|
||||
return [name, words];
|
||||
};
|
||||
|
||||
export { normalizeUrl, parseCommandLine, parseSetOption };
|
|
@ -32,6 +32,7 @@ export default {
|
|||
CONSOLE_SHOW_ERROR: 'console.show.error',
|
||||
CONSOLE_SHOW_INFO: 'console.show.info',
|
||||
CONSOLE_SHOW_FIND: 'console.show.find',
|
||||
CONSOLE_HIDE: 'console.hide',
|
||||
|
||||
FOLLOW_START: 'follow.start',
|
||||
FOLLOW_REQUEST_COUNT_TARGETS: 'follow.request.count.targets',
|
||||
|
@ -44,6 +45,8 @@ export default {
|
|||
|
||||
FIND_NEXT: 'find.next',
|
||||
FIND_PREV: 'find.prev',
|
||||
FIND_GET_KEYWORD: 'find.get.keyword',
|
||||
FIND_SET_KEYWORD: 'find.set.keyword',
|
||||
|
||||
OPEN_URL: 'open.url',
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
export default {
|
||||
// Hide console, or cancel some user actions
|
||||
CANCEL: 'cancel',
|
||||
|
||||
// Addons
|
||||
ADDON_ENABLE: 'addon.enable',
|
||||
ADDON_DISABLE: 'addon.disable',
|
||||
|
@ -31,13 +34,18 @@ export default {
|
|||
NAVIGATE_PARENT: 'navigate.parent',
|
||||
NAVIGATE_ROOT: 'navigate.root',
|
||||
|
||||
// Focus
|
||||
FOCUS_INPUT: 'focus.input',
|
||||
|
||||
// Tabs
|
||||
TAB_CLOSE: 'tabs.close',
|
||||
TAB_CLOSE_FORCE: 'tabs.close.force',
|
||||
TAB_REOPEN: 'tabs.reopen',
|
||||
TAB_PREV: 'tabs.prev',
|
||||
TAB_NEXT: 'tabs.next',
|
||||
TAB_FIRST: 'tabs.first',
|
||||
TAB_LAST: 'tabs.last',
|
||||
TAB_PREV_SEL: 'tabs.prevsel',
|
||||
TAB_RELOAD: 'tabs.reload',
|
||||
TAB_PIN: 'tabs.pin',
|
||||
TAB_UNPIN: 'tabs.unpin',
|
||||
|
@ -49,8 +57,9 @@ export default {
|
|||
ZOOM_OUT: 'zoom.out',
|
||||
ZOOM_NEUTRAL: 'zoom.neutral',
|
||||
|
||||
// Url yank
|
||||
// Url yank/paste
|
||||
URLS_YANK: 'urls.yank',
|
||||
URLS_PASTE: 'urls.paste',
|
||||
|
||||
// Find
|
||||
FIND_START: 'find.start',
|
||||
|
|
|
@ -15,8 +15,6 @@ export default {
|
|||
"j": { "type": "scroll.vertically", "count": 1 },
|
||||
"h": { "type": "scroll.horizonally", "count": -1 },
|
||||
"l": { "type": "scroll.horizonally", "count": 1 },
|
||||
"<C-Y>": { "type": "scroll.vertically", "count": -1 },
|
||||
"<C-E>": { "type": "scroll.vertically", "count": 1 },
|
||||
"<C-U>": { "type": "scroll.pages", "count": -0.5 },
|
||||
"<C-D>": { "type": "scroll.pages", "count": 0.5 },
|
||||
"<C-B>": { "type": "scroll.pages", "count": -1 },
|
||||
|
@ -25,6 +23,7 @@ export default {
|
|||
"G": { "type": "scroll.bottom" },
|
||||
"$": { "type": "scroll.end" },
|
||||
"d": { "type": "tabs.close" },
|
||||
"!d": { "type": "tabs.close.force" },
|
||||
"u": { "type": "tabs.reopen" },
|
||||
"K": { "type": "tabs.prev", "count": 1 },
|
||||
"J": { "type": "tabs.next", "count": 1 },
|
||||
|
@ -32,6 +31,7 @@ export default {
|
|||
"gt": { "type": "tabs.next", "count": 1 },
|
||||
"g0": { "type": "tabs.first" },
|
||||
"g$": { "type": "tabs.last" },
|
||||
"<C-6>": { "type": "tabs.prevsel" },
|
||||
"r": { "type": "tabs.reload", "cache": false },
|
||||
"R": { "type": "tabs.reload", "cache": true },
|
||||
"zp": { "type": "tabs.pin.toggle" },
|
||||
|
@ -39,15 +39,18 @@ export default {
|
|||
"zi": { "type": "zoom.in" },
|
||||
"zo": { "type": "zoom.out" },
|
||||
"zz": { "type": "zoom.neutral" },
|
||||
"f": { "type": "follow.start", "newTab": false },
|
||||
"F": { "type": "follow.start", "newTab": true },
|
||||
"f": { "type": "follow.start", "newTab": false, "background": false },
|
||||
"F": { "type": "follow.start", "newTab": true, "background": false },
|
||||
"H": { "type": "navigate.history.prev" },
|
||||
"L": { "type": "navigate.history.next" },
|
||||
"[[": { "type": "navigate.link.prev" },
|
||||
"]]": { "type": "navigate.link.next" },
|
||||
"gu": { "type": "navigate.parent" },
|
||||
"gU": { "type": "navigate.root" },
|
||||
"gi": { "type": "focus.input" },
|
||||
"y": { "type": "urls.yank" },
|
||||
"p": { "type": "urls.paste", "newTab": false },
|
||||
"P": { "type": "urls.paste", "newTab": true },
|
||||
"/": { "type": "find.start" },
|
||||
"n": { "type": "find.next" },
|
||||
"N": { "type": "find.prev" },
|
||||
|
@ -63,6 +66,8 @@ export default {
|
|||
"twitter": "https://twitter.com/search?q={}",
|
||||
"wikipedia": "https://en.wikipedia.org/w/index.php?search={}"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
}
|
||||
}`
|
||||
}`,
|
||||
};
|
18
src/shared/settings/properties.js
Normal file
18
src/shared/settings/properties.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
// describe types of a propety as:
|
||||
// mystr: 'string',
|
||||
// mynum: 'number',
|
||||
// mybool: 'boolean',
|
||||
const types = {
|
||||
hintchars: 'string',
|
||||
smoothscroll: 'boolean',
|
||||
adjacenttab: 'boolean',
|
||||
};
|
||||
|
||||
// describe default values of a property
|
||||
const defaults = {
|
||||
hintchars: 'abcdefghijklmnopqrstuvwxyz',
|
||||
smoothscroll: false,
|
||||
adjacenttab: true,
|
||||
};
|
||||
|
||||
export { types, defaults };
|
36
src/shared/settings/storage.js
Normal file
36
src/shared/settings/storage.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
import DefaultSettings from './default';
|
||||
import * as settingsValues from './values';
|
||||
|
||||
const loadRaw = () => {
|
||||
return browser.storage.local.get('settings').then(({ settings }) => {
|
||||
if (!settings) {
|
||||
return DefaultSettings;
|
||||
}
|
||||
return Object.assign({}, DefaultSettings, settings);
|
||||
});
|
||||
};
|
||||
|
||||
const loadValue = () => {
|
||||
return loadRaw().then((settings) => {
|
||||
let value = JSON.parse(DefaultSettings.json);
|
||||
if (settings.source === 'json') {
|
||||
value = settingsValues.valueFromJson(settings.json);
|
||||
} else if (settings.source === 'form') {
|
||||
value = settingsValues.valueFromForm(settings.form);
|
||||
}
|
||||
if (!value.properties) {
|
||||
value.properties = {};
|
||||
}
|
||||
return Object.assign({},
|
||||
settingsValues.valueFromJson(DefaultSettings.json),
|
||||
value);
|
||||
});
|
||||
};
|
||||
|
||||
const save = (settings) => {
|
||||
return browser.storage.local.set({
|
||||
settings,
|
||||
});
|
||||
};
|
||||
|
||||
export { loadRaw, loadValue, save };
|
|
@ -1,6 +1,7 @@
|
|||
import operations from 'shared/operations';
|
||||
import * as properties from './properties';
|
||||
|
||||
const VALID_TOP_KEYS = ['keymaps', 'search', 'blacklist'];
|
||||
const VALID_TOP_KEYS = ['keymaps', 'search', 'blacklist', 'properties'];
|
||||
const VALID_OPERATION_VALUES = Object.keys(operations).map((key) => {
|
||||
return operations[key];
|
||||
});
|
||||
|
@ -48,6 +49,17 @@ const validateSearch = (search) => {
|
|||
}
|
||||
};
|
||||
|
||||
const validateProperties = (props) => {
|
||||
for (let name of Object.keys(props)) {
|
||||
if (!properties.types[name]) {
|
||||
throw new Error(`Unknown property name: "${name}"`);
|
||||
}
|
||||
if (typeof props[name] !== properties.types[name]) {
|
||||
throw new Error(`Invalid type for property: "${name}"`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const validate = (settings) => {
|
||||
validateInvalidTopKeys(settings);
|
||||
if (settings.keymaps) {
|
||||
|
@ -56,6 +68,9 @@ const validate = (settings) => {
|
|||
if (settings.search) {
|
||||
validateSearch(settings.search);
|
||||
}
|
||||
if (settings.properties) {
|
||||
validateProperties(settings.properties);
|
||||
}
|
||||
};
|
||||
|
||||
export { validate };
|
108
src/shared/settings/values.js
Normal file
108
src/shared/settings/values.js
Normal file
|
@ -0,0 +1,108 @@
|
|||
import * as properties from './properties';
|
||||
|
||||
const operationFromFormName = (name) => {
|
||||
let [type, argStr] = name.split('?');
|
||||
let args = {};
|
||||
if (argStr) {
|
||||
args = JSON.parse(argStr);
|
||||
}
|
||||
return Object.assign({ type }, args);
|
||||
};
|
||||
|
||||
const operationToFormName = (op) => {
|
||||
let type = op.type;
|
||||
let args = Object.assign({}, op);
|
||||
delete args.type;
|
||||
|
||||
if (Object.keys(args).length === 0) {
|
||||
return type;
|
||||
}
|
||||
return op.type + '?' + JSON.stringify(args);
|
||||
};
|
||||
|
||||
const valueFromJson = (json) => {
|
||||
return JSON.parse(json);
|
||||
};
|
||||
|
||||
const valueFromForm = (form) => {
|
||||
let keymaps = undefined;
|
||||
if (form.keymaps) {
|
||||
keymaps = {};
|
||||
for (let name of Object.keys(form.keymaps)) {
|
||||
let keys = form.keymaps[name];
|
||||
keymaps[keys] = operationFromFormName(name);
|
||||
}
|
||||
}
|
||||
|
||||
let search = undefined;
|
||||
if (form.search) {
|
||||
search = { default: form.search.default };
|
||||
|
||||
if (form.search.engines) {
|
||||
search.engines = {};
|
||||
for (let [name, url] of form.search.engines) {
|
||||
search.engines[name] = url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
keymaps,
|
||||
search,
|
||||
blacklist: form.blacklist,
|
||||
properties: form.properties
|
||||
};
|
||||
};
|
||||
|
||||
const jsonFromValue = (value) => {
|
||||
return JSON.stringify(value, undefined, 2);
|
||||
};
|
||||
|
||||
const formFromValue = (value, allowedOps) => {
|
||||
let keymaps = undefined;
|
||||
|
||||
if (value.keymaps) {
|
||||
let allowedSet = new Set(allowedOps);
|
||||
|
||||
keymaps = {};
|
||||
for (let keys of Object.keys(value.keymaps)) {
|
||||
let op = operationToFormName(value.keymaps[keys]);
|
||||
if (allowedSet.has(op)) {
|
||||
keymaps[op] = keys;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let search = undefined;
|
||||
if (value.search) {
|
||||
search = { default: value.search.default };
|
||||
if (value.search.engines) {
|
||||
search.engines = Object.keys(value.search.engines).map((name) => {
|
||||
return [name, value.search.engines[name]];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let formProperties = Object.assign({}, properties.defaults, value.properties);
|
||||
|
||||
return {
|
||||
keymaps,
|
||||
search,
|
||||
blacklist: value.blacklist,
|
||||
properties: formProperties,
|
||||
};
|
||||
};
|
||||
|
||||
const jsonFromForm = (form) => {
|
||||
return jsonFromValue(valueFromForm(form));
|
||||
};
|
||||
|
||||
const formFromJson = (json, allowedOps) => {
|
||||
let value = valueFromJson(json);
|
||||
return formFromValue(value, allowedOps);
|
||||
};
|
||||
|
||||
export {
|
||||
valueFromJson, valueFromForm, jsonFromValue, formFromValue,
|
||||
jsonFromForm, formFromJson
|
||||
};
|
|
@ -1,18 +1,15 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
class Provider extends React.PureComponent {
|
||||
class Provider extends Component {
|
||||
getChildContext() {
|
||||
return { store: this.props.store };
|
||||
}
|
||||
|
||||
render() {
|
||||
return React.Children.only(this.props.children);
|
||||
return <div>
|
||||
{ this.props.children }
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
Provider.childContextTypes = {
|
||||
store: PropTypes.any,
|
||||
};
|
||||
|
||||
export default Provider;
|
||||
|
|
|
@ -81,4 +81,28 @@ const viewportRect = (e) => {
|
|||
};
|
||||
};
|
||||
|
||||
export { isContentEditable, viewportRect };
|
||||
const isVisible = (element) => {
|
||||
let rect = element.getBoundingClientRect();
|
||||
let style = window.getComputedStyle(element);
|
||||
|
||||
if (style.overflow !== 'visible' && (rect.width === 0 || rect.height === 0)) {
|
||||
return false;
|
||||
}
|
||||
if (rect.right < 0 && rect.bottom < 0) {
|
||||
return false;
|
||||
}
|
||||
if (window.innerWidth < rect.left && window.innerHeight < rect.top) {
|
||||
return false;
|
||||
}
|
||||
if (element.nodeName === 'INPUT' && element.type.toLowerCase() === 'hidden') {
|
||||
return false;
|
||||
}
|
||||
|
||||
let { display, visibility } = window.getComputedStyle(element);
|
||||
if (display === 'none' || visibility === 'hidden') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export { isContentEditable, viewportRect, isVisible };
|
||||
|
|
|
@ -18,6 +18,7 @@ const fromKeyboardEvent = (e) => {
|
|||
|
||||
return {
|
||||
key: modifierdKeyName(e.key),
|
||||
repeat: e.repeat,
|
||||
shiftKey: shift,
|
||||
ctrlKey: e.ctrlKey,
|
||||
altKey: e.altKey,
|
||||
|
|
Reference in a new issue