Implemented custom key bindings

This commit is contained in:
jos 2017-07-24 15:50:18 +02:00
parent 239b702040
commit 8462bcda2c
7 changed files with 170 additions and 25 deletions

View File

@ -37,7 +37,7 @@ Constructs a new JSONEditor.
library used for JSON schema validation. Example: library used for JSON schema validation. Example:
```js ```js
var options = { const options = {
ajv: Ajv({ allErrors: true, verbose: true }) ajv: Ajv({ allErrors: true, verbose: true })
} }
``` ```
@ -72,6 +72,25 @@ Constructs a new JSONEditor.
Enables history, adds a button Undo and Redo to the menu of the JSONEditor. True by default. Only applicable when `mode` is 'tree' or 'form'. Enables history, adds a button Undo and Redo to the menu of the JSONEditor. True by default. Only applicable when `mode` is 'tree' or 'form'.
- `{Object<String, String[]>} keyBindings`
Override default key bindings. For example to replace the binding to duplicate a node from `Ctrl+D` to `Ctrl+Shift+D`:
```js
const options = {
keyBindings: {
duplicate: ['Ctrl+Shift+D', 'Command+Shift+D']
}
}
```
It's important to define bindings for both Windows and Mac. The meta keys on Windows are `Ctrl`, `Shift`, `Alt`, and on Mac they are respectively `Command`, `Shift`, and `Option`.
All available key bindings are described on the page [Key Bindings](#key_bindings.md).
Key bindings are case insensitive.
- `{String} mode` - `{String} mode`
Set the editor mode. Available values: 'tree' (default), 'view', 'form', 'code', 'text'. In 'view' mode, the data and datastructure is read-only. In 'form' mode, only the value can be changed, the datastructure is read-only. Mode 'code' requires the Ace editor to be loaded on the page. Mode 'text' shows the data as plain text. Set the editor mode. Available values: 'tree' (default), 'view', 'form', 'code', 'text'. In 'view' mode, the data and datastructure is read-only. In 'form' mode, only the value can be changed, the datastructure is read-only. Mode 'code' requires the Ace editor to be loaded on the page. Mode 'text' shows the data as plain text.
@ -229,12 +248,12 @@ Get JSON data as string.
A tree editor: A tree editor:
```js ```js
var options = { const options = {
mode: 'tree', mode: 'tree',
search: true search: true
} }
var editor = new JSONEditor(container, options) const editor = new JSONEditor(container, options)
var json = { let json = {
"Array": [1, 2, 3], "Array": [1, 2, 3],
"Boolean": true, "Boolean": true,
"Null": null, "Null": null,
@ -245,18 +264,18 @@ var json = {
editor.set(json) editor.set(json)
editor.expandAll() editor.expandAll()
var json = editor.get(json) json = editor.get(json)
``` ```
A text editor: A text editor:
```js ```js
var options = { const options = {
mode: 'text', mode: 'text',
indentation: 2 indentation: 2
} }
var editor = new JSONEditor(container, options) const editor = new JSONEditor(container, options)
var json = { let json = {
"Array": [1, 2, 3], "Array": [1, 2, 3],
"Boolean": true, "Boolean": true,
"Null": null, "Null": null,
@ -266,7 +285,7 @@ var json = {
} }
editor.set(json) editor.set(json)
var json = editor.get() json = editor.get()
``` ```
## JSON parsing and stringification ## JSON parsing and stringification
@ -274,17 +293,17 @@ var json = editor.get()
In general to parse or stringify JSON data, the browsers built in JSON parser can be used. To create a formatted string from a JSON object, use: In general to parse or stringify JSON data, the browsers built in JSON parser can be used. To create a formatted string from a JSON object, use:
```js ```js
var formattedString = JSON.stringify(json, null, 2) const formattedString = JSON.stringify(json, null, 2)
``` ```
to create a compacted string from a JSON object, use: to create a compacted string from a JSON object, use:
```js ```js
var compactString = JSON.stringify(json) const compactString = JSON.stringify(json)
``` ```
To parse a String to a JSON object, use: To parse a String to a JSON object, use:
```js ```js
var json = JSON.parse(string) const json = JSON.parse(string)
``` ```

View File

@ -0,0 +1,56 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>Custom key bindings | JSONEditor</title>
<script src="../dist/jsoneditor.js"></script>
<style type="text/css">
body {
width: 100%;
max-width: 500px;
margin: 0;
padding: 10px;
box-sizing: border-box;
font-family: sans-serif;
}
h1 {
font-size: 120%;
}
</style>
</head>
<body>
<h1>Custom key bindings</h1>
<p>
In this example, the key bindings for <code>format</code> and <code>compact</code> are changed to respectively <code>Ctrl+Alt+1</code> and <code>Ctrl+Alt+2</code> instead of the default <code>Ctrl+\</code> and <code>Ctrl+Shift+\</code>.
</p>
<p>
It's important to define key bindings for both Windows and Mac: <code>Command+Option+1</code> and <code>Command+Option+2</code> in this case.
</p>
<div id="jsoneditor"></div>
<script>
// create the editor
var container = document.getElementById('jsoneditor')
var options = {
modes: ['code', 'text'],
keyBindings: {
compact: ['Ctrl+Alt+1', 'Command+Option+1'],
format: ['Ctrl+Alt+2', 'Command+Option+2']
}
}
var json = {
'array': [1, 2, 3],
'boolean': true,
'null': null,
'number': 123,
'object': {'a': 'b', 'c': 'd'},
'string': 'Hello World'
}
var editor = jsoneditor(container, options)
editor.set(json)
</script>
</body>
</html>

View File

@ -54,9 +54,6 @@ export default class TextMode extends Component {
constructor (props) { constructor (props) {
super(props) super(props)
// TODO: make key bindings customizable
this.findKeyBinding = createFindKeyBinding(KEY_BINDINGS)
this.state = { this.state = {
text: '{}', text: '{}',
compiledSchema: null compiledSchema: null
@ -88,6 +85,14 @@ export default class TextMode extends Component {
this.setSchema(nextProps.schema) this.setSchema(nextProps.schema)
} }
// Apply key bindings
if (!this.findKeyBinding ||
JSON.stringify(nextProps.keyBindings) !== JSON.stringify(currentProps.keyBindings)) {
// merge default and custom key bindings
const keyBindings = Object.assign({}, KEY_BINDINGS, nextProps.keyBindings)
this.findKeyBinding = createFindKeyBinding(keyBindings)
}
// TODO: apply patchText // TODO: apply patchText
} }

View File

@ -66,9 +66,6 @@ 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 customizable
this.findKeyBinding = createFindKeyBinding(KEY_BINDINGS)
this.state = { this.state = {
data, data,
@ -88,7 +85,7 @@ export default class TreeMode extends Component {
onExpand: this.handleExpand, onExpand: this.handleExpand,
// TODO: now we're passing not just events but also other methods. reorganize this or rename 'state.events' // TODO: now we're passing not just events but also other methods. reorganize this or rename 'state.events'
findKeyBinding: this.findKeyBinding findKeyBinding: this.handleFindKeyBinding
}, },
search: { search: {
@ -106,7 +103,7 @@ export default class TreeMode extends Component {
this.applyProps(nextProps, this.props) this.applyProps(nextProps, this.props)
} }
// TODO: create some sort of watcher structure for these props? Is there a Reactpattern for that? // TODO: create some sort of watcher structure for these props? Is there a React pattern for that?
applyProps (nextProps, currentProps) { applyProps (nextProps, currentProps) {
// Apply text // Apply text
if (nextProps.text !== currentProps.text) { if (nextProps.text !== currentProps.text) {
@ -131,6 +128,14 @@ export default class TreeMode extends Component {
this.setSchema(nextProps.schema) this.setSchema(nextProps.schema)
} }
// Apply key bindings
if (!this.findKeyBinding ||
JSON.stringify(nextProps.keyBindings) !== JSON.stringify(currentProps.keyBindings)) {
// merge default and custom key bindings
const keyBindings = Object.assign({}, KEY_BINDINGS, nextProps.keyBindings)
this.findKeyBinding = createFindKeyBinding(keyBindings)
}
// TODO: apply patchText // TODO: apply patchText
// TODO: apply patch // TODO: apply patch
} }
@ -370,6 +375,12 @@ export default class TreeMode extends Component {
} }
} }
/** @private */
handleFindKeyBinding = (event) => {
// findKeyBinding can change on the fly, so we can't bind it statically
return this.findKeyBinding (event)
}
/** @private */ /** @private */
handleExpandAll = () => { handleExpandAll = () => {
const expanded = true const expanded = true

View File

@ -80,7 +80,11 @@
alert(err) alert(err)
}, },
mode: mode, mode: mode,
modes: ['text', 'code', 'tree', 'form', 'view'], modes: ['text', 'code', 'tree', 'form', 'view'], keyBindings: {
compact: ['Ctrl+\\', 'Command+\\', 'Ctrl+Alt+1', 'Command+Option+1'],
format: ['Ctrl+Shift+\\', 'Command+Shift+\\', 'Ctrl+Alt+2', 'Command+Option+2'],
duplicate: ['Ctrl+D', 'Ctrl+Shift+D', 'Command+D', 'Command+Shift+D']
},
indentation: 4, indentation: 4,
escapeUnicode: true, escapeUnicode: true,
history: true, history: true,

View File

@ -255,7 +255,8 @@ function jsoneditor (container, options = {}) {
unmountComponentAtNode(editor._container) unmountComponentAtNode(editor._container)
} }
editor.setMode(options && options.mode || 'tree') const mode = options && options.mode || (options.modes && options.modes[0]) || 'tree';
editor.setMode(mode)
return editor return editor
} }

View File

@ -1,5 +1,9 @@
// inspiration: https://github.com/andrepolischuk/keycomb // inspiration: https://github.com/andrepolischuk/keycomb
// TODO: write unit tests for keyBindings
// FIXME: implement an escape sequence for the separator +
/** /**
* Get a named key from a key code. * Get a named key from a key code.
* For example: * For example:
@ -44,16 +48,36 @@ export function createFindKeyBinding (keyBindings) {
// turn the map with key bindings by name (multiple per binding) into a map by key combo // turn the map with key bindings by name (multiple per binding) into a map by key combo
const keyCombos = {} const keyCombos = {}
Object.keys(keyBindings).forEach ((name) => { Object.keys(keyBindings).forEach ((name) => {
keyBindings[name].forEach(combo => keyCombos[combo.toUpperCase()] = name) keyBindings[name].forEach(combo => keyCombos[normalizeKeyCombo(combo)] = name)
}) })
return function findKeyBinding (event){ return function findKeyBinding (event) {
const keyCombo = keyComboFromEvent(event) const keyCombo = keyComboFromEvent(event)
console.log('keyCombo', keyCombo)
return keyCombos[keyCombo.toUpperCase()] || null return keyCombos[normalizeKeyCombo(keyCombo)] || null
} }
} }
/**
* Normalize a key combo:
*
* - to upper case
* - replace aliases like "?" with "/"
*
* @param {string} combo
* @return {string}
*/
function normalizeKeyCombo (combo) {
const upper = combo.toUpperCase()
const last = upper[upper.length - 1]
if (last in aliases) {
return upper.substring(0, upper.length - 1) + aliases[last]
}
return upper
}
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0 const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0
@ -165,3 +189,28 @@ const codes = {
'221': ']', '221': ']',
'222': '\'' '222': '\''
} }
// all secondary characters of the keyboard buttons (used via Shift)
const aliases = {
'~': '`',
'!': '1',
'@': '2',
'#': '3',
'$': '4',
'%': '5',
'^': '6',
'&': '7',
'*': '8',
'(': '9',
')': '0',
'_': '-',
'+': '=',
'{': '[',
'}': ']',
'|': '\\',
':': ';',
'"': '',
'<': ',',
'>': '.',
'?': '/'
}