Implemented basic support for key bindings

This commit is contained in:
jos 2017-06-11 17:14:46 +02:00
parent 82ff880c27
commit fb71b61ba5
3 changed files with 244 additions and 8 deletions

View File

@ -41,7 +41,12 @@ export default class JSONNode extends Component {
renderJSONObject ({prop, index, data, options, events}) { renderJSONObject ({prop, index, data, options, events}) {
const childCount = data.props.length const childCount = data.props.length
const node = h('div', {name: compileJSONPointer(this.props.path), key: 'node', className: 'jsoneditor-node jsoneditor-object'}, [ const node = h('div', {
name: compileJSONPointer(this.props.path),
onKeyDown: this.handleKeyDown,
key: 'node',
className: 'jsoneditor-node jsoneditor-object'
}, [
this.renderExpandButton(), this.renderExpandButton(),
this.renderActionMenuButton(), this.renderActionMenuButton(),
this.renderProperty(prop, index, data, options), this.renderProperty(prop, index, data, options),
@ -81,7 +86,12 @@ export default class JSONNode extends Component {
// TODO: extract a function renderChilds shared by both renderJSONObject and renderJSONArray (rename .props and .items to .childs?) // TODO: extract a function renderChilds shared by both renderJSONObject and renderJSONArray (rename .props and .items to .childs?)
renderJSONArray ({prop, index, data, options, events}) { renderJSONArray ({prop, index, data, options, events}) {
const childCount = data.items.length const childCount = data.items.length
const node = h('div', {name: compileJSONPointer(this.props.path), key: 'node', className: 'jsoneditor-node jsoneditor-array'}, [ const node = h('div', {
name: compileJSONPointer(this.props.path),
onKeyDown: this.handleKeyDown,
key: 'node',
className: 'jsoneditor-node jsoneditor-array'
}, [
this.renderExpandButton(), this.renderExpandButton(),
this.renderActionMenuButton(), this.renderActionMenuButton(),
this.renderProperty(prop, index, data, options), this.renderProperty(prop, index, data, options),
@ -118,7 +128,11 @@ export default class JSONNode extends Component {
} }
renderJSONValue ({prop, index, data, options}) { renderJSONValue ({prop, index, data, options}) {
return h('div', {name: compileJSONPointer(this.props.path), className: 'jsoneditor-node'}, [ return h('div', {
name: compileJSONPointer(this.props.path),
onKeyDown: this.handleKeyDown,
className: 'jsoneditor-node'
}, [
this.renderPlaceholder(), this.renderPlaceholder(),
this.renderActionMenuButton(), this.renderActionMenuButton(),
this.renderProperty(prop, index, data, options), this.renderProperty(prop, index, data, options),
@ -134,7 +148,10 @@ export default class JSONNode extends Component {
* @return {*} * @return {*}
*/ */
renderAppend (text) { renderAppend (text) {
return h('div', {className: 'jsoneditor-node'}, [ return h('div', {
className: 'jsoneditor-node',
onKeyDown: this.handleKeyDownAppend
}, [
this.renderPlaceholder(), this.renderPlaceholder(),
this.renderAppendMenuButton(), this.renderAppendMenuButton(),
this.renderReadonly(text) this.renderReadonly(text)
@ -149,7 +166,7 @@ export default class JSONNode extends Component {
return h('div', {key: 'readonly', className: 'jsoneditor-readonly', title}, text) return h('div', {key: 'readonly', className: 'jsoneditor-readonly', title}, text)
} }
renderProperty (prop: ?PropertyData, index: ?number, data: JSONData, options: {escapeUnicode: boolean, isPropertyEditable: (Path) => boolean}) { renderProperty (prop: ?PropertyData, index: ?number, data: JSONData, options: {escapeUnicode: boolean, isPropertyEditable: (path: string) => boolean}) {
const isIndex = typeof index === 'number' const isIndex = typeof index === 'number'
if (!prop && !isIndex) { if (!prop && !isIndex) {
@ -442,9 +459,41 @@ export default class JSONNode extends Component {
} }
} }
/** @private */
handleKeyDown = (event) => {
const keyBinding = this.props.events.findKeyBinding(event)
if (keyBinding === 'duplicate') {
event.preventDefault()
this.props.events.onDuplicate(this.props.path)
}
if (keyBinding === 'insert') {
event.preventDefault()
this.props.events.onInsert(this.props.path, 'value')
}
if (keyBinding === 'remove') {
event.preventDefault()
this.props.events.onRemove(this.props.path)
}
}
/** @private */
handleKeyDownAppend = (event) => {
const keyBinding = this.props.events.findKeyBinding(event)
if (keyBinding === 'insert') {
event.preventDefault()
this.props.events.onAppend(this.props.path, 'value')
}
}
/** @private */ /** @private */
handleKeyDownValue = (event) => { handleKeyDownValue = (event) => {
if (event.ctrlKey && event.which === 13) { // Ctrl+Enter const keyBinding = this.props.events.findKeyBinding(event)
if (keyBinding === 'openUrl') {
this.openLinkIfUrl(event) this.openLinkIfUrl(event)
} }
} }

View File

@ -22,6 +22,7 @@ import JSONNodeView from './JSONNodeView'
import JSONNodeForm from './JSONNodeForm' import JSONNodeForm from './JSONNodeForm'
import ModeButton from './menu/ModeButton' import ModeButton from './menu/ModeButton'
import Search from './menu/Search' import Search from './menu/Search'
import { keyComboFromEvent } from '../utils/keyBindings'
import type { JSONData, JSONPatch } from '../types' import type { JSONData, JSONPatch } from '../types'
@ -45,6 +46,14 @@ export default class TreeMode extends Component {
this.id = Math.round(Math.random() * 1e5) // TODO: create a uuid here? this.id = Math.round(Math.random() * 1e5) // TODO: create a uuid here?
// TODO: make key bindings configurable
const keyBindings = {
'duplicate': ['Ctrl+D', 'Command+D'],
'insert': ['Ctrl+Insert', 'Command+Insert'],
'remove': ['Ctrl+Delete', 'Command+Delete'],
'openUrl': ['Ctrl+4', 'Ctrl+Enter', 'Command+Enter']
}
this.state = { this.state = {
data, data,
@ -61,13 +70,18 @@ export default class TreeMode extends Component {
onRemove: this.handleRemove, onRemove: this.handleRemove,
onSort: this.handleSort, onSort: this.handleSort,
onExpand: this.handleExpand onExpand: this.handleExpand,
// TODO: now we're passing not just events but also other methods. reorganize this or rename 'state.events'
findKeyBinding: this.findKeyBinding
}, },
search: { search: {
text: '', text: '',
active: null // active search result active: null // active search result
} },
keyCombos: this.bindingsByCombos (keyBindings)
} }
} }
@ -133,6 +147,9 @@ export default class TreeMode extends Component {
return h('div', { return h('div', {
className: `jsoneditor jsoneditor-mode-${props.mode}`, className: `jsoneditor jsoneditor-mode-${props.mode}`,
'onKeyDown': (event) => {
// console.log('keydown', keyComboFromEvent(event), this.findKeyBinding(keyComboFromEvent(event)))
},
'data-jsoneditor': 'true' 'data-jsoneditor': 'true'
}, [ }, [
this.renderMenu(searchResults ? searchResults.length : null), this.renderMenu(searchResults ? searchResults.length : null),
@ -226,6 +243,27 @@ export default class TreeMode extends Component {
return h('div', {key: 'menu', className: 'jsoneditor-menu'}, items) return h('div', {key: 'menu', className: 'jsoneditor-menu'}, items)
} }
/**
* Turn a map with key bindings by name into a map by combo
* @param {Object.<String, Array.string>} keyBindings
* @return {Object.<String, string>} Returns keyCombos
*/
bindingsByCombos (keyBindings) {
const keyCombos = {}
Object.keys(keyBindings).forEach ((name) => {
keyBindings[name].forEach(combo => keyCombos[combo.toUpperCase()] = name)
})
return keyCombos
}
findKeyBinding = (event) => {
const keyCombo = keyComboFromEvent(event)
return this.state.keyCombos[keyCombo.toUpperCase()] || null
}
/** /**
* Validate the JSON against the configured JSON schema * Validate the JSON against the configured JSON schema
* Returns an array with the errors when not valid, returns an empty array * Returns an array with the errors when not valid, returns an empty array
@ -266,11 +304,13 @@ export default class TreeMode extends Component {
/** @private */ /** @private */
handleInsert = (path, type) => { handleInsert = (path, type) => {
this.handlePatch(insert(this.state.data, path, type)) this.handlePatch(insert(this.state.data, path, type))
// FIXME: apply focus to new field
} }
/** @private */ /** @private */
handleAppend = (parentPath, type) => { handleAppend = (parentPath, type) => {
this.handlePatch(append(this.state.data, parentPath, type)) this.handlePatch(append(this.state.data, parentPath, type))
// FIXME: apply focus to new field
} }
/** @private */ /** @private */
@ -281,6 +321,7 @@ export default class TreeMode extends Component {
/** @private */ /** @private */
handleRemove = (path) => { handleRemove = (path) => {
this.handlePatch(remove(path)) this.handlePatch(remove(path))
// FIXME: apply focus next/prev field
} }
/** @private */ /** @private */

146
src/utils/keyBindings.js Normal file
View File

@ -0,0 +1,146 @@
// inspiration: https://github.com/andrepolischuk/keycomb
/**
* Get a named key from a key code.
* For example:
* keyFromCode(65) returns 'A'
* keyFromCode(13) returns 'Enter'
* @param {string} code
* @return {string}
*/
export function nameFromKeyCode(code) {
return codes[code] || ''
}
/**
* Get the active key combination from a keyboard event.
* For example returns "Ctrl+Shift+Up" or "Ctrl+A"
* @param {KeyboardEvent} event
* @return {string}
*/
export function keyComboFromEvent (event) {
let combi = []
if (event.ctrlKey) { combi.push('Ctrl') }
if (event.metaKey) { combi.push('Command') }
if (event.altKey) { combi.push(isMac ? 'Option' : 'Alt') }
if (event.shiftKey) { combi.push('Shift') }
const keyName = nameFromKeyCode(event.which)
if (!metaCodes[keyName]) { // prevent output like 'Ctrl+Ctrl'
combi.push(keyName)
}
return combi.join('+')
}
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0
const metaCodes = {
'Ctrl': true,
'Command': true,
'Alt': true,
'Option': true,
'Shift': true
}
const codes = {
'8': 'Backspace',
'9': 'Tab',
'13': 'Enter',
'16': 'Shift',
'17': 'Ctrl',
'18': 'Alt',
'19': 'Pause_Break',
'20': 'Caps_Lock',
'27': 'Escape',
'33': 'Page_Up',
'34': 'Page_Down',
'35': 'End',
'36': 'Home',
'37': 'Left',
'38': 'Up',
'39': 'Right',
'40': 'Down',
'45': 'Insert',
'46': 'Delete',
'48': '0',
'49': '1',
'50': '2',
'51': '3',
'52': '4',
'53': '5',
'54': '6',
'55': '7',
'56': '8',
'57': '9',
'65': 'A',
'66': 'B',
'67': 'C',
'68': 'D',
'69': 'E',
'70': 'F',
'71': 'G',
'72': 'H',
'73': 'I',
'74': 'J',
'75': 'K',
'76': 'L',
'77': 'M',
'78': 'N',
'79': 'O',
'80': 'P',
'81': 'Q',
'82': 'R',
'83': 'S',
'84': 'T',
'85': 'U',
'86': 'V',
'87': 'W',
'88': 'X',
'89': 'Y',
'90': 'Z',
'91': 'Left_Window_Key',
'92': 'Right_Window_Key',
'93': 'Select_Key',
'96': 'Numpad_0',
'97': 'Numpad_1',
'98': 'Numpad_2',
'99': 'Numpad_3',
'100': 'Numpad_4',
'101': 'Numpad_5',
'102': 'Numpad_6',
'103': 'Numpad_7',
'104': 'Numpad_8',
'105': 'Numpad_9',
'106': 'Numpad_*',
'107': 'Numpad_+',
'109': 'Numpad_-',
'110': 'Numpad_.',
'111': 'Numpad_/',
'112': 'F1',
'113': 'F2',
'114': 'F3',
'115': 'F4',
'116': 'F5',
'117': 'F6',
'118': 'F7',
'119': 'F8',
'120': 'F9',
'121': 'F10',
'122': 'F11',
'123': 'F12',
'144': 'Num_Lock',
'145': 'Scroll_Lock',
'186': ';',
'187': '=',
'188': ',',
'189': '-',
'190': '.',
'191': '/',
'192': '`',
'219': '[',
'220': '\\',
'221': ']',
'222': '\''
}