diff --git a/x-pack/plugins/canvas/public/app.js b/x-pack/plugins/canvas/public/app.js index 51d60146b075f6..30bf9e90e8abc1 100644 --- a/x-pack/plugins/canvas/public/app.js +++ b/x-pack/plugins/canvas/public/app.js @@ -8,6 +8,8 @@ import 'ui/autoload/all'; import chrome from 'ui/chrome'; import './angular/config'; import './angular/services'; +import React from 'react'; +import ReactDOM from 'react-dom'; import { CanvasRootController } from './angular/controllers'; // Import the uiExports that the application uses @@ -24,5 +26,12 @@ import './lib/load_expression_types'; import './lib/load_transitions'; import 'uiExports/canvas'; +import { HelpMenu } from './components/help_menu/help_menu'; + // load the application chrome.setRootController('canvas', CanvasRootController); + +// add Canvas docs to help menu in global nav +chrome.helpExtension.set(domNode => { + ReactDOM.render(, domNode); +}); diff --git a/x-pack/plugins/canvas/public/components/fullscreen_control/fullscreen_control.js b/x-pack/plugins/canvas/public/components/fullscreen_control/fullscreen_control.js index 7985e468d8f57b..5a4f73775f6ca7 100644 --- a/x-pack/plugins/canvas/public/components/fullscreen_control/fullscreen_control.js +++ b/x-pack/plugins/canvas/public/components/fullscreen_control/fullscreen_control.js @@ -25,7 +25,13 @@ export class FullscreenControl extends React.PureComponent { return ( - + {children({ isFullscreen, toggleFullscreen: this.toggleFullscreen })} ); diff --git a/x-pack/plugins/canvas/public/components/help_menu/help_menu.js b/x-pack/plugins/canvas/public/components/help_menu/help_menu.js new file mode 100644 index 00000000000000..528545360a2baf --- /dev/null +++ b/x-pack/plugins/canvas/public/components/help_menu/help_menu.js @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, PureComponent } from 'react'; +import { EuiButton, EuiHorizontalRule, EuiText, EuiSpacer, EuiPortal } from '@elastic/eui'; +import { documentationLinks } from '../../lib/documentation_links'; +import { KeyboardShortcutsDoc } from '../keyboard_shortcuts_doc'; + +export class HelpMenu extends PureComponent { + state = { isFlyoutVisible: false }; + + showFlyout = () => { + this.setState({ isFlyoutVisible: true }); + }; + + hideFlyout = () => { + this.setState({ isFlyoutVisible: false }); + }; + + render() { + return ( + + + + +

For Canvas specific information

+
+ + + Canvas documentation + + + + Keyboard shortcuts + + + {this.state.isFlyoutVisible && ( + + + + )} +
+ ); + } +} diff --git a/x-pack/plugins/canvas/public/components/help_menu/index.js b/x-pack/plugins/canvas/public/components/help_menu/index.js new file mode 100644 index 00000000000000..47682169c51e03 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/help_menu/index.js @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { HelpMenu } from './help_menu'; diff --git a/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/index.js b/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/index.js new file mode 100644 index 00000000000000..8ee3da4fc35a91 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/index.js @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { KeyboardShortcutsDoc } from './keyboard_shortcuts_doc'; diff --git a/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/keyboard_shortcuts_doc.js b/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/keyboard_shortcuts_doc.js new file mode 100644 index 00000000000000..dca6013de241d0 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/keyboard_shortcuts_doc.js @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiDescriptionList, + EuiHorizontalRule, + EuiCode, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; +import { keymap } from '../../lib/keymap'; +import { getClientPlatform } from '../../lib/get_client_platform'; +import { getId } from '../../lib/get_id'; + +const getPrettyShortcut = shortcut => { + if (!shortcut) { + return ''; + } + + let result = shortcut.replace(/command/i, '⌘'); + result = result.replace(/option/i, '⌥'); + result = result.replace(/left/i, '←'); + result = result.replace(/right/i, '→'); + result = result.replace(/up/i, '↑'); + result = result.replace(/down/i, '↓'); + + return ( + + {result + .split(/(\+)/g) //splits the array by '+' and keeps the '+'s as elements in the array + .map(key => (key === '+' ? ` ${key} ` : {key}))} + + ); +}; + +const getDescriptionListItems = shortcuts => + Object.values(shortcuts).map(shortcutKeyMap => { + const os = getClientPlatform(); + const osShortcuts = shortcutKeyMap[os]; + return { + title: shortcutKeyMap.help, + description: osShortcuts.reduce((acc, shortcut, i) => { + if (i !== 0) { + acc.push(' or '); + } + acc.push(getPrettyShortcut(shortcut)); + return acc; + }, []), + }; + }); + +export const KeyboardShortcutsDoc = props => ( + + + +

Keyboard Shortcuts

+
+
+ + {Object.values(keymap).map(namespace => { + const { displayName, ...shortcuts } = namespace; + return ( +
+ +

{displayName}

+
+ + + +
+ ); + })} +
+
+); + +KeyboardShortcutsDoc.propTypes = { + onClose: PropTypes.func.isRequired, +}; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.js b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.js index 88f92f7eb69e6d..507f7a1a90d989 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.js +++ b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.js @@ -80,7 +80,7 @@ export const WorkpadHeader = ({ {({ toggleFullscreen }) => ( - + { const tabs = [ @@ -57,11 +55,6 @@ export const WorkpadManager = ({ onClose }) => { tooltipContent="Canvas is still in beta. Please help us improve by reporting issues or bugs in the Kibana repo." /> - - - Docs - - diff --git a/x-pack/plugins/canvas/public/components/workpad_page/event_handlers.js b/x-pack/plugins/canvas/public/components/workpad_page/event_handlers.js index 389a703eeaa4e2..27c534f5270e23 100644 --- a/x-pack/plugins/canvas/public/components/workpad_page/event_handlers.js +++ b/x-pack/plugins/canvas/public/components/workpad_page/event_handlers.js @@ -124,70 +124,10 @@ const handleMouseDown = (commit, e, isEditable) => { ); }; -const keyCode = key => (key === 'Meta' ? 'MetaLeft' : 'Key' + key.toUpperCase()); - -const isTextInput = ({ tagName, type }) => { - // input types that aren't variations of text input - const nonTextInputs = [ - 'button', - 'checkbox', - 'color', - 'file', - 'image', - 'radio', - 'range', - 'reset', - 'submit', - ]; - - switch (tagName.toLowerCase()) { - case 'input': - return !nonTextInputs.includes(type); - case 'textarea': - return true; - default: - return false; - } -}; - -const modifierKey = key => ['KeyALT', 'KeyCONTROL'].indexOf(keyCode(key)) > -1; - -const handleKeyDown = (commit, e, isEditable) => { - const { key } = e; - - if (isEditable && !modifierKey(key)) { - commit('keyboardEvent', { - event: 'keyDown', - code: keyCode(key), // convert to standard event code - }); - } -}; - -const handleKeyPress = (commit, e, isEditable) => { - const { key, target } = e; - const upcaseKey = key && key.toUpperCase(); - if (isEditable && !isTextInput(target) && 'GU'.indexOf(upcaseKey) !== -1) { - commit('actionEvent', { - event: upcaseKey === 'G' ? 'group' : 'ungroup', - }); - } -}; - -const handleKeyUp = (commit, { key }, isEditable) => { - if (isEditable && !modifierKey(key)) { - commit('keyboardEvent', { - event: 'keyUp', - code: keyCode(key), // convert to standard event code - }); - } -}; - 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), - onKeyPress: props => e => handleKeyPress(props.commit, e, props.isEditable), - onKeyUp: props => e => handleKeyUp(props.commit, e, props.isEditable), + onKeyDown: props => () => props.commit('keyboardEvent'), // dummy event onWheel: props => e => handleWheel(props.commit, e, props.isEditable), resetHandler: () => () => resetHandler(), }; diff --git a/x-pack/plugins/canvas/public/components/workpad_page/index.js b/x-pack/plugins/canvas/public/components/workpad_page/index.js index 326860e687d80b..03da9afa4eb9dd 100644 --- a/x-pack/plugins/canvas/public/components/workpad_page/index.js +++ b/x-pack/plugins/canvas/public/components/workpad_page/index.js @@ -112,9 +112,10 @@ export const WorkpadPage = compose( ]; }; - const selectedPrimaryShapeObjects = selectedPrimaryShapes.map(id => - shapes.find(s => s.id === id) - ); + const selectedPrimaryShapeObjects = selectedPrimaryShapes + .map(id => shapes.find(s => s.id === id)) + .filter(shape => shape); + const selectedPersistentPrimaryShapes = flatten( selectedPrimaryShapeObjects.map(shape => shape.subtype === 'adHocGroup' @@ -217,6 +218,16 @@ export const WorkpadPage = compose( }; } ), // Updates states; needs to have both local and global + withHandlers({ + groupElements: ({ commit }) => () => + commit('actionEvent', { + event: 'group', + }), + ungroupElements: ({ commit }) => () => + commit('actionEvent', { + event: 'ungroup', + }), + }), withHandlers(eventHandlers) // Captures user intent, needs to have reconciled state )(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_page/workpad_page.js b/x-pack/plugins/canvas/public/components/workpad_page/workpad_page.js index 44c5ef922f2b47..48de8e087a0426 100644 --- a/x-pack/plugins/canvas/public/components/workpad_page/workpad_page.js +++ b/x-pack/plugins/canvas/public/components/workpad_page/workpad_page.js @@ -14,6 +14,7 @@ import { TooltipAnnotation } from '../tooltip_annotation'; import { RotationHandle } from '../rotation_handle'; import { BorderConnection } from '../border_connection'; import { BorderResizeHandle } from '../border_resize_handle'; +import { isTextInput } from '../../lib/is_text_input'; // NOTE: the data-shared-* attributes here are used for reporting export class WorkpadPage extends PureComponent { @@ -39,7 +40,6 @@ export class WorkpadPage extends PureComponent { isEditable: PropTypes.bool.isRequired, onDoubleClick: PropTypes.func, onKeyDown: PropTypes.func, - onKeyUp: PropTypes.func, onMouseDown: PropTypes.func, onMouseMove: PropTypes.func, onMouseUp: PropTypes.func, @@ -72,8 +72,6 @@ export class WorkpadPage extends PureComponent { isEditable, onDoubleClick, onKeyDown, - onKeyPress, - onKeyUp, onMouseDown, onMouseMove, onMouseUp, @@ -88,38 +86,48 @@ export class WorkpadPage extends PureComponent { bringToFront, sendBackward, sendToBack, + groupElements, + ungroupElements, } = this.props; 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; + if (!isTextInput(event.target)) { + 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; + case 'GROUP': + groupElements(); + break; + case 'UNGROUP': + ungroupElements(); + break; + } } }; @@ -137,16 +145,13 @@ export class WorkpadPage extends PureComponent { width, cursor, }} + onKeyDown={onKeyDown} onMouseMove={onMouseMove} onMouseUp={onMouseUp} onMouseDown={onMouseDown} - onKeyDown={onKeyDown} - onKeyPress={onKeyPress} - onKeyUp={onKeyUp} onDoubleClick={onDoubleClick} onAnimationEnd={onAnimationEnd} onWheel={onWheel} - tabIndex={0} // needed to capture keyboard events; focusing is also needed but React apparently does so implicitly > {isEditable && ( { + const platform = navigator.platform.toLowerCase(); + if (platform.indexOf('mac') >= 0) { + return 'osx'; + } + if (platform.indexOf('win') >= 0) { + return 'windows'; + } + if (platform.indexOf('linux') >= 0) { + return 'linux'; + } + return 'other'; +}; diff --git a/x-pack/plugins/canvas/public/lib/is_text_input.js b/x-pack/plugins/canvas/public/lib/is_text_input.js new file mode 100644 index 00000000000000..b61a5df0f4c88b --- /dev/null +++ b/x-pack/plugins/canvas/public/lib/is_text_input.js @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// input types that aren't variations of text input +const nonTextInputs = [ + 'button', + 'checkbox', + 'color', + 'file', + 'image', + 'radio', + 'range', + 'reset', + 'submit', +]; + +export const isTextInput = ({ tagName, type }) => { + switch (tagName.toLowerCase()) { + case 'input': + return !nonTextInputs.includes(type); + case 'textarea': + return true; + default: + return false; + } +}; diff --git a/x-pack/plugins/canvas/public/lib/keymap.js b/x-pack/plugins/canvas/public/lib/keymap.js index 88a553fe8a6ba4..6f410e3422dd7c 100644 --- a/x-pack/plugins/canvas/public/lib/keymap.js +++ b/x-pack/plugins/canvas/public/lib/keymap.js @@ -38,36 +38,81 @@ const getCtrlShortcuts = shortcuts => { }; }; -const refreshShortcut = getAltShortcuts('r'); -const previousPageShortcut = getAltShortcuts('['); -const nextPageShortcut = getAltShortcuts(']'); +const refreshShortcut = { ...getAltShortcuts('r'), help: 'Refresh workpad' }; +const previousPageShortcut = { ...getAltShortcuts('['), help: 'Go to previous page' }; +const nextPageShortcut = { ...getAltShortcuts(']'), help: 'Go to next page' }; export const keymap = { + ELEMENT: { + displayName: 'Element controls', + COPY: { ...getCtrlShortcuts('c'), help: 'Copy' }, + CLONE: { ...getCtrlShortcuts('d'), help: 'Clone' }, + CUT: { ...getCtrlShortcuts('x'), help: 'Cut' }, + PASTE: { ...getCtrlShortcuts('v'), help: 'Paste' }, + DELETE: { + osx: ['backspace'], + windows: ['del', 'backspace'], + linux: ['del', 'backspace'], + other: ['del', 'backspace'], + help: 'Delete', + }, + BRING_FORWARD: { + ...getCtrlShortcuts('up'), + help: 'Send forward', + }, + BRING_TO_FRONT: { + ...getCtrlShortcuts('shift+up'), + help: 'Send to front', + }, + SEND_BACKWARD: { + ...getCtrlShortcuts('down'), + help: 'Send backward', + }, + SEND_TO_BACK: { + ...getCtrlShortcuts('shift+down'), + help: 'Send to back', + }, + GROUP: { + osx: ['g'], + windows: ['g'], + linux: ['g'], + other: ['g'], + help: 'Group', + }, + UNGROUP: { + osx: ['u'], + windows: ['u'], + linux: ['u'], + other: ['u'], + help: 'Ungroup', + }, + }, EDITOR: { - UNDO: getCtrlShortcuts('z'), - REDO: getCtrlShortcuts('shift+z'), + displayName: 'Editor controls', + UNDO: { ...getCtrlShortcuts('z'), help: 'Undo last action' }, + REDO: { ...getCtrlShortcuts('shift+z'), help: 'Redo last action' }, PREV: previousPageShortcut, NEXT: nextPageShortcut, - FULLSCREEN: getAltShortcuts(['p', 'f']), - FULLSCREEN_EXIT: ['escape'], - EDITING: getAltShortcuts('e'), - GRID: getAltShortcuts('g'), + EDITING: { ...getAltShortcuts('e'), help: 'Toggle edit mode' }, + GRID: { ...getAltShortcuts('g'), help: 'Show grid' }, REFRESH: refreshShortcut, }, - ELEMENT: { - 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: { - PREV: mapValues(previousPageShortcut, osShortcuts => osShortcuts.concat(['backspace', 'left'])), - NEXT: mapValues(nextPageShortcut, osShortcuts => osShortcuts.concat(['space', 'right'])), + displayName: 'Presentation mode', + FULLSCREEN: { ...getAltShortcuts(['p', 'f']), help: 'Enter presentation mode' }, + FULLSCREEN_EXIT: { + osx: ['esc'], + windows: ['esc'], + linux: ['esc'], + other: ['esc'], + help: 'Exit presentation mode', + }, + PREV: mapValues(previousPageShortcut, (osShortcuts, key) => + key === 'help' ? osShortcuts : osShortcuts.concat(['backspace', 'left']) + ), + NEXT: mapValues(nextPageShortcut, (osShortcuts, key) => + key === 'help' ? osShortcuts : osShortcuts.concat(['space', 'right']) + ), REFRESH: refreshShortcut, }, };