diff options
Diffstat (limited to 'src')
22 files changed, 716 insertions, 55 deletions
diff --git a/src/background/tabs.js b/src/background/tabs.js index d641616..b34f7c2 100644 --- a/src/background/tabs.js +++ b/src/background/tabs.js @@ -51,7 +51,7 @@ const selectByKeyword = (current, keyword) => { const getCompletions = (keyword) => { return browser.tabs.query({ currentWindow: true }).then((tabs) => { let matched = tabs.filter((t) => { - return t.url.includes(keyword) || t.title.includes(keyword); + return t.url.includes(keyword) || t.title && t.title.includes(keyword); }); return matched; }); diff --git a/src/settings/actions/setting.js b/src/settings/actions/setting.js index c1b27c8..1d01fda 100644 --- a/src/settings/actions/setting.js +++ b/src/settings/actions/setting.js @@ -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, }; }; diff --git a/src/settings/components/form/blacklist-form.jsx b/src/settings/components/form/blacklist-form.jsx new file mode 100644 index 0000000..7ae9652 --- /dev/null +++ b/src/settings/components/form/blacklist-form.jsx @@ -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; diff --git a/src/settings/components/form/blacklist-form.scss b/src/settings/components/form/blacklist-form.scss new file mode 100644 index 0000000..a230d0d --- /dev/null +++ b/src/settings/components/form/blacklist-form.scss @@ -0,0 +1,9 @@ +.form-blacklist-form { + &-row { + display: flex; + + .column-url { + flex: 1; + } + } +} diff --git a/src/settings/components/form/keymaps-form.jsx b/src/settings/components/form/keymaps-form.jsx new file mode 100644 index 0000000..f3b6abe --- /dev/null +++ b/src/settings/components/form/keymaps-form.jsx @@ -0,0 +1,106 @@ +import './keymaps-form.scss'; +import { h, Component } from 'preact'; +import Input from '../ui/input'; + +const KeyMapFields = [ + [ + ['scroll.vertically?{"count":1}', 'Scroll down'], + ['scroll.vertically?{"count":-1}', 'Scroll up'], + ['scroll.horizonally?{"count":-1}', 'Scroll left'], + ['scroll.horizonally?{"count":1}', 'Scroll right'], + ['scroll.home', 'Scroll to leftmost'], + ['scroll.end', 'Scroll to rightmost'], + ['scroll.top', 'Scroll to top'], + ['scroll.bottom', 'Scroll to bottom'], + ['scroll.pages?{"count":-0.5}', 'Scroll up by half of screen'], + ['scroll.pages?{"count":0.5}', 'Scroll 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":false}', 'Reload current tab'], + ['tabs.reload?{"cache":true}', 'Reload with no caches'], + ['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'], + ] +]; + +const AllowdOps = [].concat(...KeyMapFields.map(group => group.map(e => e[0]))); + +class KeymapsForm extends Component { + + render() { + let values = this.props.value; + if (!values) { + values = {}; + } + return <div className='form-keymaps-form'> + { + KeyMapFields.map((group, index) => { + return <div key={index} className='form-keymaps-form-field-group'> + { + group.map((field) => { + let name = field[0]; + let label = field[1]; + let value = values[name]; + return <Input + type='text' id={name} name={name} key={name} + label={label} value={value} + onChange={this.bindValue.bind(this)} + />; + }) + } + </div>; + }) + } + </div>; + } + + bindValue(e) { + if (!this.props.onChange) { + return; + } + + let next = Object.assign({}, this.props.value); + next[e.target.name] = e.target.value; + + this.props.onChange(next); + } +} + +KeymapsForm.AllowdOps = AllowdOps; + +export default KeymapsForm; diff --git a/src/settings/components/form/keymaps-form.scss b/src/settings/components/form/keymaps-form.scss new file mode 100644 index 0000000..1a4e5cd --- /dev/null +++ b/src/settings/components/form/keymaps-form.scss @@ -0,0 +1,11 @@ +.form-keymaps-form { + column-count: 3; + + &-field-group { + margin-top: 24px; + } + + &-field-group:first-of-type { + margin-top: 24px; + } +} diff --git a/src/settings/components/form/search-form.jsx b/src/settings/components/form/search-form.jsx new file mode 100644 index 0000000..e85761f --- /dev/null +++ b/src/settings/components/form/search-form.jsx @@ -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; diff --git a/src/settings/components/form/search-form.scss b/src/settings/components/form/search-form.scss new file mode 100644 index 0000000..26b2f44 --- /dev/null +++ b/src/settings/components/form/search-form.scss @@ -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; + } +} diff --git a/src/settings/components/index.jsx b/src/settings/components/index.jsx index 4418942..73520ca 100644 --- a/src/settings/components/index.jsx +++ b/src/settings/components/index.jsx @@ -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,179 @@ 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'> + <Input + type='radio' + id='setting-source-form' + name='source' + label='Use form' + checked={this.state.settings.source === 'form'} + value='form' + onChange={this.bindSource.bind(this)} /> - <p>Load settings from:</p> - <input type='radio' id='setting-source-json' + <Input + type='radio' name='source' + label='Use plain JSON' + checked={this.state.settings.source === 'json'} value='json' - onChange={this.bindAndSave.bind(this)} - checked={this.state.settings.source === 'json'} /> - <label htmlFor='settings-source-json'>JSON</label> - - <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} /> + 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(''); + } + } + + validateValue(e) { + let next = Object.assign({}, this.state); + + next.errors.json = ''; + try { + this.validate(e.target); } catch (err) { - e.target.setCustomValidity(err.message); + next.errors.json = err.message; } + next.settings[e.target.name] = e.target.value; + } + + 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); + let error = false; - this.setState({ settings: nextSettings }); - } + next.errors.json = ''; + try { + this.validate(e.target); + } catch (err) { + next.errors.json = err.message; + error = true; + } + next.settings[e.target.name] = e.target.value; - bindAndSave(e) { - this.bindValue(e); + this.setState(this.state); + if (!error) { + this.context.store.dispatch(settingActions.save(next.settings)); + } + } + migrateToForm() { + let b = window.confirm(DO_YOU_WANT_TO_CONTINUE); + if (!b) { + this.setState(this.state); + return; + } try { - let json = this.state.settings.json; - validator.validate(JSON.parse(json)); - this.context.store.dispatch(settingActions.save(this.state.settings)); + validator.validate(JSON.parse(this.state.settings.json)); } catch (err) { - // error already shown + this.setState(this.state); + return; } + + let form = settingsValues.formFromJson( + this.state.settings.json, KeymapsForm.AllowdOps); + let next = Object.assign({}, this.state); + next.settings.form = form; + next.settings.source = 'form'; + next.errors.json = ''; + + this.setState(next); + this.context.store.dispatch(settingActions.save(next.settings)); + } + + migrateToJson() { + let json = settingsValues.jsonFromForm(this.state.settings.form); + let next = Object.assign({}, this.state); + next.settings.json = json; + next.settings.source = 'json'; + next.errors.json = ''; + + this.setState(next); + this.context.store.dispatch(settingActions.save(next.settings)); } -} -SettingsComponent.contextTypes = { - store: PropTypes.any, -}; + bindSource(e) { + let from = this.state.settings.source; + let to = e.target.value; + + if (from === 'form' && to === 'json') { + this.migrateToJson(); + } else if (from === 'json' && to === 'form') { + this.migrateToForm(); + } + } +} export default SettingsComponent; diff --git a/src/settings/components/site.scss b/src/settings/components/site.scss index fae9c39..c0c4f9e 100644 --- a/src/settings/components/site.scss +++ b/src/settings/components/site.scss @@ -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; + } + } } diff --git a/src/settings/components/ui/add-button.jsx b/src/settings/components/ui/add-button.jsx new file mode 100644 index 0000000..79292d8 --- /dev/null +++ b/src/settings/components/ui/add-button.jsx @@ -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; diff --git a/src/settings/components/ui/add-button.scss b/src/settings/components/ui/add-button.scss new file mode 100644 index 0000000..beb5688 --- /dev/null +++ b/src/settings/components/ui/add-button.scss @@ -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; + } +} diff --git a/src/settings/components/ui/delete-button.jsx b/src/settings/components/ui/delete-button.jsx new file mode 100644 index 0000000..8077a76 --- /dev/null +++ b/src/settings/components/ui/delete-button.jsx @@ -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; diff --git a/src/settings/components/ui/delete-button.scss b/src/settings/components/ui/delete-button.scss new file mode 100644 index 0000000..5932a72 --- /dev/null +++ b/src/settings/components/ui/delete-button.scss @@ -0,0 +1,13 @@ + +.ui-delete-button { + border: none; + padding: 4; + display: inline; + background: none; + color: red; + cursor: pointer; + + &:hover { + color: darkred; + } +} diff --git a/src/settings/components/ui/input.jsx b/src/settings/components/ui/input.jsx new file mode 100644 index 0000000..e99dbc7 --- /dev/null +++ b/src/settings/components/ui/input.jsx @@ -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; diff --git a/src/settings/components/ui/input.scss b/src/settings/components/ui/input.scss new file mode 100644 index 0000000..ad4daf8 --- /dev/null +++ b/src/settings/components/ui/input.scss @@ -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; + } +} diff --git a/src/settings/index.jsx b/src/settings/index.jsx index 7516fb7..eb251b4 100644 --- a/src/settings/index.jsx +++ b/src/settings/index.jsx @@ -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>, diff --git a/src/settings/reducers/setting.js b/src/settings/reducers/setting.js index a61c09f..70c6183 100644 --- a/src/settings/reducers/setting.js +++ b/src/settings/reducers/setting.js @@ -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: diff --git a/src/shared/commands.js b/src/shared/commands.js index 8edeb5c..ed64a63 100644 --- a/src/shared/commands.js +++ b/src/shared/commands.js @@ -9,7 +9,7 @@ const normalizeUrl = (args, searchConfig) => { if (concat.includes('.') && !concat.includes(' ')) { return 'http://' + concat; } - let query = encodeURI(concat); + let query = concat; let template = searchConfig.engines[ searchConfig.default ]; @@ -19,7 +19,7 @@ const normalizeUrl = (args, searchConfig) => { template = searchConfig.engines[key]; } } - return template.replace('{}', query); + return template.replace('{}', encodeURIComponent(query)); } }; diff --git a/src/shared/default-settings.js b/src/shared/settings/default.js index 608890b..d187565 100644 --- a/src/shared/default-settings.js +++ b/src/shared/settings/default.js @@ -15,8 +15,6 @@ export default { "j": { "type": "scroll.vertically", "count": 1 }, "h": { "type": "scroll.horizonally", "count": -1 }, "l": { "type": "scroll.horizonally", "count": 1 }, - "<C-Y>": { "type": "scroll.vertically", "count": -1 }, - "<C-E>": { "type": "scroll.vertically", "count": 1 }, "<C-U>": { "type": "scroll.pages", "count": -0.5 }, "<C-D>": { "type": "scroll.pages", "count": 0.5 }, "<C-B>": { "type": "scroll.pages", "count": -1 }, @@ -62,5 +60,5 @@ export default { "wikipedia": "https://en.wikipedia.org/w/index.php?search={}" } } -}` +}`, }; diff --git a/src/shared/settings/values.js b/src/shared/settings/values.js new file mode 100644 index 0000000..4e55fa0 --- /dev/null +++ b/src/shared/settings/values.js @@ -0,0 +1,98 @@ +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, allowedOps) => { + let keymaps = undefined; + + if (value.keymaps) { + let allowedSet = new Set(allowedOps); + + keymaps = {}; + for (let keys of Object.keys(value.keymaps)) { + let op = operationToFormName(value.keymaps[keys]); + if (allowedSet.has(op)) { + keymaps[op] = keys; + } + } + } + + let search = undefined; + if (value.search) { + search = { default: value.search.default }; + if (value.search.engines) { + search.engines = Object.keys(value.search.engines).map((name) => { + return [name, value.search.engines[name]]; + }); + } + } + + let blacklist = value.blacklist; + + return { keymaps, search, blacklist }; +}; + +const jsonFromForm = (form) => { + return jsonFromValue(valueFromForm(form)); +}; + +const formFromJson = (json, allowedOps) => { + let value = valueFromJson(json); + return formFromValue(value, allowedOps); +}; + +export { + valueFromJson, valueFromForm, jsonFromValue, formFromValue, + jsonFromForm, formFromJson +}; diff --git a/src/shared/store/provider.jsx b/src/shared/store/provider.jsx index 743f656..fe925aa 100644 --- a/src/shared/store/provider.jsx +++ b/src/shared/store/provider.jsx @@ -1,18 +1,15 @@ -import React from 'react'; -import PropTypes from 'prop-types'; +import { h, Component } from 'preact'; -class Provider extends React.PureComponent { +class Provider extends Component { getChildContext() { return { store: this.props.store }; } render() { - return React.Children.only(this.props.children); + return <div> + { this.props.children } + </div>; } } -Provider.childContextTypes = { - store: PropTypes.any, -}; - export default Provider; |