Implemented quickkeys to move up/down/left/right

This commit is contained in:
jos 2017-07-12 15:01:19 +02:00
parent 111b85a4cb
commit bb6565f3b3
4 changed files with 282 additions and 50 deletions

View File

@ -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

View File

@ -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()

View File

@ -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'],
}
}

View File

@ -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.