[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:
parent
a323753b5c
commit
57869c64df
|
@ -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];
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
height: 9px;
|
||||
width: 9px;
|
||||
margin-left: -5px;
|
||||
margin-top: -3px;
|
||||
margin-top: -6px;
|
||||
border-radius: 50%;
|
||||
background-color: $euiColorMediumShade;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
.canvasLayout__sidebarHeader {
|
||||
padding: $euiSizeS 0;
|
||||
}
|
||||
|
||||
.canvasContextMenu--topBorder {
|
||||
border-top: $euiBorderThin;
|
||||
padding: ($euiSizeXS * 0.5) 0;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 ? (
|
||||
|
|
|
@ -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);
|
|
@ -0,0 +1,11 @@
|
|||
.canvasWorkpadExport__panelContent {
|
||||
padding: $euiSize;
|
||||
}
|
||||
|
||||
.canvasWorkpadExport__reportingConfig {
|
||||
.euiCodeBlock__pre {
|
||||
@include euiScrollBar;
|
||||
overflow-x: auto;
|
||||
white-space: pre;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -50,4 +50,5 @@ export const interactiveWorkpadPagePropTypes = {
|
|||
saveCanvasOrigin: PropTypes.func.isRequired,
|
||||
commit: PropTypes.func.isRequired,
|
||||
setMultiplePositions: PropTypes.func.isRequired,
|
||||
zoomScale: PropTypes.number.isRequired,
|
||||
};
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 -');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
};
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -48,6 +48,13 @@ export const transientReducer = handleActions(
|
|||
};
|
||||
},
|
||||
|
||||
[transientActions.setZoomScale]: (transientState, { payload }) => {
|
||||
return {
|
||||
...transientState,
|
||||
zoomScale: payload || 1,
|
||||
};
|
||||
},
|
||||
|
||||
[pageActions.setPage]: transientState => {
|
||||
return { ...transientState, selectedToplevelNodes: [] };
|
||||
},
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -36,6 +36,10 @@ $canvasElementCardWidth: 210px;
|
|||
max-width: 100%;
|
||||
}
|
||||
|
||||
.canvasContextMenu--topBorder {
|
||||
border-top: $euiBorderThin;
|
||||
}
|
||||
|
||||
#canvas-app {
|
||||
overflow-y: hidden;
|
||||
|
||||
|
|
Loading…
Reference in a new issue