commit
0b37c2250e
30 changed files with 1190 additions and 147 deletions
|
@ -36,6 +36,7 @@
|
||||||
"newline-after-var": "off",
|
"newline-after-var": "off",
|
||||||
"newline-before-return": "off",
|
"newline-before-return": "off",
|
||||||
"newline-per-chained-call": "off",
|
"newline-per-chained-call": "off",
|
||||||
|
"no-alert": "off",
|
||||||
"no-bitwise": "off",
|
"no-bitwise": "off",
|
||||||
"no-console": ["error", { "allow": ["warn", "error"] }],
|
"no-console": ["error", { "allow": ["warn", "error"] }],
|
||||||
"no-empty-function": "off",
|
"no-empty-function": "off",
|
||||||
|
@ -44,6 +45,8 @@
|
||||||
"no-plusplus": "off",
|
"no-plusplus": "off",
|
||||||
"no-ternary": "off",
|
"no-ternary": "off",
|
||||||
"no-undefined": "off",
|
"no-undefined": "off",
|
||||||
|
"no-undef-init": "off",
|
||||||
|
"no-unused-vars": ["error", { "varsIgnorePattern": "h" }],
|
||||||
"no-use-before-define": "off",
|
"no-use-before-define": "off",
|
||||||
"no-warning-comments": "off",
|
"no-warning-comments": "off",
|
||||||
"object-curly-newline": ["error", { "consistent": true }],
|
"object-curly-newline": ["error", { "consistent": true }],
|
||||||
|
@ -65,5 +68,6 @@
|
||||||
|
|
||||||
"react/jsx-indent": ["error", 2],
|
"react/jsx-indent": ["error", 2],
|
||||||
"react/prop-types": "off",
|
"react/prop-types": "off",
|
||||||
|
"react/react-in-jsx-scope": "off"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,11 +7,13 @@ module.exports = function (config) {
|
||||||
frameworks: ['mocha'],
|
frameworks: ['mocha'],
|
||||||
files: [
|
files: [
|
||||||
'test/**/*.test.js',
|
'test/**/*.test.js',
|
||||||
|
'test/**/*.test.jsx',
|
||||||
'test/**/*.html'
|
'test/**/*.html'
|
||||||
],
|
],
|
||||||
|
|
||||||
preprocessors: {
|
preprocessors: {
|
||||||
'test/**/*.test.js': [ 'webpack' ],
|
'test/**/*.test.js': [ 'webpack' ],
|
||||||
|
'test/**/*.test.jsx': [ 'webpack' ],
|
||||||
'test/**/*.html': ['html2js']
|
'test/**/*.html': ['html2js']
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
98
package-lock.json
generated
98
package-lock.json
generated
|
@ -735,12 +735,6 @@
|
||||||
"babel-helper-is-void-0": "0.2.0"
|
"babel-helper-is-void-0": "0.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"babel-plugin-syntax-flow": {
|
|
||||||
"version": "6.18.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz",
|
|
||||||
"integrity": "sha1-TDqyCiryaqIM0lmVw5jE63AxDI0=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"babel-plugin-syntax-jsx": {
|
"babel-plugin-syntax-jsx": {
|
||||||
"version": "6.18.0",
|
"version": "6.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz",
|
||||||
|
@ -981,16 +975,6 @@
|
||||||
"regexpu-core": "2.0.0"
|
"regexpu-core": "2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"babel-plugin-transform-flow-strip-types": {
|
|
||||||
"version": "6.22.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.22.0.tgz",
|
|
||||||
"integrity": "sha1-hMtnKTXUNxT9wyvOhFaNh0Qc988=",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"babel-plugin-syntax-flow": "6.18.0",
|
|
||||||
"babel-runtime": "6.25.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"babel-plugin-transform-inline-consecutive-adds": {
|
"babel-plugin-transform-inline-consecutive-adds": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/babel-plugin-transform-inline-consecutive-adds/-/babel-plugin-transform-inline-consecutive-adds-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/babel-plugin-transform-inline-consecutive-adds/-/babel-plugin-transform-inline-consecutive-adds-0.2.0.tgz",
|
||||||
|
@ -1024,15 +1008,6 @@
|
||||||
"esutils": "2.0.2"
|
"esutils": "2.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"babel-plugin-transform-react-display-name": {
|
|
||||||
"version": "6.25.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/babel-plugin-transform-react-display-name/-/babel-plugin-transform-react-display-name-6.25.0.tgz",
|
|
||||||
"integrity": "sha1-Z+K/Hx6ck6sI25Z5LgU5K/LMKNE=",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"babel-runtime": "6.25.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"babel-plugin-transform-react-jsx": {
|
"babel-plugin-transform-react-jsx": {
|
||||||
"version": "6.24.1",
|
"version": "6.24.1",
|
||||||
"resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx/-/babel-plugin-transform-react-jsx-6.24.1.tgz",
|
"resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx/-/babel-plugin-transform-react-jsx-6.24.1.tgz",
|
||||||
|
@ -1044,26 +1019,6 @@
|
||||||
"babel-runtime": "6.25.0"
|
"babel-runtime": "6.25.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"babel-plugin-transform-react-jsx-self": {
|
|
||||||
"version": "6.22.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx-self/-/babel-plugin-transform-react-jsx-self-6.22.0.tgz",
|
|
||||||
"integrity": "sha1-322AqdomEqEh5t3XVYvL7PBuY24=",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"babel-plugin-syntax-jsx": "6.18.0",
|
|
||||||
"babel-runtime": "6.25.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"babel-plugin-transform-react-jsx-source": {
|
|
||||||
"version": "6.22.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx-source/-/babel-plugin-transform-react-jsx-source-6.22.0.tgz",
|
|
||||||
"integrity": "sha1-ZqwSFT9c0tF7PBkmj0vwGX9E7NY=",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"babel-plugin-syntax-jsx": "6.18.0",
|
|
||||||
"babel-runtime": "6.25.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"babel-plugin-transform-regenerator": {
|
"babel-plugin-transform-regenerator": {
|
||||||
"version": "6.24.1",
|
"version": "6.24.1",
|
||||||
"resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.24.1.tgz",
|
"resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.24.1.tgz",
|
||||||
|
@ -1165,15 +1120,6 @@
|
||||||
"babel-plugin-transform-regenerator": "6.24.1"
|
"babel-plugin-transform-regenerator": "6.24.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"babel-preset-flow": {
|
|
||||||
"version": "6.23.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/babel-preset-flow/-/babel-preset-flow-6.23.0.tgz",
|
|
||||||
"integrity": "sha1-5xIYiHCFrpoktb5Baa/7WZgWxJ0=",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"babel-plugin-transform-flow-strip-types": "6.22.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"babel-preset-minify": {
|
"babel-preset-minify": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/babel-preset-minify/-/babel-preset-minify-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/babel-preset-minify/-/babel-preset-minify-0.2.0.tgz",
|
||||||
|
@ -1205,18 +1151,14 @@
|
||||||
"lodash.isplainobject": "4.0.6"
|
"lodash.isplainobject": "4.0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"babel-preset-react": {
|
"babel-preset-preact": {
|
||||||
"version": "6.24.1",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/babel-preset-react/-/babel-preset-react-6.24.1.tgz",
|
"resolved": "https://registry.npmjs.org/babel-preset-preact/-/babel-preset-preact-1.1.0.tgz",
|
||||||
"integrity": "sha1-umnfrqRfw+xjm2pOzqbhdwLJE4A=",
|
"integrity": "sha1-NaxlWnOkm4Q4FjzgU4Fld+GYCGE=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"babel-plugin-syntax-jsx": "6.18.0",
|
"babel-plugin-syntax-jsx": "6.18.0",
|
||||||
"babel-plugin-transform-react-display-name": "6.25.0",
|
"babel-plugin-transform-react-jsx": "6.24.1"
|
||||||
"babel-plugin-transform-react-jsx": "6.24.1",
|
|
||||||
"babel-plugin-transform-react-jsx-self": "6.22.0",
|
|
||||||
"babel-plugin-transform-react-jsx-source": "6.22.0",
|
|
||||||
"babel-preset-flow": "6.23.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"babel-register": {
|
"babel-register": {
|
||||||
|
@ -6273,6 +6215,12 @@
|
||||||
"uniqs": "2.0.0"
|
"uniqs": "2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"preact": {
|
||||||
|
"version": "8.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/preact/-/preact-8.2.6.tgz",
|
||||||
|
"integrity": "sha1-ACi0Ju+Y/Mp0Gjxhf/W4E7mpR8c=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"prelude-ls": {
|
"prelude-ls": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
|
||||||
|
@ -6491,30 +6439,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react": {
|
|
||||||
"version": "16.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-16.0.0.tgz",
|
|
||||||
"integrity": "sha1-zn348ZQbA28Cssyp29DLHw6FXi0=",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"fbjs": "0.8.16",
|
|
||||||
"loose-envify": "1.3.1",
|
|
||||||
"object-assign": "4.1.1",
|
|
||||||
"prop-types": "15.6.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"react-dom": {
|
|
||||||
"version": "16.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.0.0.tgz",
|
|
||||||
"integrity": "sha1-nMMHnD3NcNTG4BuEqrKn40wwP1g=",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"fbjs": "0.8.16",
|
|
||||||
"loose-envify": "1.3.1",
|
|
||||||
"object-assign": "4.1.1",
|
|
||||||
"prop-types": "15.6.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"read-pkg": {
|
"read-pkg": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz",
|
||||||
|
|
|
@ -3,10 +3,9 @@
|
||||||
"description": "Vim vixen",
|
"description": "Vim vixen",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "webpack -w --debug --devtool inline-source-map",
|
"start": "webpack -w --debug --devtool inline-source-map",
|
||||||
"lint": "eslint --ext .jsx,.js src",
|
|
||||||
"build": "NODE_ENV=production webpack --progress --display-error-details",
|
"build": "NODE_ENV=production webpack --progress --display-error-details",
|
||||||
"package": "npm run build && ./package.sh",
|
"package": "npm run build && ./package.sh",
|
||||||
"lint": "eslint src",
|
"lint": "eslint --ext .jsx,.js src",
|
||||||
"test": "karma start"
|
"test": "karma start"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -24,9 +23,8 @@
|
||||||
"babel-eslint": "^7.2.3",
|
"babel-eslint": "^7.2.3",
|
||||||
"babel-loader": "^7.1.1",
|
"babel-loader": "^7.1.1",
|
||||||
"babel-minify-webpack-plugin": "^0.2.0",
|
"babel-minify-webpack-plugin": "^0.2.0",
|
||||||
"babel-plugin-transform-react-jsx": "^6.24.1",
|
|
||||||
"babel-preset-es2015": "^6.24.1",
|
"babel-preset-es2015": "^6.24.1",
|
||||||
"babel-preset-react": "^6.24.1",
|
"babel-preset-preact": "^1.1.0",
|
||||||
"chai": "^4.1.1",
|
"chai": "^4.1.1",
|
||||||
"css-loader": "^0.28.4",
|
"css-loader": "^0.28.4",
|
||||||
"eslint": "^4.7.0",
|
"eslint": "^4.7.0",
|
||||||
|
@ -41,8 +39,7 @@
|
||||||
"karma-webpack": "^2.0.4",
|
"karma-webpack": "^2.0.4",
|
||||||
"mocha": "^3.5.0",
|
"mocha": "^3.5.0",
|
||||||
"node-sass": "^4.5.3",
|
"node-sass": "^4.5.3",
|
||||||
"react": "^16.0.0",
|
"preact": "^8.2.6",
|
||||||
"react-dom": "^16.0.0",
|
|
||||||
"sass-loader": "^6.0.6",
|
"sass-loader": "^6.0.6",
|
||||||
"style-loader": "^0.18.2",
|
"style-loader": "^0.18.2",
|
||||||
"webpack": "^3.5.3"
|
"webpack": "^3.5.3"
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import actions from 'settings/actions';
|
import actions from 'settings/actions';
|
||||||
import messages from 'shared/messages';
|
import messages from 'shared/messages';
|
||||||
import DefaultSettings from 'shared/default-settings';
|
import DefaultSettings from 'shared/settings/default';
|
||||||
|
import * as settingsValues from 'shared/settings/values';
|
||||||
|
|
||||||
const load = () => {
|
const load = () => {
|
||||||
return browser.storage.local.get('settings').then(({ settings }) => {
|
return browser.storage.local.get('settings').then(({ settings }) => {
|
||||||
if (settings) {
|
if (!settings) {
|
||||||
return set(settings);
|
return set(DefaultSettings);
|
||||||
}
|
}
|
||||||
return set(DefaultSettings);
|
return set(Object.assign({}, DefaultSettings, settings));
|
||||||
}, console.error);
|
}, console.error);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -24,11 +25,19 @@ const save = (settings) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const 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 {
|
return {
|
||||||
type: actions.SETTING_SET_SETTINGS,
|
type: actions.SETTING_SET_SETTINGS,
|
||||||
source: settings.source,
|
source: settings.source,
|
||||||
json: settings.json,
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
99
src/settings/components/form/keymaps-form.jsx
Normal file
99
src/settings/components/form/keymaps-form.jsx
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
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 leftmost'],
|
||||||
|
['scroll.end', 'Scroll last'],
|
||||||
|
['scroll.pages?{"count":-0.5}', 'Scroll up by half of screen'],
|
||||||
|
['scroll.pages?{"count":0.5}', 'Scroll up by half of screen'],
|
||||||
|
['scroll.pages?{"count":-1}', 'Scroll up by a screen'],
|
||||||
|
['scroll.pages?{"count":1}', 'Scroll up 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":true}', 'Reload current tab'],
|
||||||
|
['tabs.pin.toggle', 'Toggle pinned state'],
|
||||||
|
['tabs.duplicate', 'Dupplicate 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'],
|
||||||
|
], [
|
||||||
|
['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'],
|
||||||
|
['zoom.in', 'Zoom-in'],
|
||||||
|
['zoom.out', 'Zoom-out'],
|
||||||
|
['zoom.neutral', 'Reset zoom level'],
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
class KeymapsForm extends Component {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let values = this.props.value;
|
||||||
|
if (!values) {
|
||||||
|
values = {};
|
||||||
|
}
|
||||||
|
return <div className='keymap-fields'>
|
||||||
|
{
|
||||||
|
KeyMapFields.map((group, index) => {
|
||||||
|
return <div key={index} className='form-keymaps-form'>
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default KeymapsForm;
|
9
src/settings/components/form/keymaps-form.scss
Normal file
9
src/settings/components/form/keymaps-form.scss
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
.form-keymaps-form {
|
||||||
|
column-count: 3;
|
||||||
|
.keymap-fields-group {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
.keymap-fields-group:first-of-type {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
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,27 @@
|
||||||
import './site.scss';
|
import './site.scss';
|
||||||
import React from 'react';
|
import { h, Component } from 'preact';
|
||||||
import PropTypes from 'prop-types';
|
import Input from './ui/input';
|
||||||
|
import SearchForm from './form/search-form';
|
||||||
|
import KeymapsForm from './form/keymaps-form';
|
||||||
|
import BlacklistForm from './form/blacklist-form';
|
||||||
import * as settingActions from 'settings/actions/setting';
|
import * as settingActions from 'settings/actions/setting';
|
||||||
import * as validator from 'shared/validators/setting';
|
import * as validator from 'shared/validators/setting';
|
||||||
|
import * as settingsValues from 'shared/settings/values';
|
||||||
|
|
||||||
class SettingsComponent extends React.Component {
|
const DO_YOU_WANT_TO_CONTINUE =
|
||||||
|
'Some settings in JSON can be lose on migrating. ' +
|
||||||
|
'Do you want to continue ?';
|
||||||
|
|
||||||
|
class SettingsComponent extends Component {
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
settings: {
|
settings: {
|
||||||
json: '',
|
json: '',
|
||||||
|
},
|
||||||
|
errors: {
|
||||||
|
json: '',
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.context.store.subscribe(this.stateChanged.bind(this));
|
this.context.store.subscribe(this.stateChanged.bind(this));
|
||||||
|
@ -26,66 +37,140 @@ class SettingsComponent extends React.Component {
|
||||||
settings: {
|
settings: {
|
||||||
source: settings.source,
|
source: settings.source,
|
||||||
json: settings.json,
|
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>
|
||||||
|
</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() {
|
render() {
|
||||||
|
let fields = null;
|
||||||
|
if (this.state.settings.source === 'form') {
|
||||||
|
fields = this.renderFormFields();
|
||||||
|
} else if (this.state.settings.source === 'json') {
|
||||||
|
fields = this.renderJsonFields();
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>Configure Vim-Vixen</h1>
|
<h1>Configure Vim-Vixen</h1>
|
||||||
<form className='vimvixen-settings-form'>
|
<form className='vimvixen-settings-form'>
|
||||||
|
<Input
|
||||||
<p>Load settings from:</p>
|
type='radio'
|
||||||
<input type='radio' id='setting-source-json'
|
id='setting-source-form'
|
||||||
name='source'
|
name='source'
|
||||||
value='json'
|
label='Use form'
|
||||||
onChange={this.bindAndSave.bind(this)}
|
checked={this.state.settings.source === 'form'}
|
||||||
checked={this.state.settings.source === 'json'} />
|
value='form'
|
||||||
<label htmlFor='settings-source-json'>JSON</label>
|
onChange={this.bindSource.bind(this)} />
|
||||||
|
|
||||||
<textarea name='json' spellCheck='false'
|
<Input
|
||||||
onInput={this.validate.bind(this)}
|
type='radio'
|
||||||
onChange={this.bindValue.bind(this)}
|
name='source'
|
||||||
onBlur={this.bindAndSave.bind(this)}
|
label='Use plain JSON'
|
||||||
value={this.state.settings.json} />
|
checked={this.state.settings.source === 'json'}
|
||||||
|
value='json'
|
||||||
|
onChange={this.bindSource.bind(this)} />
|
||||||
|
|
||||||
|
{ fields }
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
validate(e) {
|
validate(target) {
|
||||||
try {
|
if (target.name === 'json') {
|
||||||
let settings = JSON.parse(e.target.value);
|
let settings = JSON.parse(target.value);
|
||||||
validator.validate(settings);
|
validator.validate(settings);
|
||||||
e.target.setCustomValidity('');
|
|
||||||
} catch (err) {
|
|
||||||
e.target.setCustomValidity(err.message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
bindValue(e) {
|
||||||
let nextSettings = Object.assign({}, this.state.settings);
|
let next = Object.assign({}, this.state);
|
||||||
nextSettings[e.target.name] = e.target.value;
|
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
|
this.setState(next);
|
||||||
|
this.context.store.dispatch(settingActions.save(next.settings));
|
||||||
}
|
}
|
||||||
|
|
||||||
bindAndSave(e) {
|
bindSource(e) {
|
||||||
this.bindValue(e);
|
let from = this.state.settings.source;
|
||||||
|
let to = e.target.value;
|
||||||
|
|
||||||
try {
|
let next = Object.assign({}, this.state);
|
||||||
let json = this.state.settings.json;
|
if (from === 'form' && to === 'json') {
|
||||||
validator.validate(JSON.parse(json));
|
next.settings.json =
|
||||||
this.context.store.dispatch(settingActions.save(this.state.settings));
|
settingsValues.jsonFromForm(this.state.settings.form);
|
||||||
} catch (err) {
|
} else if (from === 'json' && to === 'form') {
|
||||||
// error already shown
|
let b = window.confirm(DO_YOU_WANT_TO_CONTINUE);
|
||||||
|
if (!b) {
|
||||||
|
this.setState(this.state);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next.settings.form =
|
||||||
|
settingsValues.formFromJson(this.state.settings.json);
|
||||||
}
|
}
|
||||||
|
next.settings.source = to;
|
||||||
|
|
||||||
|
this.setState(next);
|
||||||
|
this.context.store.dispatch(settingActions.save(next.settings));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsComponent.contextTypes = {
|
|
||||||
store: PropTypes.any,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SettingsComponent;
|
export default SettingsComponent;
|
||||||
|
|
|
@ -1,8 +1,27 @@
|
||||||
.vimvixen-settings-form {
|
.vimvixen-settings-form {
|
||||||
|
padding: 2px;
|
||||||
|
|
||||||
textarea[name=json] {
|
textarea[name=json] {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 64ex;
|
min-height: 64ex;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
margin-top: 1rem;
|
||||||
|
|
||||||
|
fieldset:first-of-type {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
legend {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
padding: .5rem 0;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
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 { h, render } from 'preact';
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
import SettingsComponent from './components';
|
import SettingsComponent from './components';
|
||||||
import reducer from 'settings/reducers/setting';
|
import reducer from 'settings/reducers/setting';
|
||||||
import Provider from 'shared/store/provider';
|
import Provider from 'shared/store/provider';
|
||||||
|
@ -9,7 +8,7 @@ const store = createStore(reducer);
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
let wrapper = document.getElementById('vimvixen-settings');
|
let wrapper = document.getElementById('vimvixen-settings');
|
||||||
ReactDOM.render(
|
render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<SettingsComponent />
|
<SettingsComponent />
|
||||||
</Provider>,
|
</Provider>,
|
||||||
|
|
|
@ -3,6 +3,7 @@ import actions from 'settings/actions';
|
||||||
const defaultState = {
|
const defaultState = {
|
||||||
source: '',
|
source: '',
|
||||||
json: '',
|
json: '',
|
||||||
|
form: null,
|
||||||
value: {}
|
value: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -12,6 +13,7 @@ export default function reducer(state = defaultState, action = {}) {
|
||||||
return {
|
return {
|
||||||
source: action.source,
|
source: action.source,
|
||||||
json: action.json,
|
json: action.json,
|
||||||
|
form: action.form,
|
||||||
value: action.value,
|
value: action.value,
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -62,5 +62,70 @@ export default {
|
||||||
"wikipedia": "https://en.wikipedia.org/w/index.php?search={}"
|
"wikipedia": "https://en.wikipedia.org/w/index.php?search={}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`
|
}`,
|
||||||
|
|
||||||
|
'form': {
|
||||||
|
'keymaps': {
|
||||||
|
'scroll.vertically?{"count":1}': 'j',
|
||||||
|
'scroll.vertically?{"count":-1}': 'k',
|
||||||
|
'scroll.horizonally?{"count":-1}': 'h',
|
||||||
|
'scroll.horizonally?{"count":1}': 'l',
|
||||||
|
'scroll.home': '0',
|
||||||
|
'scroll.end': '$',
|
||||||
|
'scroll.pages?{"count":-0.5}': '<C-U>',
|
||||||
|
'scroll.pages?{"count":0.5}': '<C-D>',
|
||||||
|
'scroll.pages?{"count":-1}': '<C-B>',
|
||||||
|
'scroll.pages?{"count":1}': '<C-F>',
|
||||||
|
|
||||||
|
'tabs.close': 'd',
|
||||||
|
'tabs.reopen': 'u',
|
||||||
|
'tabs.next?{"count":1}': 'J',
|
||||||
|
'tabs.prev?{"count":1}': 'K',
|
||||||
|
'tabs.first': 'g0',
|
||||||
|
'tabs.last': 'g$',
|
||||||
|
'tabs.reload?{"cache":true}': 'r',
|
||||||
|
'tabs.pin.toggle': 'zp',
|
||||||
|
'tabs.duplicate': 'zd',
|
||||||
|
|
||||||
|
'follow.start?{"newTab":false}': 'f',
|
||||||
|
'follow.start?{"newTab":true}': 'F',
|
||||||
|
'navigate.history.prev': 'H',
|
||||||
|
'navigate.history.next': 'L',
|
||||||
|
'navigate.link.next': ']]',
|
||||||
|
'navigate.link.prev': '[[',
|
||||||
|
'navigate.parent': 'gu',
|
||||||
|
'navigate.root': 'gU',
|
||||||
|
|
||||||
|
'find.start': '/',
|
||||||
|
'find.next': 'n',
|
||||||
|
'find.prev': 'N',
|
||||||
|
|
||||||
|
'command.show': ':',
|
||||||
|
'command.show.open?{"alter":false}': 'o',
|
||||||
|
'command.show.open?{"alter":true}': 'O',
|
||||||
|
'command.show.tabopen?{"alter":false}': 't',
|
||||||
|
'command.show.tabopen?{"alter":true}': 'T',
|
||||||
|
'command.show.winopen?{"alter":false}': 'w',
|
||||||
|
'command.show.winopen?{"alter":true}': 'W',
|
||||||
|
'command.show.buffer': 'b',
|
||||||
|
|
||||||
|
'addon.toggle.enabled': '<S-Esc>',
|
||||||
|
'urls.yank': 'y',
|
||||||
|
'zoom.in': 'zi',
|
||||||
|
'zoom.out': 'zo',
|
||||||
|
'zoom.neutral': 'zz',
|
||||||
|
},
|
||||||
|
'search': {
|
||||||
|
'default': 'google',
|
||||||
|
'engines': [
|
||||||
|
['google', 'https,//google.com/search?q={}'],
|
||||||
|
['yahoo', 'https,//search.yahoo.com/search?p={}'],
|
||||||
|
['bing', 'https,//www.bing.com/search?q={}'],
|
||||||
|
['duckduckgo', 'https,//duckduckgo.com/?q={}'],
|
||||||
|
['twitter', 'https,//twitter.com/search?q={}'],
|
||||||
|
['wikipedia', 'https,//en.wikipedia.org/w/index.php?search={}'],
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'blacklist': [],
|
||||||
|
}
|
||||||
};
|
};
|
100
src/shared/settings/values.js
Normal file
100
src/shared/settings/values.js
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
import DefaultSettings from './default';
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let blacklist = form.blacklist;
|
||||||
|
|
||||||
|
return { keymaps, search, blacklist };
|
||||||
|
};
|
||||||
|
|
||||||
|
const jsonFromValue = (value) => {
|
||||||
|
return JSON.stringify(value, undefined, 2);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formFromValue = (value) => {
|
||||||
|
|
||||||
|
let keymaps = undefined;
|
||||||
|
if (value.keymaps) {
|
||||||
|
let allowedOps = new Set(Object.keys(DefaultSettings.form.keymaps));
|
||||||
|
|
||||||
|
keymaps = {};
|
||||||
|
for (let keys of Object.keys(value.keymaps)) {
|
||||||
|
let op = operationToFormName(value.keymaps[keys]);
|
||||||
|
if (allowedOps.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 blacklist = value.blacklist;
|
||||||
|
|
||||||
|
return { keymaps, search, blacklist };
|
||||||
|
};
|
||||||
|
|
||||||
|
const jsonFromForm = (form) => {
|
||||||
|
return jsonFromValue(valueFromForm(form));
|
||||||
|
};
|
||||||
|
|
||||||
|
const formFromJson = (json) => {
|
||||||
|
let value = valueFromJson(json);
|
||||||
|
return formFromValue(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
valueFromJson, valueFromForm, jsonFromValue, formFromValue,
|
||||||
|
jsonFromForm, formFromJson
|
||||||
|
};
|
|
@ -1,18 +1,15 @@
|
||||||
import React from 'react';
|
import { h, Component } from 'preact';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
class Provider extends React.PureComponent {
|
class Provider extends Component {
|
||||||
getChildContext() {
|
getChildContext() {
|
||||||
return { store: this.props.store };
|
return { store: this.props.store };
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return React.Children.only(this.props.children);
|
return <div>
|
||||||
|
{ this.props.children }
|
||||||
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Provider.childContextTypes = {
|
|
||||||
store: PropTypes.any,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Provider;
|
export default Provider;
|
||||||
|
|
82
test/settings/components/form/blacklist-form.test.jsx
Normal file
82
test/settings/components/form/blacklist-form.test.jsx
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { h, render } from 'preact';
|
||||||
|
import BlacklistForm from 'settings/components/form/blacklist-form'
|
||||||
|
|
||||||
|
describe("settings/form/BlacklistForm", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('render', () => {
|
||||||
|
it('renders BlacklistForm', () => {
|
||||||
|
render(<BlacklistForm value={['*.slack.com', 'www.google.com/maps']} />, document.body);
|
||||||
|
|
||||||
|
let inputs = document.querySelectorAll('input[type=text]');
|
||||||
|
expect(inputs).to.have.lengthOf(2);
|
||||||
|
expect(inputs[0].value).to.equal('*.slack.com');
|
||||||
|
expect(inputs[1].value).to.equal('www.google.com/maps');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders blank value', () => {
|
||||||
|
render(<BlacklistForm />, document.body);
|
||||||
|
|
||||||
|
let inputs = document.querySelectorAll('input[type=text]');
|
||||||
|
expect(inputs).to.be.empty;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders blank value', () => {
|
||||||
|
render(<BlacklistForm />, document.body);
|
||||||
|
|
||||||
|
let inputs = document.querySelectorAll('input[type=text]');
|
||||||
|
expect(inputs).to.be.empty;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onChange', () => {
|
||||||
|
it('invokes onChange event on edit', (done) => {
|
||||||
|
render(<BlacklistForm
|
||||||
|
value={['*.slack.com', 'www.google.com/maps*']}
|
||||||
|
onChange={value => {
|
||||||
|
expect(value).to.have.lengthOf(2)
|
||||||
|
.and.have.members(['gitter.im', 'www.google.com/maps*']);
|
||||||
|
|
||||||
|
done();
|
||||||
|
}}
|
||||||
|
/>, document.body);
|
||||||
|
|
||||||
|
let input = document.querySelectorAll('input[type=text]')[0];
|
||||||
|
input.value = 'gitter.im';
|
||||||
|
input.dispatchEvent(new Event('change'))
|
||||||
|
});
|
||||||
|
|
||||||
|
it('invokes onChange event on delete', (done) => {
|
||||||
|
render(<BlacklistForm
|
||||||
|
value={['*.slack.com', 'www.google.com/maps*']}
|
||||||
|
onChange={value => {
|
||||||
|
expect(value).to.have.lengthOf(1)
|
||||||
|
.and.have.members(['www.google.com/maps*']);
|
||||||
|
|
||||||
|
done();
|
||||||
|
}}
|
||||||
|
/>, document.body);
|
||||||
|
|
||||||
|
let button = document.querySelectorAll('input[type=button]')[0];
|
||||||
|
button.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('invokes onChange event on add', (done) => {
|
||||||
|
render(<BlacklistForm
|
||||||
|
value={['*.slack.com']}
|
||||||
|
onChange={value => {
|
||||||
|
expect(value).to.have.lengthOf(2)
|
||||||
|
.and.have.members(['*.slack.com', '']);
|
||||||
|
|
||||||
|
done();
|
||||||
|
}}
|
||||||
|
/>, document.body);
|
||||||
|
|
||||||
|
let button = document.querySelector('input[type=button].ui-add-button');
|
||||||
|
button.click();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
53
test/settings/components/form/keymaps-form.test.jsx
Normal file
53
test/settings/components/form/keymaps-form.test.jsx
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { h, render } from 'preact';
|
||||||
|
import KeymapsForm from 'settings/components/form/keymaps-form'
|
||||||
|
|
||||||
|
describe("settings/form/KeymapsForm", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('render', () => {
|
||||||
|
it('renders KeymapsForm', () => {
|
||||||
|
render(<KeymapsForm value={{
|
||||||
|
'scroll.vertically?{"count":1}': 'j',
|
||||||
|
'scroll.vertically?{"count":-1}': 'k',
|
||||||
|
}} />, document.body);
|
||||||
|
|
||||||
|
let inputj = document.getElementById('scroll.vertically?{"count":1}');
|
||||||
|
let inputk = document.getElementById('scroll.vertically?{"count":-1}');
|
||||||
|
|
||||||
|
expect(inputj.value).to.equal('j');
|
||||||
|
expect(inputk.value).to.equal('k');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders blank value', () => {
|
||||||
|
render(<KeymapsForm />, document.body);
|
||||||
|
|
||||||
|
let inputj = document.getElementById('scroll.vertically?{"count":1}');
|
||||||
|
let inputk = document.getElementById('scroll.vertically?{"count":-1}');
|
||||||
|
|
||||||
|
expect(inputj.value).to.be.empty;
|
||||||
|
expect(inputk.value).to.be.empty;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onChange event', () => {
|
||||||
|
it('invokes onChange event on edit', (done) => {
|
||||||
|
render(<KeymapsForm
|
||||||
|
value={{
|
||||||
|
'scroll.vertically?{"count":1}': 'j',
|
||||||
|
'scroll.vertically?{"count":-1}': 'k',
|
||||||
|
}}
|
||||||
|
onChange={value => {
|
||||||
|
expect(value['scroll.vertically?{"count":1}']).to.equal('jjj');
|
||||||
|
|
||||||
|
done();
|
||||||
|
}} />, document.body);
|
||||||
|
|
||||||
|
let input = document.getElementById('scroll.vertically?{"count":1}');
|
||||||
|
input.value = 'jjj';
|
||||||
|
input.dispatchEvent(new Event('change'))
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
104
test/settings/components/form/search-engine-form.test.jsx
Normal file
104
test/settings/components/form/search-engine-form.test.jsx
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { h, render } from 'preact';
|
||||||
|
import SearchForm from 'settings/components/form/search-form'
|
||||||
|
|
||||||
|
describe("settings/form/SearchForm", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('render', () => {
|
||||||
|
it('renders SearchForm', () => {
|
||||||
|
render(<SearchForm value={{
|
||||||
|
default: 'google',
|
||||||
|
engines: [['google', 'google.com'], ['yahoo', 'yahoo.com']],
|
||||||
|
}} />, document.body);
|
||||||
|
|
||||||
|
let names = document.querySelectorAll('input[name=name]');
|
||||||
|
expect(names).to.have.lengthOf(2);
|
||||||
|
expect(names[0].value).to.equal('google');
|
||||||
|
expect(names[1].value).to.equal('yahoo');
|
||||||
|
|
||||||
|
let urls = document.querySelectorAll('input[name=url]');
|
||||||
|
expect(urls).to.have.lengthOf(2);
|
||||||
|
expect(urls[0].value).to.equal('google.com');
|
||||||
|
expect(urls[1].value).to.equal('yahoo.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders blank value', () => {
|
||||||
|
render(<SearchForm />, document.body);
|
||||||
|
|
||||||
|
let names = document.querySelectorAll('input[name=name]');
|
||||||
|
let urls = document.querySelectorAll('input[name=url]');
|
||||||
|
expect(names).to.have.lengthOf(0);
|
||||||
|
expect(urls).to.have.lengthOf(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders blank engines', () => {
|
||||||
|
render(<SearchForm value={{ default: 'google' }} />, document.body);
|
||||||
|
|
||||||
|
let names = document.querySelectorAll('input[name=name]');
|
||||||
|
let urls = document.querySelectorAll('input[name=url]');
|
||||||
|
expect(names).to.have.lengthOf(0);
|
||||||
|
expect(urls).to.have.lengthOf(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onChange event', () => {
|
||||||
|
it('invokes onChange event on edit', (done) => {
|
||||||
|
render(<SearchForm
|
||||||
|
value={{
|
||||||
|
default: 'google',
|
||||||
|
engines: [['google', 'google.com'], ['yahoo', 'yahoo.com']]
|
||||||
|
}}
|
||||||
|
onChange={value => {
|
||||||
|
expect(value.default).to.equal('louvre');
|
||||||
|
expect(value.engines).to.have.lengthOf(2)
|
||||||
|
.and.have.deep.members([['louvre', 'google.com'], ['yahoo', 'yahoo.com']])
|
||||||
|
|
||||||
|
done();
|
||||||
|
}} />, document.body);
|
||||||
|
|
||||||
|
let radio = document.querySelectorAll('input[type=radio]');
|
||||||
|
radio.checked = true;
|
||||||
|
|
||||||
|
let name = document.querySelector('input[name=name]');
|
||||||
|
name.value = 'louvre';
|
||||||
|
name.dispatchEvent(new Event('change'))
|
||||||
|
});
|
||||||
|
|
||||||
|
it('invokes onChange event on delete', (done) => {
|
||||||
|
render(<SearchForm value={{
|
||||||
|
default: 'yahoo',
|
||||||
|
engines: [['louvre', 'google.com'], ['yahoo', 'yahoo.com']]
|
||||||
|
}}
|
||||||
|
onChange={value => {
|
||||||
|
expect(value.default).to.equal('yahoo');
|
||||||
|
expect(value.engines).to.have.lengthOf(1)
|
||||||
|
.and.have.deep.members([['yahoo', 'yahoo.com']])
|
||||||
|
|
||||||
|
done();
|
||||||
|
}} />, document.body);
|
||||||
|
|
||||||
|
let button = document.querySelector('input[type=button]');
|
||||||
|
button.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('invokes onChange event on add', (done) => {
|
||||||
|
render(<SearchForm value={{
|
||||||
|
default: 'yahoo',
|
||||||
|
engines: [['google', 'google.com']]
|
||||||
|
}}
|
||||||
|
onChange={value => {
|
||||||
|
expect(value.default).to.equal('yahoo');
|
||||||
|
expect(value.engines).to.have.lengthOf(2)
|
||||||
|
.and.have.deep.members([['google', 'google.com'], ['', '']])
|
||||||
|
|
||||||
|
done();
|
||||||
|
}} />, document.body);
|
||||||
|
|
||||||
|
let button = document.querySelector('input[type=button].ui-add-button');
|
||||||
|
button.click();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
83
test/settings/components/ui/input.test.jsx
Normal file
83
test/settings/components/ui/input.test.jsx
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { h, render } from 'preact';
|
||||||
|
import Input from 'settings/components/ui/input'
|
||||||
|
|
||||||
|
describe("settings/ui/Input", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
context("type=text", () => {
|
||||||
|
it('renders text input', () => {
|
||||||
|
render(<Input type='text' name='myname' label='myfield' value='myvalue'/>, document.body)
|
||||||
|
|
||||||
|
let label = document.querySelector('label');
|
||||||
|
let input = document.querySelector('input');
|
||||||
|
expect(label.textContent).to.contain('myfield');
|
||||||
|
expect(input.type).to.contain('text');
|
||||||
|
expect(input.name).to.contain('myname');
|
||||||
|
expect(input.value).to.contain('myvalue');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('invoke onChange', (done) => {
|
||||||
|
render(<Input type='text' name='myname' label='myfield' value='myvalue' onChange={(e) => {
|
||||||
|
expect(e.target.value).to.equal('newvalue');
|
||||||
|
done();
|
||||||
|
}}/>, document.body);
|
||||||
|
|
||||||
|
let input = document.querySelector('input');
|
||||||
|
input.value = 'newvalue';
|
||||||
|
input.dispatchEvent(new Event('change'))
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context("type=radio", () => {
|
||||||
|
it('renders radio button', () => {
|
||||||
|
render(<Input type='radio' name='myname' label='myfield' value='myvalue'/>, document.body)
|
||||||
|
|
||||||
|
let label = document.querySelector('label');
|
||||||
|
let input = document.querySelector('input');
|
||||||
|
expect(label.textContent).to.contain('myfield');
|
||||||
|
expect(input.type).to.contain('radio');
|
||||||
|
expect(input.name).to.contain('myname');
|
||||||
|
expect(input.value).to.contain('myvalue');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('invoke onChange', (done) => {
|
||||||
|
render(<Input type='text' name='radio' label='myfield' value='myvalue' onChange={(e) => {
|
||||||
|
expect(e.target.checked).to.be.true;
|
||||||
|
done();
|
||||||
|
}}/>, document.body);
|
||||||
|
|
||||||
|
let input = document.querySelector('input');
|
||||||
|
input.checked = true;
|
||||||
|
input.dispatchEvent(new Event('change'))
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context("type=textarea", () => {
|
||||||
|
it('renders textarea button', () => {
|
||||||
|
render(<Input type='textarea' name='myname' label='myfield' value='myvalue' error='myerror' />, document.body)
|
||||||
|
|
||||||
|
let label = document.querySelector('label');
|
||||||
|
let textarea = document.querySelector('textarea');
|
||||||
|
let error = document.querySelector('.settings-ui-input-error');
|
||||||
|
expect(label.textContent).to.contain('myfield');
|
||||||
|
expect(textarea.nodeName).to.contain('TEXTAREA');
|
||||||
|
expect(textarea.name).to.contain('myname');
|
||||||
|
expect(textarea.value).to.contain('myvalue');
|
||||||
|
expect(error.textContent).to.contain('myerror');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('invoke onChange', (done) => {
|
||||||
|
render(<Input type='textarea' name='myname' label='myfield' value='myvalue' onChange={(e) => {
|
||||||
|
expect(e.target.value).to.equal('newvalue');
|
||||||
|
done();
|
||||||
|
}}/>, document.body);
|
||||||
|
|
||||||
|
let input = document.querySelector('textarea');
|
||||||
|
input.value = 'newvalue'
|
||||||
|
input.dispatchEvent(new Event('change'))
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
112
test/shared/settings/values.test.js
Normal file
112
test/shared/settings/values.test.js
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import * as values from 'shared/settings/values';
|
||||||
|
|
||||||
|
describe("settings values", () => {
|
||||||
|
describe('valueFromJson', () => {
|
||||||
|
it('return object from json string', () => {
|
||||||
|
let json = `{
|
||||||
|
"keymaps": { "0": {"type": "scroll.home"}},
|
||||||
|
"search": { "default": "google", "engines": { "google": "https://google.com/search?q={}" }},
|
||||||
|
"blacklist": [ "*.slack.com"]
|
||||||
|
}`;
|
||||||
|
let value = values.valueFromJson(json);
|
||||||
|
|
||||||
|
expect(value.keymaps).to.deep.equal({ 0: {type: "scroll.home"}});
|
||||||
|
expect(value.search).to.deep.equal({ default: "google", engines: { google: "https://google.com/search?q={}"} });
|
||||||
|
expect(value.blacklist).to.deep.equal(["*.slack.com"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('valueFromForm', () => {
|
||||||
|
it('returns value from form', () => {
|
||||||
|
let form = {
|
||||||
|
keymaps: {
|
||||||
|
'scroll.vertically?{"count":1}': 'j',
|
||||||
|
'scroll.home': '0',
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
default: 'google',
|
||||||
|
engines: [['google', 'https://google.com/search?q={}']],
|
||||||
|
},
|
||||||
|
blacklist: ['*.slack.com'],
|
||||||
|
};
|
||||||
|
let value = values.valueFromForm(form);
|
||||||
|
|
||||||
|
expect(value.keymaps).to.have.deep.property('j', { type: "scroll.vertically", count: 1 });
|
||||||
|
expect(value.keymaps).to.have.deep.property('0', { type: "scroll.home" });
|
||||||
|
expect(JSON.stringify(value.search)).to.deep.equal(JSON.stringify({ default: "google", engines: { google: "https://google.com/search?q={}"} }));
|
||||||
|
expect(value.search).to.deep.equal({ default: "google", engines: { google: "https://google.com/search?q={}"} });
|
||||||
|
expect(value.blacklist).to.deep.equal(["*.slack.com"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('convert from empty form', () => {
|
||||||
|
let form = {};
|
||||||
|
let value = values.valueFromForm(form);
|
||||||
|
expect(value).to.not.have.key('keymaps');
|
||||||
|
expect(value).to.not.have.key('search');
|
||||||
|
expect(value).to.not.have.key('blacklist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('override keymaps', () => {
|
||||||
|
let form = {
|
||||||
|
keymaps: {
|
||||||
|
'scroll.vertically?{"count":1}': 'j',
|
||||||
|
'scroll.vertically?{"count":-1}': 'j',
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let value = values.valueFromForm(form);
|
||||||
|
|
||||||
|
expect(value.keymaps).to.have.key('j');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('override search engine', () => {
|
||||||
|
let form = {
|
||||||
|
search: {
|
||||||
|
default: 'google',
|
||||||
|
engines: [
|
||||||
|
['google', 'https://google.com/search?q={}'],
|
||||||
|
['google', 'https://google.co.jp/search?q={}'],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let value = values.valueFromForm(form);
|
||||||
|
|
||||||
|
expect(value.search.engines).to.have.property('google', 'https://google.co.jp/search?q={}');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('jsonFromValue', () => {
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formFromValue', () => {
|
||||||
|
it('convert empty value to form', () => {
|
||||||
|
let value = {};
|
||||||
|
let form = values.formFromValue(value);
|
||||||
|
|
||||||
|
expect(value).to.not.have.key('keymaps');
|
||||||
|
expect(value).to.not.have.key('search');
|
||||||
|
expect(value).to.not.have.key('blacklist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('convert value to form', () => {
|
||||||
|
let value = {
|
||||||
|
keymaps: {
|
||||||
|
j: { type: 'scroll.vertically', count: 1 },
|
||||||
|
JJ: { type: 'scroll.vertically', count: 100 },
|
||||||
|
0: { type: 'scroll.home' },
|
||||||
|
},
|
||||||
|
search: { default: 'google', engines: { google: 'https://google.com/search?q={}' }},
|
||||||
|
blacklist: [ '*.slack.com']
|
||||||
|
};
|
||||||
|
let form = values.formFromValue(value);
|
||||||
|
|
||||||
|
expect(form.keymaps).to.have.property('scroll.vertically?{"count":1}', 'j');
|
||||||
|
expect(form.keymaps).to.have.property('scroll.home', '0');
|
||||||
|
expect(Object.keys(form.keymaps)).to.have.lengthOf(2);
|
||||||
|
expect(form.search).to.have.property('default', 'google');
|
||||||
|
expect(form.search).to.have.deep.property('engines', [['google', 'https://google.com/search?q={}']]);
|
||||||
|
expect(form.blacklist).to.have.lengthOf(1);
|
||||||
|
expect(form.blacklist).to.include('*.slack.com');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -25,7 +25,7 @@ config = {
|
||||||
exclude: /node_modules/,
|
exclude: /node_modules/,
|
||||||
loader: 'babel-loader',
|
loader: 'babel-loader',
|
||||||
query: {
|
query: {
|
||||||
presets: ['es2015', 'react']
|
presets: ['es2015', 'preact']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
Reference in a new issue