Implemented quickkeys to move up/down/left/right
This commit is contained in:
parent
111b85a4cb
commit
bb6565f3b3
|
@ -149,6 +149,7 @@ export default class JSONNode extends Component {
|
|||
*/
|
||||
renderAppend (text) {
|
||||
return h('div', {
|
||||
name: compileJSONPointer(this.props.path) + '/#',
|
||||
className: 'jsoneditor-node',
|
||||
onKeyDown: this.handleKeyDownAppend
|
||||
}, [
|
||||
|
@ -506,52 +507,6 @@ export default class JSONNode extends Component {
|
|||
this.props.events.onExpand(this.props.path, expanded, recurse)
|
||||
}
|
||||
|
||||
/** @private */
|
||||
handleContextMenu = (event) => {
|
||||
event.stopPropagation()
|
||||
|
||||
if (this.state.menu) {
|
||||
// hide context menu
|
||||
JSONNode.hideActionMenu()
|
||||
}
|
||||
else {
|
||||
// hide any currently visible context menu
|
||||
JSONNode.hideActionMenu()
|
||||
|
||||
// show context menu
|
||||
this.setState({
|
||||
menu: {
|
||||
anchor: event.target,
|
||||
root: JSONNode.findRootElement(event)
|
||||
}
|
||||
})
|
||||
activeContextMenu = this
|
||||
}
|
||||
}
|
||||
|
||||
/** @private */
|
||||
handleAppendContextMenu = (event) => {
|
||||
event.stopPropagation()
|
||||
|
||||
if (this.state.appendMenu) {
|
||||
// hide append context menu
|
||||
JSONNode.hideActionMenu()
|
||||
}
|
||||
else {
|
||||
// hide any currently visible context menu
|
||||
JSONNode.hideActionMenu()
|
||||
|
||||
// show append context menu
|
||||
this.setState({
|
||||
appendMenu: {
|
||||
anchor: event.target,
|
||||
root: JSONNode.findRootElement(event)
|
||||
}
|
||||
})
|
||||
activeContextMenu = this
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Singleton function to hide the currently visible context menu if any.
|
||||
* @protected
|
||||
|
|
|
@ -22,6 +22,7 @@ import JSONNodeView from './JSONNodeView'
|
|||
import JSONNodeForm from './JSONNodeForm'
|
||||
import ModeButton from './menu/ModeButton'
|
||||
import Search from './menu/Search'
|
||||
import { moveUp, moveDown, moveLeft, moveRight } from './util/domSelector'
|
||||
import { keyComboFromEvent } from '../utils/keyBindings'
|
||||
|
||||
import type { JSONData, JSONPatch } from '../types'
|
||||
|
@ -51,7 +52,23 @@ export default class TreeMode extends Component {
|
|||
'duplicate': ['Ctrl+D', 'Command+D'],
|
||||
'insert': ['Ctrl+Insert', 'Command+Insert'],
|
||||
'remove': ['Ctrl+Delete', 'Command+Delete'],
|
||||
'openUrl': ['Ctrl+4', 'Ctrl+Enter', 'Command+Enter']
|
||||
'up': ['Alt+Up', 'Option+Up'],
|
||||
'down': ['Alt+Down', 'Option+Down'],
|
||||
'left': ['Alt+Left', 'Option+Left'],
|
||||
'right': ['Alt+Right', 'Option+Right'],
|
||||
'openUrl': ['Ctrl+Enter', 'Command+Enter']
|
||||
// TODO: implement all quick keys
|
||||
// Ctrl+Shift+Arrow Up/Down Select multiple fields
|
||||
// Shift+Alt+Arrows Move current field or selected fields up/down/left/right
|
||||
// Ctrl+Ins Insert a new field with type auto
|
||||
// Ctrl+Shift+Ins Append a new field with type auto
|
||||
// Ctrl+E Expand or collapse field
|
||||
// Ctrl+F Find
|
||||
// F3, Ctrl+G Find next
|
||||
// Shift+F3, Ctrl+Shift+G Find previous
|
||||
// Ctrl+M Show actions menu
|
||||
// Ctrl+Z Undo last action
|
||||
// Ctrl+Shift+Z Redo
|
||||
}
|
||||
|
||||
this.state = {
|
||||
|
@ -147,9 +164,7 @@ 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)))
|
||||
},
|
||||
'onKeyDown': this.handleKeyDown,
|
||||
'data-jsoneditor': 'true'
|
||||
}, [
|
||||
this.renderMenu(searchResults ? searchResults.length : null),
|
||||
|
@ -281,6 +296,30 @@ export default class TreeMode extends Component {
|
|||
return []
|
||||
}
|
||||
|
||||
handleKeyDown = (event) => {
|
||||
const keyBinding = this.findKeyBinding(event)
|
||||
|
||||
if (keyBinding === 'up') {
|
||||
event.preventDefault()
|
||||
moveUp(event.target)
|
||||
}
|
||||
|
||||
if (keyBinding === 'down') {
|
||||
event.preventDefault()
|
||||
moveDown(event.target)
|
||||
}
|
||||
|
||||
if (keyBinding === 'left') {
|
||||
event.preventDefault()
|
||||
moveLeft(event.target)
|
||||
}
|
||||
|
||||
if (keyBinding === 'right') {
|
||||
event.preventDefault()
|
||||
moveRight(event.target)
|
||||
}
|
||||
}
|
||||
|
||||
/** @private */
|
||||
handleHideMenus = () => {
|
||||
JSONNode.hideActionMenu()
|
||||
|
|
|
@ -0,0 +1,220 @@
|
|||
import { selectContentEditable } from '../../utils/domUtils'
|
||||
|
||||
// singleton
|
||||
let lastInputName = null
|
||||
|
||||
/**
|
||||
* Move the selection to the input field above current selected input
|
||||
* Heavily relies on classNames of the JSONEditor DOM
|
||||
* @param {Element} fromElement
|
||||
*/
|
||||
export function moveUp (fromElement) {
|
||||
const prev = findPreviousNode(fromElement)
|
||||
if (prev) {
|
||||
if (!lastInputName) {
|
||||
lastInputName = getInputName(fromElement)
|
||||
}
|
||||
|
||||
const container = findContainer(fromElement)
|
||||
setSelection(container, prev.getAttribute('name'), lastInputName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the selection to the input field below current selected input
|
||||
* Heavily relies on classNames of the JSONEditor DOM
|
||||
* @param {Element} fromElement
|
||||
*/
|
||||
export function moveDown (fromElement) {
|
||||
const prev = findNextNode(fromElement)
|
||||
if (prev) {
|
||||
if (!lastInputName) {
|
||||
lastInputName = getInputName(fromElement)
|
||||
}
|
||||
|
||||
const container = findContainer(fromElement)
|
||||
setSelection(container, prev.getAttribute('name'), lastInputName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the selection to the input field left from current selected input
|
||||
* Heavily relies on classNames of the JSONEditor DOM
|
||||
* @param {Element} fromElement
|
||||
*/
|
||||
export function moveLeft (fromElement) {
|
||||
const container = findContainer(fromElement)
|
||||
const node = findNode(fromElement, 'jsoneditor-node')
|
||||
const inputName = getInputName(fromElement)
|
||||
lastInputName = findInput(node, inputName, 'left')
|
||||
setSelection(container, node.getAttribute('name'), lastInputName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the selection to the input field right from current selected input
|
||||
* Heavily relies on classNames of the JSONEditor DOM
|
||||
* @param {Element} fromElement
|
||||
*/
|
||||
export function moveRight (fromElement) {
|
||||
const container = findContainer(fromElement)
|
||||
const node = findNode(fromElement, 'jsoneditor-node')
|
||||
const inputName = getInputName(fromElement)
|
||||
lastInputName = findInput(node, inputName, 'right')
|
||||
setSelection(container, node.getAttribute('name'), lastInputName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set selection to a specific node and input field
|
||||
* @param {Element} container
|
||||
* @param {JSONPointer} path
|
||||
* @param {string} inputName
|
||||
*/
|
||||
export function setSelection (container, path, inputName) {
|
||||
const node = container.querySelector(`div[name="${path}"]`)
|
||||
if (node) {
|
||||
const closestInputName = findInput(node, inputName, 'closest')
|
||||
const element = findInputName(node, closestInputName)
|
||||
if (element) {
|
||||
element.focus()
|
||||
if (element.nodeName === 'DIV') {
|
||||
selectContentEditable(element)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function findContainer (element) {
|
||||
return findNode (element, 'jsoneditor-tree-contents')
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the base element of a node from one of it's childs
|
||||
* @param {Element} element
|
||||
* @param {string} className
|
||||
* @return {Element} Returns the base element of the node
|
||||
*/
|
||||
function findNode (element, className) {
|
||||
let e = element
|
||||
do {
|
||||
if (e && e.className.includes(className)) {
|
||||
return e
|
||||
}
|
||||
|
||||
e = e.parentNode
|
||||
}
|
||||
while (e)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function findPreviousNode (element) {
|
||||
const container = findContainer(element)
|
||||
const node = findNode(element, 'jsoneditor-node')
|
||||
|
||||
// TODO: implement a faster way to find the previous node, by walking the DOM tree back, instead of a slow find all query
|
||||
const all = Array.from(container.querySelectorAll('div.jsoneditor-node'))
|
||||
const index = all.indexOf(node)
|
||||
|
||||
return all[index - 1]
|
||||
}
|
||||
|
||||
function findNextNode (element) {
|
||||
const container = findContainer(element)
|
||||
const node = findNode(element, 'jsoneditor-node')
|
||||
|
||||
// TODO: implement a faster way to find the previous node, by walking the DOM tree, instead of a slow find all query
|
||||
const all = Array.from(container.querySelectorAll('div.jsoneditor-node'))
|
||||
const index = all.indexOf(node)
|
||||
|
||||
return all[index + 1]
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the input name of an element
|
||||
* @param {Element} element
|
||||
* @return {'property' | 'value' | 'action' | 'expand' | null}
|
||||
*/
|
||||
function getInputName (element) {
|
||||
if (element.className.includes('jsoneditor-property')) {
|
||||
return 'property'
|
||||
}
|
||||
|
||||
if (element.className.includes('jsoneditor-value')) {
|
||||
return 'value'
|
||||
}
|
||||
|
||||
if (element.className.includes('jsoneditor-actionmenu')) {
|
||||
return 'action'
|
||||
}
|
||||
|
||||
if (element.className.includes('jsoneditor-expanded') ||
|
||||
element.className.includes('jsoneditor-collapsed')) {
|
||||
return 'expand'
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function findInputName (node, name) {
|
||||
if (node) {
|
||||
if (name === 'property') {
|
||||
const div = node.querySelector('.jsoneditor-property')
|
||||
return (div && div.contentEditable === 'true') ? div : null
|
||||
}
|
||||
|
||||
if (name === 'value') {
|
||||
const div = node.querySelector('.jsoneditor-value')
|
||||
return (div && div.contentEditable === 'true') ? div : null
|
||||
}
|
||||
|
||||
if (name === 'action') {
|
||||
return node.querySelector('.jsoneditor-actionmenu')
|
||||
}
|
||||
|
||||
if (name === 'expand') {
|
||||
return node.querySelector('.jsoneditor-expanded') || node.querySelector('.jsoneditor-collapsed')
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* find the closest input that actually exists in this node
|
||||
* @param {Element} node
|
||||
* @param {string} inputName
|
||||
* @param {'closest' | 'left' | 'right'} [rule]
|
||||
* @return {Element}
|
||||
*/
|
||||
function findInput (node, inputName, rule = 'closest') {
|
||||
const inputNames = INPUT_NAME_RULES[rule][inputName]
|
||||
if (inputNames) {
|
||||
return inputNames.find(name => {
|
||||
return findInputName(node, name)
|
||||
})
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const INPUT_NAME_RULES = {
|
||||
closest: {
|
||||
'property': ['property', 'value', 'action', 'expand'],
|
||||
'value': ['value', 'property', 'action', 'expand'],
|
||||
'action': ['action', 'expand', 'property', 'value'],
|
||||
'expand': ['expand', 'action', 'property', 'value'],
|
||||
},
|
||||
left: {
|
||||
'property': ['action', 'expand'],
|
||||
'value': ['property', 'action', 'expand'],
|
||||
'action': ['expand'],
|
||||
'expand': [],
|
||||
},
|
||||
right: {
|
||||
'property': ['value'],
|
||||
'value': [],
|
||||
'action': ['property', 'value'],
|
||||
'expand': ['action', 'property', 'value'],
|
||||
}
|
||||
}
|
||||
|
|
@ -70,6 +70,24 @@ export function getInnerText (element, buffer) {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Select all text of a content editable div.
|
||||
* http://stackoverflow.com/a/3806004/1262753
|
||||
* @param {Element} contentEditableElement A content editable div
|
||||
*/
|
||||
export function selectContentEditable(contentEditableElement) {
|
||||
if (!contentEditableElement || contentEditableElement.nodeName !== 'DIV') {
|
||||
return
|
||||
}
|
||||
|
||||
if (window.getSelection && document.createRange) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(contentEditableElement)
|
||||
const sel = window.getSelection()
|
||||
sel.removeAllRanges()
|
||||
sel.addRange(range)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the parent node of an element which has an attribute with given value.
|
||||
|
|
Loading…
Reference in New Issue