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