Add keybindings form

jh-changes
Shin'ya Ueoka 7 years ago
parent d33b37cdb9
commit 4e5ddc1d57
  1. 7
      src/settings/actions/setting.js
  2. 139
      src/settings/components/index.jsx
  3. 27
      src/settings/components/site.scss
  4. 35
      src/settings/components/ui/input.jsx
  5. 37
      src/settings/components/ui/input.scss
  6. 2
      src/settings/reducers/setting.js
  7. 66
      src/shared/default-settings.js

@ -4,10 +4,10 @@ import DefaultSettings from 'shared/default-settings';
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);
};
@ -28,6 +28,7 @@ const set = (settings) => {
type: actions.SETTING_SET_SETTINGS,
source: settings.source,
json: settings.json,
form: settings.form,
value: JSON.parse(settings.json),
};
};

@ -4,6 +4,59 @@ import Input from './ui/input';
import * as settingActions from 'settings/actions/setting';
import * as validator from 'shared/validators/setting';
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.histories.prev', 'Go back in histories'],
['navigate.histories.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 SettingsComponent extends Component {
constructor(props, context) {
super(props, context);
@ -29,33 +82,87 @@ class SettingsComponent extends Component {
settings: {
source: settings.source,
json: settings.json,
form: settings.form,
}
});
}
renderFormFields() {
let keymapFields = KeyMapFields.map((group, index) => {
return <div key={index} className='keymap-fields-group'>
{
group.map((field) => {
let name = field[0];
let label = field[1];
let value = this.state.settings.form.keymaps[name];
return <Input
type='text'
id={name}
name={name}
key={name}
label={label}
value={value}
onChange={this.bindFormKeymapsValue.bind(this)}
/>;
})
}
</div>;
});
return <div>
<fieldset>
<legend>Keybindings</legend>
<div className='keymap-fields'>
{ keymapFields }
</div>
</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)}
onBlur={this.save.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.bindValue.bind(this)} />
<Input
type='radio'
name='source'
label='Use plain JSON'
checked={this.state.settings.source === 'json'}
value='json' />
value='json'
onChange={this.bindValue.bind(this)} />
<Input
type='textarea'
name='json'
label='Plane JSON'
spellCheck='false'
error={this.state.errors.json}
onChange={this.bindValue.bind(this)}
onBlur={this.bindAndSave.bind(this)}
value={this.state.settings.json}
/>
{ fields }
</form>
</div>
);
@ -82,9 +189,15 @@ class SettingsComponent extends Component {
this.setState(next);
}
bindAndSave(e) {
this.bindValue(e);
bindFormKeymapsValue(e) {
let next = Object.assign({}, this.state);
next.settings.form.keymaps[e.target.name] = e.target.value;
this.context.store.dispatch(settingActions.save(next.settings));
}
save() {
try {
let json = this.state.settings.json;
validator.validate(JSON.parse(json));

@ -7,4 +7,31 @@
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;
line-height: 1.5rem;
font-weight: bold;
}
}
.keymap-fields{
column-count: 2;
.keymap-fields-group {
margin-top: 24px;
}
.keymap-fields-group:first-of-type {
margin-top: 0;
}
}
}

@ -3,32 +3,35 @@ import './input.scss';
class Input extends Component {
renderText(props) {
let inputClassName = props.error ? 'input-error' : '';
return <div className='settings-ui-input'>
<label
type='text'
htmlFor={props.id}
>{ props.label }</label>
<input type='text' className={inputClassName} {...props} />
</div>;
}
renderRadio(props) {
let inputClasses = 'form-field-input';
if (props.error) {
inputClasses += ' input-error';
}
return <div>
let inputClassName = props.error ? 'input-error' : '';
return <div className='settings-ui-input'>
<label>
<input className={ inputClasses } type='radio' {...props} />
<input type='radio' className={inputClassName} {...props} />
{ props.label }
</label>
</div>;
}
renderTextArea(props) {
let inputClasses = 'form-field-input';
if (props.error) {
inputClasses += ' input-error';
}
return <div>
let inputClassName = props.error ? 'input-error' : '';
return <div className='settings-ui-input'>
<label
className='form-field-label'
htmlFor={props.id}
>{ props.label }</label>
<textarea className={inputClasses} {...props} />
<p className='form-field-error'>{ this.props.error }</p>
<textarea className={inputClassName} {...props} />
<p className='settings-ui-input-error'>{ this.props.error }</p>
</div>;
}
@ -36,6 +39,8 @@ class Input extends Component {
let { type } = this.props;
switch (this.props.type) {
case 'text':
return this.renderText(this.props);
case 'radio':
return this.renderRadio(this.props);
case 'textarea':

@ -1,17 +1,28 @@
.form-field-label {
font-weight: bold
}
.settings-ui-input {
page-break-inside: avoid;
.form-field-error {
font-weight: bold;
color: red;
min-height: 1.5em;
}
* {
page-break-inside: avoid;
}
.form-field-input {
padding: 4px;
}
label {
font-weight: bold;
min-width: 14rem;
display: inline-block;
}
input {
padding: 4px;
}
input.input-crror,
textarea.input-error {
box-shadow: 0 0 2px red;
}
.form-field-input.input-error {
box-shadow: 0 0 2px red;
&-error {
font-weight: bold;
color: red;
min-height: 1.5em;
}
}

@ -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,69 @@ 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.histories.prev': 'H',
'navigate.histories.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={}'
}
}
}
};