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}) {
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 */
|
||||||
|
|
|
@ -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