commit
0b37c2250e
30 changed files with 1190 additions and 147 deletions
|
@ -36,6 +36,7 @@
|
|||
"newline-after-var": "off",
|
||||
"newline-before-return": "off",
|
||||
"newline-per-chained-call": "off",
|
||||
"no-alert": "off",
|
||||
"no-bitwise": "off",
|
||||
"no-console": ["error", { "allow": ["warn", "error"] }],
|
||||
"no-empty-function": "off",
|
||||
|
@ -44,6 +45,8 @@
|
|||
"no-plusplus": "off",
|
||||
"no-ternary": "off",
|
||||
"no-undefined": "off",
|
||||
"no-undef-init": "off",
|
||||
"no-unused-vars": ["error", { "varsIgnorePattern": "h" }],
|
||||
"no-use-before-define": "off",
|
||||
"no-warning-comments": "off",
|
||||
"object-curly-newline": ["error", { "consistent": true }],
|
||||
|
@ -65,5 +68,6 @@
|
|||
|
||||
"react/jsx-indent": ["error", 2],
|
||||
"react/prop-types": "off",
|
||||
"react/react-in-jsx-scope": "off"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,11 +7,13 @@ module.exports = function (config) {
|
|||
frameworks: ['mocha'],
|
||||
files: [
|
||||
'test/**/*.test.js',
|
||||
'test/**/*.test.jsx',
|
||||
'test/**/*.html'
|
||||
],
|
||||
|
||||
preprocessors: {
|
||||
'test/**/*.test.js': [ 'webpack' ],
|
||||
'test/**/*.test.jsx': [ 'webpack' ],
|
||||
'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-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": {
|
||||
"version": "6.18.0",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"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",
|
||||
|
@ -1024,15 +1008,6 @@
|
|||
"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": {
|
||||
"version": "6.24.1",
|
||||
"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-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": {
|
||||
"version": "6.24.1",
|
||||
"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-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": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-preset-minify/-/babel-preset-minify-0.2.0.tgz",
|
||||
|
@ -1205,18 +1151,14 @@
|
|||
"lodash.isplainobject": "4.0.6"
|
||||
}
|
||||
},
|
||||
"babel-preset-react": {
|
||||
"version": "6.24.1",
|
||||
"resolved": "https://registry.npmjs.org/babel-preset-react/-/babel-preset-react-6.24.1.tgz",
|
||||
"integrity": "sha1-umnfrqRfw+xjm2pOzqbhdwLJE4A=",
|
||||
"babel-preset-preact": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-preset-preact/-/babel-preset-preact-1.1.0.tgz",
|
||||
"integrity": "sha1-NaxlWnOkm4Q4FjzgU4Fld+GYCGE=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"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-self": "6.22.0",
|
||||
"babel-plugin-transform-react-jsx-source": "6.22.0",
|
||||
"babel-preset-flow": "6.23.0"
|
||||
"babel-plugin-transform-react-jsx": "6.24.1"
|
||||
}
|
||||
},
|
||||
"babel-register": {
|
||||
|
@ -6273,6 +6215,12 @@
|
|||
"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": {
|
||||
"version": "1.1.2",
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz",
|
||||
|
|
|
@ -3,10 +3,9 @@
|
|||
"description": "Vim vixen",
|
||||
"scripts": {
|
||||
"start": "webpack -w --debug --devtool inline-source-map",
|
||||
"lint": "eslint --ext .jsx,.js src",
|
||||
"build": "NODE_ENV=production webpack --progress --display-error-details",
|
||||
"package": "npm run build && ./package.sh",
|
||||
"lint": "eslint src",
|
||||
"lint": "eslint --ext .jsx,.js src",
|
||||
"test": "karma start"
|
||||
},
|
||||
"repository": {
|
||||
|
@ -24,9 +23,8 @@
|
|||
"babel-eslint": "^7.2.3",
|
||||
"babel-loader": "^7.1.1",
|
||||
"babel-minify-webpack-plugin": "^0.2.0",
|
||||
"babel-plugin-transform-react-jsx": "^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",
|
||||
"css-loader": "^0.28.4",
|
||||
"eslint": "^4.7.0",
|
||||
|
@ -41,8 +39,7 @@
|
|||
"karma-webpack": "^2.0.4",
|
||||
"mocha": "^3.5.0",
|
||||
"node-sass": "^4.5.3",
|
||||
"react": "^16.0.0",
|
||||
"react-dom": "^16.0.0",
|
||||
"preact": "^8.2.6",
|
||||
"sass-loader": "^6.0.6",
|
||||
"style-loader": "^0.18.2",
|
||||
"webpack": "^3.5.3"
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import actions from 'settings/actions';
|
||||
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 = () => {
|
||||
return browser.storage.local.get('settings').then(({ settings }) => {
|
||||
if (settings) {
|
||||
return set(settings);
|
||||
if (!settings) {
|
||||
return set(DefaultSettings);
|
||||
}
|
||||
return set(DefaultSettings);
|
||||
return set(Object.assign({}, DefaultSettings, settings));
|
||||
}, console.error);
|
||||
};
|
||||
|
||||
|
@ -24,11 +25,19 @@ const save = (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;
|
||||
}
|
||||
}
|
||||
}
|
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 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 * as settingActions from 'settings/actions/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) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
settings: {
|
||||
json: '',
|
||||
},
|
||||
errors: {
|
||||
json: '',
|
||||
}
|
||||
};
|
||||
this.context.store.subscribe(this.stateChanged.bind(this));
|
||||
|
@ -26,66 +37,140 @@ 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>
|
||||
</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);
|
||||
}
|
||||
}
|
||||
|
||||
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 nextSettings = Object.assign({}, this.state.settings);
|
||||
nextSettings[e.target.name] = e.target.value;
|
||||
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;
|
||||
|
||||
this.setState(next);
|
||||
this.context.store.dispatch(settingActions.save(next.settings));
|
||||
}
|
||||
|
||||
bindAndSave(e) {
|
||||
this.bindValue(e);
|
||||
bindSource(e) {
|
||||
let from = this.state.settings.source;
|
||||
let to = e.target.value;
|
||||
|
||||
try {
|
||||
let json = this.state.settings.json;
|
||||
validator.validate(JSON.parse(json));
|
||||
this.context.store.dispatch(settingActions.save(this.state.settings));
|
||||
} catch (err) {
|
||||
// error already shown
|
||||
let next = Object.assign({}, this.state);
|
||||
if (from === 'form' && to === 'json') {
|
||||
next.settings.json =
|
||||
settingsValues.jsonFromForm(this.state.settings.form);
|
||||
} else if (from === 'json' && to === 'form') {
|
||||
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;
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -62,5 +62,70 @@ export default {
|
|||
"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 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;
|
||||
|
|
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/,
|
||||
loader: 'babel-loader',
|
||||
query: {
|
||||
presets: ['es2015', 'react']
|
||||
presets: ['es2015', 'preact']
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
Reference in a new issue