aboutsummaryrefslogtreecommitdiff
path: root/src/shared
diff options
context:
space:
mode:
Diffstat (limited to 'src/shared')
-rw-r--r--src/shared/commands.js169
-rw-r--r--src/shared/commands/complete.js84
-rw-r--r--src/shared/commands/index.js3
-rw-r--r--src/shared/commands/parsers.js59
-rw-r--r--src/shared/messages.js3
-rw-r--r--src/shared/operations.js11
-rw-r--r--src/shared/settings/default.js (renamed from src/shared/default-settings.js)15
-rw-r--r--src/shared/settings/properties.js18
-rw-r--r--src/shared/settings/storage.js36
-rw-r--r--src/shared/settings/validator.js (renamed from src/shared/validators/setting.js)17
-rw-r--r--src/shared/settings/values.js108
-rw-r--r--src/shared/store/provider.jsx13
-rw-r--r--src/shared/utils/dom.js26
-rw-r--r--src/shared/utils/keys.js1
14 files changed, 378 insertions, 185 deletions
diff --git a/src/shared/commands.js b/src/shared/commands.js
deleted file mode 100644
index 8edeb5c..0000000
--- a/src/shared/commands.js
+++ /dev/null
@@ -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 };
diff --git a/src/shared/commands/complete.js b/src/shared/commands/complete.js
new file mode 100644
index 0000000..0bdbab8
--- /dev/null
+++ b/src/shared/commands/complete.js
@@ -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;
diff --git a/src/shared/commands/index.js b/src/shared/commands/index.js
new file mode 100644
index 0000000..78cb4df
--- /dev/null
+++ b/src/shared/commands/index.js
@@ -0,0 +1,3 @@
+import complete from './complete';
+
+export { complete };
diff --git a/src/shared/commands/parsers.js b/src/shared/commands/parsers.js
new file mode 100644
index 0000000..fb37d2a
--- /dev/null
+++ b/src/shared/commands/parsers.js
@@ -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 };
diff --git a/src/shared/messages.js b/src/shared/messages.js
index de00a3f..a404658 100644
--- a/src/shared/messages.js
+++ b/src/shared/messages.js
@@ -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',
diff --git a/src/shared/operations.js b/src/shared/operations.js
index 4c221ba..a2f980f 100644
--- a/src/shared/operations.js
+++ b/src/shared/operations.js
@@ -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',
diff --git a/src/shared/default-settings.js b/src/shared/settings/default.js
index e9c3d15..3b6706d 100644
--- a/src/shared/default-settings.js
+++ b/src/shared/settings/default.js
@@ -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": {
}
-}`
+}`,
};
diff --git a/src/shared/settings/properties.js b/src/shared/settings/properties.js
new file mode 100644
index 0000000..4bda8d6
--- /dev/null
+++ b/src/shared/settings/properties.js
@@ -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 };
diff --git a/src/shared/settings/storage.js b/src/shared/settings/storage.js
new file mode 100644
index 0000000..9b8045d
--- /dev/null
+++ b/src/shared/settings/storage.js
@@ -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 };
diff --git a/src/shared/validators/setting.js b/src/shared/settings/validator.js
index 949ab29..1589420 100644
--- a/src/shared/validators/setting.js
+++ b/src/shared/settings/validator.js
@@ -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 };
diff --git a/src/shared/settings/values.js b/src/shared/settings/values.js
new file mode 100644
index 0000000..bd03be2
--- /dev/null
+++ b/src/shared/settings/values.js
@@ -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
+};
diff --git a/src/shared/store/provider.jsx b/src/shared/store/provider.jsx
index 743f656..fe925aa 100644
--- a/src/shared/store/provider.jsx
+++ b/src/shared/store/provider.jsx
@@ -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;
diff --git a/src/shared/utils/dom.js b/src/shared/utils/dom.js
index d4fd68a..974d534 100644
--- a/src/shared/utils/dom.js
+++ b/src/shared/utils/dom.js
@@ -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 };
diff --git a/src/shared/utils/keys.js b/src/shared/utils/keys.js
index fba8ce3..8a86dfb 100644
--- a/src/shared/utils/keys.js
+++ b/src/shared/utils/keys.js
@@ -18,6 +18,7 @@ const fromKeyboardEvent = (e) => {
return {
key: modifierdKeyName(e.key),
+ repeat: e.repeat,
shiftKey: shift,
ctrlKey: e.ctrlKey,
altKey: e.altKey,