Implemented basic support for key bindings
This commit is contained in:
parent
82ff880c27
commit
fb71b61ba5
|
@ -41,7 +41,12 @@ export default class JSONNode extends Component {
|
|||
|
||||
renderJSONObject ({prop, index, data, options, events}) {
|
||||
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.renderActionMenuButton(),
|
||||
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?)
|
||||
renderJSONArray ({prop, index, data, options, events}) {
|
||||
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.renderActionMenuButton(),
|
||||
this.renderProperty(prop, index, data, options),
|
||||
|
@ -118,7 +128,11 @@ export default class JSONNode extends Component {
|
|||
}
|
||||
|
||||
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.renderActionMenuButton(),
|
||||
this.renderProperty(prop, index, data, options),
|
||||
|
@ -134,7 +148,10 @@ export default class JSONNode extends Component {
|
|||
* @return {*}
|
||||
*/
|
||||
renderAppend (text) {
|
||||
return h('div', {className: 'jsoneditor-node'}, [
|
||||
return h('div', {
|
||||
className: 'jsoneditor-node',
|
||||
onKeyDown: this.handleKeyDownAppend
|
||||
}, [
|
||||
this.renderPlaceholder(),
|
||||
this.renderAppendMenuButton(),
|
||||
this.renderReadonly(text)
|
||||
|
@ -149,7 +166,7 @@ export default class JSONNode extends Component {
|
|||
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'
|
||||
|
||||
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 */
|
||||
handleKeyDownValue = (event) => {
|
||||
if (event.ctrlKey && event.which === 13) { // Ctrl+Enter
|
||||
const keyBinding = this.props.events.findKeyBinding(event)
|
||||
|
||||
if (keyBinding === 'openUrl') {
|
||||
this.openLinkIfUrl(event)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import JSONNodeView from './JSONNodeView'
|
|||
import JSONNodeForm from './JSONNodeForm'
|
||||
import ModeButton from './menu/ModeButton'
|
||||
import Search from './menu/Search'
|
||||
import { keyComboFromEvent } from '../utils/keyBindings'
|
||||
|
||||
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?
|
||||
|
||||
// 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 = {
|
||||
data,
|
||||
|
||||
|
@ -61,13 +70,18 @@ export default class TreeMode extends Component {
|
|||
onRemove: this.handleRemove,
|
||||
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: {
|
||||
text: '',
|
||||
active: null // active search result
|
||||
}
|
||||
},
|
||||
|
||||
keyCombos: this.bindingsByCombos (keyBindings)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -133,6 +147,9 @@ export default class TreeMode extends Component {
|
|||
|
||||
return h('div', {
|
||||
className: `jsoneditor jsoneditor-mode-${props.mode}`,
|
||||
'onKeyDown': (event) => {
|
||||
// console.log('keydown', keyComboFromEvent(event), this.findKeyBinding(keyComboFromEvent(event)))
|
||||
},
|
||||
'data-jsoneditor': 'true'
|
||||
}, [
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* Returns an array with the errors when not valid, returns an empty array
|
||||
|
@ -266,11 +304,13 @@ export default class TreeMode extends Component {
|
|||
/** @private */
|
||||
handleInsert = (path, type) => {
|
||||
this.handlePatch(insert(this.state.data, path, type))
|
||||
// FIXME: apply focus to new field
|
||||
}
|
||||
|
||||
/** @private */
|
||||
handleAppend = (parentPath, type) => {
|
||||
this.handlePatch(append(this.state.data, parentPath, type))
|
||||
// FIXME: apply focus to new field
|
||||
}
|
||||
|
||||
/** @private */
|
||||
|
@ -281,6 +321,7 @@ export default class TreeMode extends Component {
|
|||
/** @private */
|
||||
handleRemove = (path) => {
|
||||
this.handlePatch(remove(path))
|
||||
// FIXME: apply focus next/prev field
|
||||
}
|
||||
|
||||
/** @private */
|
||||
|
|
|
@ -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': '\''
|
||||
}
|
Loading…
Reference in New Issue