[Canvas] Updates keyboard shortcuts (#29394) (#31214)

* Updated redo shortcuts

* Moved element deleting handling from event_handler.js to keyHandler used in the Shortcut component

    Added shortcut for duplicating elements

    Removed cmd/ctrl+y for redo. conflicts with google chrome

    Added backspace to navigate back a slide in presentation mode

    fixed presentation shortcuts

    Added comments

    Fixed duplicate elements function

    Refactored event handlers

    Added shortcuts for layer manipulation

* Added TODOs

* Added TODO

* Reverted TS changes in keymap.js

* Fixed relayer handlers

* Fixed remove element

* Disables layer manipulation shortcuts when multiple elements are selected

* Added comment
This commit is contained in:
Catherine Liu 2019-02-14 16:37:49 -07:00 committed by GitHub
parent 56075e0190
commit af6299bb66
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 146 additions and 58 deletions

View file

@ -4,8 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { withHandlers } from 'recompose';
const ancestorElement = element => {
if (!element) {
return element;
@ -137,19 +135,14 @@ const isTextInput = ({ tagName, type }) => {
const modifierKey = key => ['KeyALT', 'KeyCONTROL'].indexOf(keyCode(key)) > -1;
const handleKeyDown = (commit, e, isEditable, remove) => {
const { key, target } = e;
const handleKeyDown = (commit, e, isEditable) => {
const { key } = e;
if (isEditable) {
if ((key === 'Backspace' || key === 'Delete') && !isTextInput(target)) {
e.preventDefault();
remove();
} else if (!modifierKey(key)) {
commit('keyboardEvent', {
event: 'keyDown',
code: keyCode(key), // convert to standard event code
});
}
if (isEditable && !modifierKey(key)) {
commit('keyboardEvent', {
event: 'keyDown',
code: keyCode(key), // convert to standard event code
});
}
};
@ -172,12 +165,12 @@ const handleKeyUp = (commit, { key }, isEditable) => {
}
};
export const withEventHandlers = withHandlers({
export const eventHandlers = {
onMouseDown: props => e => handleMouseDown(props.commit, e, props.isEditable),
onMouseMove: props => e => handleMouseMove(props.commit, e, props.isEditable),
onKeyDown: props => e => handleKeyDown(props.commit, e, props.isEditable, props.remove),
onKeyDown: props => e => handleKeyDown(props.commit, e, props.isEditable),
onKeyPress: props => e => handleKeyPress(props.commit, e, props.isEditable),
onKeyUp: props => e => handleKeyUp(props.commit, e, props.isEditable),
onWheel: props => e => handleWheel(props.commit, e, props.isEditable),
resetHandler: () => () => resetHandler(),
});
};

View file

@ -6,16 +6,16 @@
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { compose, withState, withProps } from 'recompose';
import { compose, withState, withProps, withHandlers } from 'recompose';
import { notify } from '../../lib/notify';
import { aeroelastic } from '../../lib/aeroelastic_kibana';
import { setClipboardData, getClipboardData } from '../../lib/clipboard';
import { cloneSubgraphs } from '../../lib/clone_subgraphs';
import { removeElements, insertNodes } from '../../state/actions/elements';
import { removeElements, insertNodes, elementLayer } from '../../state/actions/elements';
import { getFullscreen, canUserWrite } from '../../state/selectors/app';
import { getNodes, isWriteable } from '../../state/selectors/workpad';
import { flatten } from '../../lib/aeroelastic/functional';
import { withEventHandlers } from './event_handlers';
import { eventHandlers } from './event_handlers';
import { WorkpadPage as Component } from './workpad_page';
import { selectElement } from './../../state/actions/transient';
@ -31,6 +31,16 @@ const mapDispatchToProps = dispatch => {
insertNodes: pageId => selectedElements => dispatch(insertNodes(selectedElements, pageId)),
removeElements: pageId => elementIds => dispatch(removeElements(elementIds, pageId)),
selectElement: selectedElement => dispatch(selectElement(selectedElement)),
// TODO: Abstract this out. This is the same code as in sidebar/index.js
elementLayer: (pageId, selectedElement, movement) => {
dispatch(
elementLayer({
pageId,
elementId: selectedElement.id,
movement,
})
);
},
};
};
@ -84,6 +94,7 @@ export const WorkpadPage = compose(
insertNodes,
removeElements,
selectElement,
elementLayer,
}) => {
const { shapes, selectedPrimaryShapes = [], cursor } = aeroelastic.getStore(
page.id
@ -100,6 +111,7 @@ export const WorkpadPage = compose(
),
];
};
const selectedPrimaryShapeObjects = selectedPrimaryShapes.map(id =>
shapes.find(s => s.id === id)
);
@ -131,7 +143,7 @@ export const WorkpadPage = compose(
// TODO: remove this, it's a hack to force react to rerender
setUpdateCount(updateCount + 1);
},
remove: () => {
removeElements: () => {
// currently, handle the removal of one element, exploiting multiselect subsequently
if (selectedElementIds.length) {
removeElements(page.id)(selectedElementIds);
@ -150,6 +162,27 @@ export const WorkpadPage = compose(
notify.success('Copied element to clipboard');
}
},
// TODO: This is slightly different from the duplicateElements function in sidebar/index.js. Should they be doing the same thing?
// This should also be abstracted.
duplicateElements: () => {
const clonedElements = selectedElements && cloneSubgraphs(selectedElements);
if (clonedElements) {
insertNodes(page.id)(clonedElements);
if (selectedPrimaryShapes.length) {
if (selectedElements.length > 1) {
// adHocGroup branch (currently, pasting will leave only the 1st element selected, rather than forming a
// new adHocGroup - todo)
selectElement(clonedElements[0].id);
} else {
// single element or single persistentGroup branch
selectElement(
clonedElements[selectedElements.findIndex(s => s.id === selectedPrimaryShapes[0])]
.id
);
}
}
}
},
pasteElements: () => {
const { selectedElements, rootShapes } = JSON.parse(getClipboardData()) || {};
const clonedElements = selectedElements && cloneSubgraphs(selectedElements);
@ -171,10 +204,20 @@ export const WorkpadPage = compose(
}
}
},
// TODO: Same as above. Abstract these out. This is the same code as in sidebar/index.js
// Note: these layer actions only work when a single element is selected
bringForward: () =>
selectedElements.length === 1 && elementLayer(page.id, selectedElements[0], 1),
bringToFront: () =>
selectedElements.length === 1 && elementLayer(page.id, selectedElements[0], Infinity),
sendBackward: () =>
selectedElements.length === 1 && elementLayer(page.id, selectedElements[0], -1),
sendToBack: () =>
selectedElements.length === 1 && elementLayer(page.id, selectedElements[0], -Infinity),
};
}
), // Updates states; needs to have both local and global
withEventHandlers // Captures user intent, needs to have reconciled state
withHandlers(eventHandlers) // Captures user intent, needs to have reconciled state
)(Component);
WorkpadPage.propTypes = {

View file

@ -47,7 +47,13 @@ export class WorkpadPage extends PureComponent {
resetHandler: PropTypes.func,
copyElements: PropTypes.func,
cutElements: PropTypes.func,
duplicateElements: PropTypes.func,
pasteElements: PropTypes.func,
removeElements: PropTypes.func,
bringForward: PropTypes.func,
bringToFront: PropTypes.func,
sendBackward: PropTypes.func,
sendToBack: PropTypes.func,
};
componentWillUnmount() {
@ -73,22 +79,47 @@ export class WorkpadPage extends PureComponent {
onMouseUp,
onAnimationEnd,
onWheel,
removeElements,
copyElements,
cutElements,
duplicateElements,
pasteElements,
bringForward,
bringToFront,
sendBackward,
sendToBack,
} = this.props;
const keyHandler = action => {
const keyHandler = (action, event) => {
event.preventDefault();
switch (action) {
case 'COPY':
copyElements();
break;
case 'CLONE':
duplicateElements();
break;
case 'CUT':
cutElements();
break;
case 'DELETE':
removeElements();
break;
case 'PASTE':
pasteElements();
break;
case 'BRING_FORWARD':
bringForward();
break;
case 'BRING_TO_FRONT':
bringToFront();
break;
case 'SEND_BACKWARD':
sendBackward();
break;
case 'SEND_TO_BACK':
sendToBack();
break;
}
};

View file

@ -4,49 +4,70 @@
* you may not use this file except in compliance with the Elastic License.
*/
const refresh = { osx: 'option+r', windows: 'alt+r', linux: 'alt+r', other: 'alt+r' };
import { mapValues } from 'lodash';
// maps 'option' for mac and 'alt' for other OS
const getAltShortcuts = shortcuts => {
if (!Array.isArray(shortcuts)) {
shortcuts = [shortcuts];
}
const optionShortcuts = shortcuts.map(shortcut => `option+${shortcut}`);
const altShortcuts = shortcuts.map(shortcut => `alt+${shortcut}`);
return {
osx: optionShortcuts,
windows: altShortcuts,
linux: altShortcuts,
other: altShortcuts,
};
};
// maps 'command' for mac and 'ctrl' for other OS
const getCtrlShortcuts = shortcuts => {
if (!Array.isArray(shortcuts)) {
shortcuts = [shortcuts];
}
const cmdShortcuts = shortcuts.map(shortcut => `command+${shortcut}`);
const ctrlShortcuts = shortcuts.map(shortcut => `ctrl+${shortcut}`);
return {
osx: cmdShortcuts,
windows: ctrlShortcuts,
linux: ctrlShortcuts,
other: ctrlShortcuts,
};
};
const refreshShortcut = getAltShortcuts('r');
const previousPageShortcut = getAltShortcuts('[');
const nextPageShortcut = getAltShortcuts(']');
export const keymap = {
EDITOR: {
UNDO: { osx: 'command+z', windows: 'ctrl+z', linux: 'ctrl+z', other: 'ctrl+z' },
REDO: {
osx: 'command+shift+y',
windows: 'ctrl+shift+y',
linux: 'ctrl+shift+y',
other: 'ctrl+shift+y',
},
NEXT: { osx: 'option+]', windows: 'alt+]', linux: 'alt+]', other: 'alt+]' },
PREV: { osx: 'option+[', windows: 'alt+[', linux: 'alt+[', other: 'alt+[' },
FULLSCREEN: {
osx: ['option+p', 'option+f'],
windows: ['alt+p', 'alt+f'],
linux: ['alt+p', 'alt+f'],
other: ['alt+p', 'alt+f'],
},
UNDO: getCtrlShortcuts('z'),
REDO: getCtrlShortcuts('shift+z'),
PREV: previousPageShortcut,
NEXT: nextPageShortcut,
FULLSCREEN: getAltShortcuts(['p', 'f']),
FULLSCREEN_EXIT: ['escape'],
EDITING: { osx: 'option+e', windows: 'alt+e', linux: 'alt+e', other: 'alt+e' },
GRID: { osx: 'option+g', windows: 'alt+g', linux: 'alt+g', other: 'alt+g' },
REFRESH: refresh,
EDITING: getAltShortcuts('e'),
GRID: getAltShortcuts('g'),
REFRESH: refreshShortcut,
},
ELEMENT: {
COPY: { osx: 'command+c', windows: 'ctrl+c', linux: 'ctrl+c', other: 'ctrl+c' },
CUT: { osx: 'command+x', windows: 'ctrl+x', linux: 'ctrl+x', other: 'ctrl+x' },
PASTE: { osx: 'command+v', windows: 'ctrl+v', linux: 'ctrl+v', other: 'ctrl+v' },
COPY: getCtrlShortcuts('c'),
CLONE: getCtrlShortcuts('d'),
CUT: getCtrlShortcuts('x'),
PASTE: getCtrlShortcuts('v'),
DELETE: ['del', 'backspace'],
BRING_FORWARD: getCtrlShortcuts('up'),
SEND_BACKWARD: getCtrlShortcuts('down'),
BRING_TO_FRONT: getCtrlShortcuts('shift+up'),
SEND_TO_BACK: getCtrlShortcuts('shift+down'),
},
PRESENTATION: {
NEXT: {
osx: ['space', 'right', 'option+]'],
windows: ['space', 'right', 'alt+]'],
linux: ['space', 'right', 'alt+]'],
other: ['space', 'right', 'alt+]'],
},
PREV: {
osx: ['left', 'option+['],
windows: ['left', 'alt+['],
linux: ['left', 'alt+['],
other: ['left', 'alt+['],
},
REFRESH: refresh,
PREV: mapValues(previousPageShortcut, osShortcuts => osShortcuts.concat(['backspace', 'left'])),
NEXT: mapValues(nextPageShortcut, osShortcuts => osShortcuts.concat(['space', 'right'])),
REFRESH: refreshShortcut,
},
};