[Canvas] Feat: Zoom In/Out (#38832)

* Added zoomScale to transient state

* Added scaling to workpad

* Fixed transform origin

* Fixed mouse coordinate calculation

* Unscaled BorderResizeHandle and RotationHandle

* Added keyboard shortcuts

* Fixed keyboard shortcuts reference

* Updated tests for getPrettyShortcut

* Added tooltip shortcuts

* Added preventDefault to workpad shortcuts

* define interface sections

* Refactor key handler

* Added zoom context menu

* Updated zoom levels

* Fixed ts errors and tests

* Simplified mouse coordinate calculation

* Moved new files to x-pack/legacy/plugins/canvas

* Added TODOs to change icons
This commit is contained in:
Catherine Liu 2019-06-25 13:14:59 -07:00 committed by GitHub
parent a323753b5c
commit 57869c64df
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 435 additions and 105 deletions

View file

@ -27,3 +27,6 @@ export const VALID_IMAGE_TYPES = ['gif', 'jpeg', 'png', 'svg+xml'];
export const ASSET_MAX_SIZE = 25000;
export const ELEMENT_SHIFT_OFFSET = 10;
export const ELEMENT_NUDGE_OFFSET = 1;
export const ZOOM_LEVELS = [0.25, 0.33, 0.5, 0.67, 0.75, 1, 1.25, 1.5, 1.75, 2, 3, 4];
export const MIN_ZOOM_LEVEL = ZOOM_LEVELS[0];
export const MAX_ZOOM_LEVEL = ZOOM_LEVELS[ZOOM_LEVELS.length - 1];

View file

@ -12,6 +12,7 @@ import { setWorkpad } from '../../state/actions/workpad';
import { setAssets, resetAssets } from '../../state/actions/assets';
import { setPage } from '../../state/actions/pages';
import { getWorkpad } from '../../state/selectors/workpad';
import { setZoomScale } from '../../state/actions/transient';
import { WorkpadApp } from './workpad_app';
export const routes = [
@ -51,6 +52,9 @@ export const routes = [
const { assets, ...workpad } = fetchedWorkpad;
dispatch(setWorkpad(workpad));
dispatch(setAssets(assets));
// reset transient properties when changing workpads
dispatch(setZoomScale(1));
} catch (err) {
notify.error(err, { title: `Couldn't load workpad with ID` });
return router.redirectTo('home');

View file

@ -30,8 +30,10 @@ $canvasLayoutFontSize: $euiFontSizeS;
.canvasLayout__stageHeader {
flex-grow: 0;
flex-basis: auto;
padding: $euiSizeM $euiSize $euiSizeS $euiSize;
padding: ($euiSizeXS +1px) $euiSize $euiSizeXS $euiSize;
font-size: $canvasLayoutFontSize;
border-bottom: $euiBorderThin;
background: $euiColorLightestShade;
}
.canvasLayout__stageContent {
@ -60,6 +62,7 @@ $canvasLayoutFontSize: $euiFontSizeS;
background: $euiColorLightestShade;
display: flex;
position: relative;
border-left: $euiBorderThin;
.euiPanel {
margin-bottom: $euiSizeS;

View file

@ -8,10 +8,12 @@ import React from 'react';
import PropTypes from 'prop-types';
import { matrixToCSS } from '../../lib/dom';
export const BorderResizeHandle = ({ transformMatrix }) => (
export const BorderResizeHandle = ({ transformMatrix, zoomScale }) => (
<div
className="canvasBorderResizeHandle canvasLayoutAnnotation"
style={{ transform: matrixToCSS(transformMatrix) }}
style={{
transform: `${matrixToCSS(transformMatrix)} scale3d(${1 / zoomScale},${1 / zoomScale}, 1)`,
}}
/>
);

View file

@ -1096,6 +1096,92 @@ exports[`Storyshots components/KeyboardShortcutsDoc default 1`] = `
</span>
</span>
</dd>
<dt
className="euiDescriptionList__title"
>
Zoom in
</dt>
<dd
className="euiDescriptionList__description"
>
<span>
<span
className="euiCodeBlock euiCodeBlock--fontSmall euiCodeBlock--paddingLarge euiCodeBlock--inline"
style={Object {}}
>
<code
className="euiCodeBlock__code"
>
CTRL
</code>
</span>
<span
className="euiCodeBlock euiCodeBlock--fontSmall euiCodeBlock--paddingLarge euiCodeBlock--inline"
style={Object {}}
>
<code
className="euiCodeBlock__code"
>
ALT
</code>
</span>
<span
className="euiCodeBlock euiCodeBlock--fontSmall euiCodeBlock--paddingLarge euiCodeBlock--inline"
style={Object {}}
>
<code
className="euiCodeBlock__code"
>
+
</code>
</span>
</span>
</dd>
<dt
className="euiDescriptionList__title"
>
Zoom out
</dt>
<dd
className="euiDescriptionList__description"
>
<span>
<span
className="euiCodeBlock euiCodeBlock--fontSmall euiCodeBlock--paddingLarge euiCodeBlock--inline"
style={Object {}}
>
<code
className="euiCodeBlock__code"
>
CTRL
</code>
</span>
<span
className="euiCodeBlock euiCodeBlock--fontSmall euiCodeBlock--paddingLarge euiCodeBlock--inline"
style={Object {}}
>
<code
className="euiCodeBlock__code"
>
ALT
</code>
</span>
<span
className="euiCodeBlock euiCodeBlock--fontSmall euiCodeBlock--paddingLarge euiCodeBlock--inline"
style={Object {}}
>
<code
className="euiCodeBlock__code"
>
-
</code>
</span>
</span>
</dd>
</dl>
<div
className="euiSpacer euiSpacer--l"

View file

@ -42,14 +42,16 @@ const getDescriptionListItems = (shortcuts: ShortcutMap[]): DescriptionListItem[
return {
title: shortcutKeyMap.help,
description: osShortcuts.reduce((acc: JSX.Element[], shortcut, i): JSX.Element[] => {
// replace +'s with spaces so we can display the plus symbol for the plus key
shortcut = shortcut.replace(/\+/g, ' ');
if (i !== 0) {
acc.push(<span key={getId('span')}> or </span>);
}
acc.push(
<span key={getId('span')}>
{getPrettyShortcut(shortcut)
.split(/(\+)/g) // splits the array by '+' and keeps the '+'s as elements in the array
.map(key => (key === '+' ? ` ` : <EuiCode key={getId('shortcut')}>{key}</EuiCode>))}
.split(/( )/g)
.map(key => (key === ' ' ? key : <EuiCode key={getId('shortcut')}>{key}</EuiCode>))}
</span>
);
return acc;

View file

@ -8,12 +8,17 @@ import React from 'react';
import PropTypes from 'prop-types';
import { matrixToCSS } from '../../lib/dom';
export const RotationHandle = ({ transformMatrix }) => (
export const RotationHandle = ({ transformMatrix, zoomScale }) => (
<div
className="canvasRotationHandle canvasRotationHandle--connector canvasLayoutAnnotation"
style={{ transform: matrixToCSS(transformMatrix) }}
style={{
transform: matrixToCSS(transformMatrix),
}}
>
<div className="canvasRotationHandle--handle" />
<div
className="canvasRotationHandle--handle"
style={{ transform: `scale3d(${1 / zoomScale},${1 / zoomScale},1)` }}
/>
</div>
);

View file

@ -19,7 +19,7 @@
height: 9px;
width: 9px;
margin-left: -5px;
margin-top: -3px;
margin-top: -6px;
border-radius: 50%;
background-color: $euiColorMediumShade;
}

View file

@ -1,7 +1,3 @@
.canvasLayout__sidebarHeader {
padding: $euiSizeS 0;
}
.canvasContextMenu--topBorder {
border-top: $euiBorderThin;
padding: ($euiSizeXS * 0.5) 0;
}

View file

@ -10,13 +10,15 @@ import { pure, compose, withState, withProps, getContext, withHandlers } from 'r
import { transitionsRegistry } from '../../lib/transitions_registry';
import { undoHistory, redoHistory } from '../../state/actions/history';
import { fetchAllRenderables } from '../../state/actions/elements';
import { getFullscreen } from '../../state/selectors/app';
import { setZoomScale } from '../../state/actions/transient';
import { getFullscreen, getZoomScale } from '../../state/selectors/app';
import {
getSelectedPageIndex,
getAllElements,
getWorkpad,
getPages,
} from '../../state/selectors/workpad';
import { zoomHandlerCreators } from '../../lib/app_handler_creators';
import { Workpad as Component } from './workpad';
const mapStateToProps = state => {
@ -30,6 +32,7 @@ const mapStateToProps = state => {
workpadCss,
workpadId,
isFullscreen: getFullscreen(state),
zoomScale: getZoomScale(state),
};
};
@ -37,6 +40,7 @@ const mapDispatchToProps = {
undoHistory,
redoHistory,
fetchAllRenderables,
setZoomScale,
};
export const Workpad = compose(
@ -92,5 +96,6 @@ export const Workpad = compose(
const pageNumber = Math.max(1, props.selectedPageNumber - 1);
props.onPageChange(pageNumber);
},
})
}),
withHandlers(zoomHandlerCreators)
)(Component);

View file

@ -10,6 +10,7 @@ import { Shortcuts } from 'react-shortcuts';
import Style from 'style-it';
import { WorkpadPage } from '../workpad_page';
import { Fullscreen } from '../fullscreen';
import { isTextInput } from '../../lib/is_text_input';
const WORKPAD_CANVAS_BUFFER = 32; // 32px padding around the workpad
@ -35,40 +36,25 @@ export class Workpad extends React.PureComponent {
unregisterLayout: PropTypes.func.isRequired,
};
keyHandler = action => {
const {
fetchAllRenderables,
undoHistory,
redoHistory,
nextPage,
previousPage,
grid, // TODO: Get rid of grid when we improve the layout engine
setGrid,
} = this.props;
// handle keypress events for editor and presentation events
// handle keypress events for editor and presentation events
_keyMap = {
// this exists in both contexts
if (action === 'REFRESH') {
return fetchAllRenderables();
}
REFRESH: this.props.fetchAllRenderables,
// editor events
if (action === 'UNDO') {
return undoHistory();
}
if (action === 'REDO') {
return redoHistory();
}
if (action === 'GRID') {
return setGrid(!grid);
}
UNDO: this.props.undoHistory,
REDO: this.props.redoHistory,
GRID: () => this.props.setGrid(!this.props.grid),
ZOOM_IN: this.props.zoomIn,
ZOOM_OUT: this.props.zoomOut,
// presentation events
if (action === 'PREV') {
return previousPage();
}
if (action === 'NEXT') {
return nextPage();
PREV: this.props.previousPage,
NEXT: this.props.nextPage,
};
_keyHandler = (action, event) => {
if (!isTextInput(event.target)) {
event.preventDefault();
this._keyMap[action]();
}
};
@ -86,18 +72,27 @@ export class Workpad extends React.PureComponent {
isFullscreen,
registerLayout,
unregisterLayout,
zoomScale,
} = this.props;
const bufferStyle = {
height: isFullscreen ? height : height + WORKPAD_CANVAS_BUFFER,
width: isFullscreen ? width : width + WORKPAD_CANVAS_BUFFER,
height: isFullscreen ? height : (height + 2 * WORKPAD_CANVAS_BUFFER) * zoomScale,
width: isFullscreen ? width : (width + 2 * WORKPAD_CANVAS_BUFFER) * zoomScale,
};
return (
<div className="canvasWorkpad__buffer" style={bufferStyle}>
<div className="canvasCheckered" style={{ height, width }}>
<div
className="canvasCheckered"
style={{
height,
width,
transformOrigin: '0 0',
transform: isFullscreen ? undefined : `scale3d(${zoomScale}, ${zoomScale}, 1)`, // don't scale in fullscreen mode
}}
>
{!isFullscreen && (
<Shortcuts name="EDITOR" handler={this.keyHandler} targetNodeSelector="body" global />
<Shortcuts name="EDITOR" handler={this._keyHandler} targetNodeSelector="body" global />
)}
<Fullscreen>

View file

@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { compose } from 'recompose';
import { connect } from 'react-redux';
import { canUserWrite } from '../../state/selectors/app';
import { getSelectedPage, isWriteable } from '../../state/selectors/workpad';
@ -25,13 +24,11 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => ({
...stateProps,
...dispatchProps,
...ownProps,
toggleWriteable: () => dispatchProps.setWriteable(!stateProps.isWriteable),
toggleWriteable: () => setWriteable(!stateProps.isWriteable),
});
export const WorkpadHeader = compose(
connect(
mapStateToProps,
mapDispatchToProps,
mergeProps
)
export const WorkpadHeader = connect(
mapStateToProps,
mapDispatchToProps,
mergeProps
)(Component);

View file

@ -24,6 +24,7 @@ import { ControlSettings } from './control_settings';
import { RefreshControl } from './refresh_control';
import { FullscreenControl } from './fullscreen_control';
import { WorkpadExport } from './workpad_export';
import { WorkpadZoom } from './workpad_zoom';
export class WorkpadHeader extends React.PureComponent {
static propTypes = {
@ -131,6 +132,9 @@ export class WorkpadHeader extends React.PureComponent {
/>
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem>
<WorkpadZoom />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
{isWriteable ? (

View file

@ -0,0 +1,35 @@
/*
* 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 { compose, withHandlers } from 'recompose';
import { connect } from 'react-redux';
import { Dispatch } from 'redux';
// @ts-ignore unconverted local file
import { getZoomScale } from '../../../state/selectors/app';
// @ts-ignore unconverted local file
import { setZoomScale } from '../../../state/actions/transient';
import { zoomHandlerCreators } from '../../../lib/app_handler_creators';
import { WorkpadZoom as Component, Props as ComponentProps } from './workpad_zoom';
interface State {
transient: { zoomScale: number };
}
const mapStateToProps = (state: State) => ({
zoomScale: getZoomScale(state),
});
const mapDispatchToProps = (dispatch: Dispatch) => ({
setZoomScale: (scale: number) => dispatch(setZoomScale(scale)),
});
export const WorkpadZoom = compose<ComponentProps, void>(
connect(
mapStateToProps,
mapDispatchToProps
),
withHandlers(zoomHandlerCreators)
)(Component);

View file

@ -0,0 +1,11 @@
.canvasWorkpadExport__panelContent {
padding: $euiSize;
}
.canvasWorkpadExport__reportingConfig {
.euiCodeBlock__pre {
@include euiScrollBar;
overflow-x: auto;
white-space: pre;
}
}

View file

@ -0,0 +1,107 @@
/*
* 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, { MouseEventHandler, PureComponent } from 'react';
import PropTypes from 'prop-types';
import {
EuiButtonIcon,
EuiContextMenu,
EuiContextMenuPanelDescriptor,
EuiContextMenuPanelItemDescriptor,
} from '@elastic/eui';
// @ts-ignore unconverted local component
import { Popover } from '../../popover';
import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL } from '../../../../common/lib/constants';
export interface Props {
/**
* current workpad zoom level
*/
zoomScale: number;
/**
* handler to set the workpad zoom level to a specific value
*/
setZoomScale: (scale: number) => void;
/**
* handler to increase the workpad zoom level
*/
zoomIn: () => void;
/**
* handler to decrease workpad zoom level
*/
zoomOut: () => void;
}
const QUICK_ZOOM_LEVELS = [0.5, 1, 2];
export class WorkpadZoom extends PureComponent<Props> {
static propTypes = {
zoomScale: PropTypes.number.isRequired,
setZoomScale: PropTypes.func.isRequired,
zoomIn: PropTypes.func.isRequired,
zoomOut: PropTypes.func.isRequired,
};
_button = (togglePopover: MouseEventHandler<HTMLButtonElement>) => (
<EuiButtonIcon
iconType="starPlusFilled" // TODO: change this to magnifyWithPlus when available
aria-label="Share this workpad"
onClick={togglePopover}
/>
);
_getPrettyZoomLevel = (scale: number) => `${scale * 100}%`;
_getScaleMenuItems = (): EuiContextMenuPanelItemDescriptor[] =>
QUICK_ZOOM_LEVELS.map(scale => ({
name: this._getPrettyZoomLevel(scale),
icon: 'empty',
onClick: () => this.props.setZoomScale(scale),
}));
_getPanels = (): EuiContextMenuPanelDescriptor[] => {
const { zoomScale, zoomIn, zoomOut } = this.props;
const items: EuiContextMenuPanelItemDescriptor[] = [
...this._getScaleMenuItems(),
{
name: 'Zoom in',
icon: 'starPlusFilled', // TODO: change this to magnifyWithPlus when available
onClick: zoomIn,
disabled: zoomScale === MAX_ZOOM_LEVEL,
className: 'canvasContextMenu--topBorder',
},
{
name: 'Zoom out',
icon: 'starMinusFilled', // TODO: change this to magnifyWithMinus when available
onClick: zoomOut,
disabled: zoomScale === MIN_ZOOM_LEVEL,
},
];
const panels: EuiContextMenuPanelDescriptor[] = [
{
id: 0,
title: `Zoom`,
items,
},
];
return panels;
};
render() {
return (
<Popover
button={this._button}
panelPaddingSize="none"
tooltip="Zoom"
tooltipPosition="bottom"
>
{() => <EuiContextMenu initialPanelId={0} panels={this._getPanels()} />}
</Popover>
);
}
}

View file

@ -50,4 +50,5 @@ export const interactiveWorkpadPagePropTypes = {
saveCanvasOrigin: PropTypes.func.isRequired,
commit: PropTypes.func.isRequired,
setMultiplePositions: PropTypes.func.isRequired,
zoomScale: PropTypes.number.isRequired,
};

View file

@ -4,11 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
const localMousePosition = (canvasOrigin, clientX, clientY) => {
const localMousePosition = (canvasOrigin, clientX, clientY, zoomScale = 1) => {
const { left, top } = canvasOrigin();
return {
x: clientX - left,
y: clientY - top,
// commit unscaled coordinates
x: (clientX - left) / zoomScale,
y: (clientY - top) / zoomScale,
};
};
@ -17,12 +18,12 @@ const resetHandler = () => {
window.onmouseup = null;
};
const setupHandler = (commit, canvasOrigin) => {
const setupHandler = (commit, canvasOrigin, zoomScale) => {
// Ancestor has to be identified on setup, rather than 1st interaction, otherwise events may be triggered on
// DOM elements that had been removed: kibana-canvas github issue #1093
window.onmousemove = ({ buttons, clientX, clientY, altKey, metaKey, shiftKey, ctrlKey }) => {
const { x, y } = localMousePosition(canvasOrigin, clientX, clientY);
const { x, y } = localMousePosition(canvasOrigin, clientX, clientY, zoomScale);
// only commits the cursor position if there's a way to latch onto x/y calculation (canvasOrigin is knowable)
// or if left button is being held down (i.e. an element is being dragged)
if (buttons === 1 || canvasOrigin) {
@ -35,7 +36,7 @@ const setupHandler = (commit, canvasOrigin) => {
window.onmouseup = e => {
e.stopPropagation();
const { clientX, clientY, altKey, metaKey, shiftKey, ctrlKey } = e;
const { x, y } = localMousePosition(canvasOrigin, clientX, clientY);
const { x, y } = localMousePosition(canvasOrigin, clientX, clientY, zoomScale);
commit('mouseEvent', { event: 'mouseUp', x, y, altKey, metaKey, shiftKey, ctrlKey });
resetHandler();
};
@ -44,9 +45,10 @@ const setupHandler = (commit, canvasOrigin) => {
const handleMouseMove = (
commit,
{ clientX, clientY, altKey, metaKey, shiftKey, ctrlKey },
canvasOrigin
canvasOrigin,
zoomScale
) => {
const { x, y } = localMousePosition(canvasOrigin, clientX, clientY);
const { x, y } = localMousePosition(canvasOrigin, clientX, clientY, zoomScale);
if (commit) {
commit('cursorPosition', { x, y, altKey, metaKey, shiftKey, ctrlKey });
}
@ -58,21 +60,21 @@ const handleMouseLeave = (commit, { buttons }) => {
}
};
const handleMouseDown = (commit, e, canvasOrigin) => {
const handleMouseDown = (commit, e, canvasOrigin, zoomScale) => {
e.stopPropagation();
const { clientX, clientY, buttons, altKey, metaKey, shiftKey, ctrlKey } = e;
if (buttons !== 1 || !commit) {
resetHandler();
return; // left-click only
}
setupHandler(commit, canvasOrigin);
const { x, y } = localMousePosition(canvasOrigin, clientX, clientY);
setupHandler(commit, canvasOrigin, zoomScale);
const { x, y } = localMousePosition(canvasOrigin, clientX, clientY, zoomScale);
commit('mouseEvent', { event: 'mouseDown', x, y, altKey, metaKey, shiftKey, ctrlKey });
};
export const eventHandlers = {
onMouseDown: props => e => handleMouseDown(props.commit, e, props.canvasOrigin),
onMouseMove: props => e => handleMouseMove(props.commit, e, props.canvasOrigin),
onMouseDown: props => e => handleMouseDown(props.commit, e, props.canvasOrigin, props.zoomScale),
onMouseMove: props => e => handleMouseMove(props.commit, e, props.canvasOrigin, props.zoomScale),
onMouseLeave: props => e => handleMouseLeave(props.commit, e),
onWheel: props => e => handleMouseMove(props.commit, e, props.canvasOrigin),
resetHandler: () => () => resetHandler(),

View file

@ -10,7 +10,7 @@ import { createStore } from '../../../lib/aeroelastic/store';
import { updater } from '../../../lib/aeroelastic/layout';
import { getNodes, getPageById, isWriteable } from '../../../state/selectors/workpad';
import { flatten } from '../../../lib/aeroelastic/functional';
import { canUserWrite, getFullscreen } from '../../../state/selectors/app';
import { canUserWrite, getFullscreen, getZoomScale } from '../../../state/selectors/app';
import {
elementLayer,
insertNodes,
@ -113,6 +113,7 @@ const mapStateToProps = (state, ownProps) => {
selectedToplevelNodes,
selectedNodes: selectedNodeIds.map(id => nodes.find(s => s.id === id)),
pageStyle: getPageById(state, ownProps.pageId).style,
zoomScale: getZoomScale(state),
};
};

View file

@ -48,6 +48,7 @@ export class InteractiveWorkpadPage extends PureComponent {
saveCanvasOrigin,
commit,
setMultiplePositions,
zoomScale,
} = this.props;
let shortcuts = null;
@ -97,6 +98,7 @@ export class InteractiveWorkpadPage extends PureComponent {
width: node.width,
height: node.height,
text: node.text,
zoomScale,
};
switch (node.subtype) {

View file

@ -9,54 +9,70 @@ import { getPrettyShortcut } from '../get_pretty_shortcut';
describe('getPrettyShortcut', () => {
test('uppercases shortcuts', () => {
expect(getPrettyShortcut('g')).toBe('G');
expect(getPrettyShortcut('shift+click')).toBe('SHIFT+CLICK');
expect(getPrettyShortcut('shift click')).toBe('SHIFT CLICK');
expect(getPrettyShortcut('backspace')).toBe('BACKSPACE');
});
test('preserves shortcut order', () => {
expect(getPrettyShortcut('command+c')).toBe('⌘+C');
expect(getPrettyShortcut('c+command')).toBe('C+⌘');
expect(getPrettyShortcut('command c')).toBe('⌘ C');
expect(getPrettyShortcut('c command')).toBe('C ⌘');
});
test(`replaces 'command' with ⌘`, () => {
expect(getPrettyShortcut('command')).toBe('⌘');
expect(getPrettyShortcut('command+c')).toBe('⌘+C');
expect(getPrettyShortcut('command+shift+b')).toBe('⌘+SHIFT+B');
expect(getPrettyShortcut('command c')).toBe('⌘ C');
expect(getPrettyShortcut('command shift b')).toBe('⌘ SHIFT B');
});
test(`replaces 'option' with ⌥`, () => {
expect(getPrettyShortcut('option')).toBe('⌥');
expect(getPrettyShortcut('option+f')).toBe('⌥+F');
expect(getPrettyShortcut('option+shift+G')).toBe('⌥+SHIFT+G');
expect(getPrettyShortcut('command+option+shift+G')).toBe('⌘+⌥+SHIFT+G');
expect(getPrettyShortcut('option f')).toBe('⌥ F');
expect(getPrettyShortcut('option shift G')).toBe('⌥ SHIFT G');
expect(getPrettyShortcut('command option shift G')).toBe('⌘ ⌥ SHIFT G');
});
test(`replaces 'left' with ←`, () => {
expect(getPrettyShortcut('left')).toBe('←');
expect(getPrettyShortcut('command+left')).toBe('⌘+←');
expect(getPrettyShortcut('option+left')).toBe('⌥+←');
expect(getPrettyShortcut('option+shift+left')).toBe('⌥+SHIFT+←');
expect(getPrettyShortcut('command+shift+left')).toBe('⌘+SHIFT+←');
expect(getPrettyShortcut('command+option+shift+left')).toBe('⌘+⌥+SHIFT+←');
expect(getPrettyShortcut('command left')).toBe('⌘ ←');
expect(getPrettyShortcut('option left')).toBe('⌥ ←');
expect(getPrettyShortcut('option shift left')).toBe('⌥ SHIFT ←');
expect(getPrettyShortcut('command shift left')).toBe('⌘ SHIFT ←');
expect(getPrettyShortcut('command option shift left')).toBe('⌘ ⌥ SHIFT ←');
});
test(`replaces 'right' with →`, () => {
expect(getPrettyShortcut('right')).toBe('→');
expect(getPrettyShortcut('command+right')).toBe('⌘+→');
expect(getPrettyShortcut('option+right')).toBe('⌥+→');
expect(getPrettyShortcut('option+shift+right')).toBe('⌥+SHIFT+→');
expect(getPrettyShortcut('command+shift+right')).toBe('⌘+SHIFT+→');
expect(getPrettyShortcut('command+option+shift+right')).toBe('⌘+⌥+SHIFT+→');
expect(getPrettyShortcut('command right')).toBe('⌘ →');
expect(getPrettyShortcut('option right')).toBe('⌥ →');
expect(getPrettyShortcut('option shift right')).toBe('⌥ SHIFT →');
expect(getPrettyShortcut('command shift right')).toBe('⌘ SHIFT →');
expect(getPrettyShortcut('command option shift right')).toBe('⌘ ⌥ SHIFT →');
});
test(`replaces 'up' with ←`, () => {
expect(getPrettyShortcut('up')).toBe('↑');
expect(getPrettyShortcut('command+up')).toBe('⌘+↑');
expect(getPrettyShortcut('option+up')).toBe('⌥+↑');
expect(getPrettyShortcut('option+shift+up')).toBe('⌥+SHIFT+↑');
expect(getPrettyShortcut('command+shift+up')).toBe('⌘+SHIFT+↑');
expect(getPrettyShortcut('command+option+shift+up')).toBe('⌘+⌥+SHIFT+↑');
expect(getPrettyShortcut('command up')).toBe('⌘ ↑');
expect(getPrettyShortcut('option up')).toBe('⌥ ↑');
expect(getPrettyShortcut('option shift up')).toBe('⌥ SHIFT ↑');
expect(getPrettyShortcut('command shift up')).toBe('⌘ SHIFT ↑');
expect(getPrettyShortcut('command option shift up')).toBe('⌘ ⌥ SHIFT ↑');
});
test(`replaces 'down' with ↓`, () => {
expect(getPrettyShortcut('down')).toBe('↓');
expect(getPrettyShortcut('command+down')).toBe('⌘+↓');
expect(getPrettyShortcut('option+down')).toBe('⌥+↓');
expect(getPrettyShortcut('option+shift+down')).toBe('⌥+SHIFT+↓');
expect(getPrettyShortcut('command+shift+down')).toBe('⌘+SHIFT+↓');
expect(getPrettyShortcut('command+option+shift+down')).toBe('⌘+⌥+SHIFT+↓');
expect(getPrettyShortcut('command down')).toBe('⌘ ↓');
expect(getPrettyShortcut('option down')).toBe('⌥ ↓');
expect(getPrettyShortcut('option shift down')).toBe('⌥ SHIFT ↓');
expect(getPrettyShortcut('command shift down')).toBe('⌘ SHIFT ↓');
expect(getPrettyShortcut('command option shift down')).toBe('⌘ ⌥ SHIFT ↓');
});
test(`replaces 'plus' with +`, () => {
expect(getPrettyShortcut('plus')).toBe('+');
expect(getPrettyShortcut('command plus')).toBe('⌘ +');
expect(getPrettyShortcut('option plus')).toBe('⌥ +');
expect(getPrettyShortcut('option shift plus')).toBe('⌥ SHIFT +');
expect(getPrettyShortcut('command shift plus')).toBe('⌘ SHIFT +');
expect(getPrettyShortcut('command option shift plus')).toBe('⌘ ⌥ SHIFT +');
});
test(`replaces 'minus' with -`, () => {
expect(getPrettyShortcut('minus')).toBe('-');
expect(getPrettyShortcut('command minus')).toBe('⌘ -');
expect(getPrettyShortcut('option minus')).toBe('⌥ -');
expect(getPrettyShortcut('option shift minus')).toBe('⌥ SHIFT -');
expect(getPrettyShortcut('command shift minus')).toBe('⌘ SHIFT -');
expect(getPrettyShortcut('command option shift minus')).toBe('⌘ ⌥ SHIFT -');
});
});

View file

@ -0,0 +1,32 @@
/*
* 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 { ZOOM_LEVELS, MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL } from '../../common/lib/constants';
export interface Props {
/**
* current zoom level of the workpad
*/
zoomScale: number;
/**
* sets the new zoom level
*/
setZoomScale: (scale: number) => void;
}
// handlers for zooming in and out
export const zoomHandlerCreators = {
zoomIn: ({ zoomScale, setZoomScale }: Props) => (): void => {
const scaleIndex = ZOOM_LEVELS.indexOf(zoomScale);
const scaleUp =
scaleIndex + 1 < ZOOM_LEVELS.length ? ZOOM_LEVELS[scaleIndex + 1] : MAX_ZOOM_LEVEL;
setZoomScale(scaleUp);
},
zoomOut: ({ zoomScale, setZoomScale }: Props) => (): void => {
const scaleIndex = ZOOM_LEVELS.indexOf(zoomScale);
const scaleDown = scaleIndex - 1 >= 0 ? ZOOM_LEVELS[scaleIndex - 1] : MIN_ZOOM_LEVEL;
setZoomScale(scaleDown);
},
};

View file

@ -16,6 +16,8 @@ export const getPrettyShortcut = (shortcut: string): string => {
result = result.replace(/right/i, '→');
result = result.replace(/up/i, '↑');
result = result.replace(/down/i, '↓');
result = result.replace(/plus/i, '+');
result = result.replace(/minus/i, '-');
return result;
};

View file

@ -40,23 +40,23 @@ const getShortcuts = (
modifiers = [modifiers];
}
let macShortcuts = shortcuts;
let macShortcuts = [...shortcuts];
// handle shift modifier
if (modifiers.includes('shift')) {
macShortcuts = shortcuts.map(shortcut => `shift+${shortcut}`);
macShortcuts = macShortcuts.map(shortcut => `shift+${shortcut}`);
shortcuts = shortcuts.map(shortcut => `shift+${shortcut}`);
}
// handle alt modifier
if (modifiers.includes('alt') || modifiers.includes('option')) {
macShortcuts = shortcuts.map(shortcut => `option+${shortcut}`);
macShortcuts = macShortcuts.map(shortcut => `option+${shortcut}`);
shortcuts = shortcuts.map(shortcut => `alt+${shortcut}`);
}
// handle ctrl modifier
if (modifiers.includes('ctrl') || modifiers.includes('command')) {
macShortcuts = shortcuts.map(shortcut => `command+${shortcut}`);
macShortcuts = macShortcuts.map(shortcut => `command+${shortcut}`);
shortcuts = shortcuts.map(shortcut => `ctrl+${shortcut}`);
}
@ -138,6 +138,8 @@ export const keymap: KeyMap = {
EDITING: getShortcuts('e', { modifiers: 'alt', help: 'Toggle edit mode' }),
GRID: getShortcuts('g', { modifiers: 'alt', help: 'Show grid' }),
REFRESH: refreshShortcut,
ZOOM_IN: getShortcuts('plus', { modifiers: ['ctrl', 'alt'], help: 'Zoom in' }),
ZOOM_OUT: getShortcuts('minus', { modifiers: ['ctrl', 'alt'], help: 'Zoom out' }),
},
PRESENTATION: {
displayName: 'Presentation controls',

View file

@ -10,3 +10,4 @@ export const setFullscreen = createAction('setFullscreen');
export const selectToplevelNodes = createAction('selectToplevelNodes');
export const setFirstLoad = createAction('setFirstLoad');
export const setElementStats = createAction('setElementStats');
export const setZoomScale = createAction('setZoomScale');

View file

@ -14,6 +14,7 @@ export const getInitialState = path => {
assets: {}, // assets end up here
transient: {
canUserWrite: capabilities.get().canvas.save,
zoomScale: 1,
elementStats: {
total: 0,
ready: 0,

View file

@ -48,6 +48,13 @@ export const transientReducer = handleActions(
};
},
[transientActions.setZoomScale]: (transientState, { payload }) => {
return {
...transientState,
zoomScale: payload || 1,
};
},
[pageActions.setPage]: transientState => {
return { ...transientState, selectedToplevelNodes: [] };
},

View file

@ -15,6 +15,10 @@ export function getFullscreen(state) {
return get(state, 'transient.fullscreen', false);
}
export function getZoomScale(state) {
return get(state, 'transient.zoomScale', 1);
}
export function getServerFunctions(state) {
return get(state, 'app.serverFunctions');
}

View file

@ -36,6 +36,10 @@ $canvasElementCardWidth: 210px;
max-width: 100%;
}
.canvasContextMenu--topBorder {
border-top: $euiBorderThin;
}
#canvas-app {
overflow-y: hidden;