Skip to content

Commit

Permalink
[Canvas] Updates keyboard shortcuts (#29394) (#31213)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
cqliu1 authored Feb 14, 2019
1 parent 8a6d5ba commit 417541e
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 58 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -154,19 +152,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
});
}
};

Expand All @@ -189,12 +182,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(),
});
};
53 changes: 48 additions & 5 deletions x-pack/plugins/canvas/public/components/workpad_page/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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,
})
);
},
};
};

Expand Down Expand Up @@ -84,6 +94,7 @@ export const WorkpadPage = compose(
insertNodes,
removeElements,
selectElement,
elementLayer,
}) => {
const { shapes, selectedPrimaryShapes = [], cursor } = aeroelastic.getStore(
page.id
Expand All @@ -100,6 +111,7 @@ export const WorkpadPage = compose(
),
];
};

const selectedPrimaryShapeObjects = selectedPrimaryShapes.map(id =>
shapes.find(s => s.id === id)
);
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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 = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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;
}
};

Expand Down
91 changes: 56 additions & 35 deletions x-pack/plugins/canvas/public/lib/keymap.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
};

0 comments on commit 417541e

Please sign in to comment.