[Canvas] Feat: Fit Workpad to Window (#39864)

* Added fit to window zoom option

Added reset zoom option

Added divider between zoom and reset options

* Fixed zoom in/out from fit to window scale

* PR feedback

* Fixed fit to window with elements way off the workpad

* Ensure zoom level array is always in ascending order

* Updated storyshot
This commit is contained in:
Catherine Liu 2019-06-28 16:17:55 -07:00 committed by GitHub
parent d142171366
commit af56c9b732
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 180 additions and 19 deletions

View file

@ -27,6 +27,8 @@ 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 ZOOM_LEVELS = [0.25, 0.33, 0.5, 0.67, 0.75, 1, 1.25, 1.5, 1.75, 2, 3, 4].sort();
export const MIN_ZOOM_LEVEL = ZOOM_LEVELS[0];
export const MAX_ZOOM_LEVEL = ZOOM_LEVELS[ZOOM_LEVELS.length - 1];
export const WORKPAD_CANVAS_BUFFER = 32; // 32px padding around the workpad
export const CANVAS_LAYOUT_STAGE_CONTENT_SELECTOR = `canvasLayout__stageContent`;

View file

@ -10,6 +10,7 @@ import { Sidebar } from '../../../components/sidebar';
import { Toolbar } from '../../../components/toolbar';
import { Workpad } from '../../../components/workpad';
import { WorkpadHeader } from '../../../components/workpad_header';
import { CANVAS_LAYOUT_STAGE_CONTENT_SELECTOR } from '../../../../common/lib/constants';
export const WORKPAD_CONTAINER_ID = 'canvasWorkpadContainer';
@ -45,7 +46,11 @@ export class WorkpadApp extends React.PureComponent {
<WorkpadHeader />
</div>
<div className="canvasLayout__stageContent" onMouseDown={deselectElement}>
<div
id={CANVAS_LAYOUT_STAGE_CONTENT_SELECTOR}
className={CANVAS_LAYOUT_STAGE_CONTENT_SELECTOR}
onMouseDown={deselectElement}
>
{/* NOTE: canvasWorkpadContainer is used for exporting */}
<div
id={WORKPAD_CONTAINER_ID}

View file

@ -1182,6 +1182,49 @@ exports[`Storyshots components/KeyboardShortcutsDoc default 1`] = `
</span>
</span>
</dd>
<dt
className="euiDescriptionList__title"
>
Reset zoom to 100%
</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

@ -11,8 +11,7 @@ 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
import { WORKPAD_CANVAS_BUFFER } from '../../../common/lib/constants';
export class Workpad extends React.PureComponent {
static propTypes = {
@ -34,6 +33,9 @@ export class Workpad extends React.PureComponent {
fetchAllRenderables: PropTypes.func.isRequired,
registerLayout: PropTypes.func.isRequired,
unregisterLayout: PropTypes.func.isRequired,
zoomIn: PropTypes.func.isRequired,
zoomOut: PropTypes.func.isRequired,
resetZoom: PropTypes.func.isRequired,
};
// handle keypress events for editor and presentation events
@ -46,6 +48,7 @@ export class Workpad extends React.PureComponent {
GRID: () => this.props.setGrid(!this.props.grid),
ZOOM_IN: this.props.zoomIn,
ZOOM_OUT: this.props.zoomOut,
ZOOM_RESET: this.props.resetZoom,
// presentation events
PREV: this.props.previousPage,
NEXT: this.props.nextPage,
@ -103,8 +106,6 @@ export class Workpad extends React.PureComponent {
transform: `scale3d(${scale}, ${scale}, 1)`,
WebkitTransform: `scale3d(${scale}, ${scale}, 1)`,
msTransform: `scale3d(${scale}, ${scale}, 1)`,
// height,
// width,
height: windowSize.height < height ? 'auto' : height,
width: windowSize.width < width ? 'auto' : width,
}

View file

@ -110,6 +110,9 @@ export class WorkpadHeader extends React.PureComponent {
<EuiFlexItem grow={false}>
<FullscreenControl>{this._fullscreenButton}</FullscreenControl>
</EuiFlexItem>
<EuiFlexItem>
<WorkpadZoom />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<WorkpadExport />
</EuiFlexItem>
@ -132,9 +135,6 @@ export class WorkpadHeader extends React.PureComponent {
/>
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem>
<WorkpadZoom />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
{isWriteable ? (

View file

@ -9,6 +9,10 @@ import { connect } from 'react-redux';
import { Dispatch } from 'redux';
// @ts-ignore unconverted local file
import { getZoomScale } from '../../../state/selectors/app';
import {
getWorkpadBoundingBox,
// @ts-ignore unconverted local file
} from '../../../state/selectors/workpad';
// @ts-ignore unconverted local file
import { setZoomScale } from '../../../state/actions/transient';
import { zoomHandlerCreators } from '../../../lib/app_handler_creators';
@ -20,6 +24,7 @@ interface State {
const mapStateToProps = (state: State) => ({
zoomScale: getZoomScale(state),
boundingBox: getWorkpadBoundingBox(state),
});
const mapDispatchToProps = (dispatch: Dispatch) => ({

View file

@ -14,13 +14,22 @@ import {
} from '@elastic/eui';
// @ts-ignore unconverted local component
import { Popover } from '../../popover';
import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL } from '../../../../common/lib/constants';
import {
MAX_ZOOM_LEVEL,
MIN_ZOOM_LEVEL,
WORKPAD_CANVAS_BUFFER,
CANVAS_LAYOUT_STAGE_CONTENT_SELECTOR,
} from '../../../../common/lib/constants';
export interface Props {
/**
* current workpad zoom level
*/
zoomScale: number;
/**
* minimum bounding box for the workpad
*/
boundingBox: { left: number; right: number; top: number; bottom: number };
/**
* handler to set the workpad zoom level to a specific value
*/
@ -33,6 +42,10 @@ export interface Props {
* handler to decrease workpad zoom level
*/
zoomOut: () => void;
/**
* reset zoom to 100%
*/
resetZoom: () => void;
}
const QUICK_ZOOM_LEVELS = [0.5, 1, 2];
@ -43,11 +56,39 @@ export class WorkpadZoom extends PureComponent<Props> {
setZoomScale: PropTypes.func.isRequired,
zoomIn: PropTypes.func.isRequired,
zoomOut: PropTypes.func.isRequired,
resetZoom: PropTypes.func.isRequired,
boundingBox: PropTypes.shape({
left: PropTypes.number.isRequired,
right: PropTypes.number.isRequired,
top: PropTypes.number.isRequired,
bottom: PropTypes.number.isRequired,
}),
};
_fitToWindow = () => {
const { boundingBox, setZoomScale } = this.props;
const canvasLayoutContent = document.querySelector(
`#${CANVAS_LAYOUT_STAGE_CONTENT_SELECTOR}`
) as HTMLElement;
const layoutWidth = canvasLayoutContent.clientWidth;
const layoutHeight = canvasLayoutContent.clientHeight;
const boundingWidth =
Math.max(layoutWidth, boundingBox.right) -
Math.min(0, boundingBox.left) +
WORKPAD_CANVAS_BUFFER * 2;
const boundingHeight =
Math.max(layoutHeight, boundingBox.bottom) -
Math.min(0, boundingBox.top) +
WORKPAD_CANVAS_BUFFER * 2;
const xScale = layoutWidth / boundingWidth;
const yScale = layoutHeight / boundingHeight;
setZoomScale(Math.min(xScale, yScale));
};
_button = (togglePopover: MouseEventHandler<HTMLButtonElement>) => (
<EuiButtonIcon
iconType="starPlusFilled" // TODO: change this to magnifyWithPlus when available
iconType="magnifyWithPlus"
aria-label="Share this workpad"
onClick={togglePopover}
/>
@ -63,21 +104,34 @@ export class WorkpadZoom extends PureComponent<Props> {
}));
_getPanels = (): EuiContextMenuPanelDescriptor[] => {
const { zoomScale, zoomIn, zoomOut } = this.props;
const { zoomScale, zoomIn, zoomOut, resetZoom } = this.props;
const items: EuiContextMenuPanelItemDescriptor[] = [
{
name: 'Fit to window',
icon: 'empty',
onClick: this._fitToWindow,
disabled: zoomScale === MAX_ZOOM_LEVEL,
},
...this._getScaleMenuItems(),
{
name: 'Zoom in',
icon: 'starPlusFilled', // TODO: change this to magnifyWithPlus when available
icon: 'magnifyWithPlus',
onClick: zoomIn,
disabled: zoomScale === MAX_ZOOM_LEVEL,
className: 'canvasContextMenu--topBorder',
},
{
name: 'Zoom out',
icon: 'starMinusFilled', // TODO: change this to magnifyWithMinus when available
icon: 'magnifyWithMinus',
onClick: zoomOut,
disabled: zoomScale === MIN_ZOOM_LEVEL,
disabled: zoomScale <= MIN_ZOOM_LEVEL,
},
{
name: 'Reset',
icon: 'empty',
onClick: resetZoom,
disabled: zoomScale >= MAX_ZOOM_LEVEL,
className: 'canvasContextMenu--topBorder',
},
];

View file

@ -10,6 +10,10 @@ export interface Props {
* current zoom level of the workpad
*/
zoomScale: number;
/**
* zoom level to scale workpad to fit into the viewport
*/
fitZoomScale: number;
/**
* sets the new zoom level
*/
@ -19,14 +23,18 @@ export interface Props {
// 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;
ZOOM_LEVELS.find((zoomLevel: number) => zoomScale < zoomLevel) || 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;
const scaleDown =
ZOOM_LEVELS.slice()
.reverse()
.find((zoomLevel: number) => zoomScale > zoomLevel) || MIN_ZOOM_LEVEL;
setZoomScale(scaleDown);
},
resetZoom: ({ setZoomScale }: Props) => (): void => {
setZoomScale(1);
},
};

View file

@ -140,6 +140,7 @@ export const keymap: KeyMap = {
REFRESH: refreshShortcut,
ZOOM_IN: getShortcuts('plus', { modifiers: ['ctrl', 'alt'], help: 'Zoom in' }),
ZOOM_OUT: getShortcuts('minus', { modifiers: ['ctrl', 'alt'], help: 'Zoom out' }),
ZOOM_RESET: getShortcuts('[', { modifiers: ['ctrl', 'alt'], help: 'Reset zoom to 100%' }),
},
PRESENTATION: {
displayName: 'Presentation controls',

View file

@ -71,6 +71,48 @@ export function getWorkpadName(state) {
return get(state, append(workpadRoot, 'name'));
}
export function getWorkpadHeight(state) {
return get(state, append(workpadRoot, 'height'));
}
export function getWorkpadWidth(state) {
return get(state, append(workpadRoot, 'width'));
}
export function getWorkpadBoundingBox(state) {
return getPages(state).reduce(
(boundingBox, page) => {
page.elements.forEach(({ position }) => {
const { left, top, width, height } = position;
const right = left + width;
const bottom = top + height;
if (left < boundingBox.left) {
boundingBox.left = left;
}
if (top < boundingBox.top) {
boundingBox.top = top;
}
if (right > boundingBox.right) {
boundingBox.right = right;
}
if (bottom > boundingBox.bottom) {
boundingBox.bottom = bottom;
}
});
return boundingBox;
},
{
left: 0,
right: getWorkpadWidth(state),
top: 0,
bottom: getWorkpadHeight(state),
}
);
}
export function getWorkpadColors(state) {
return get(state, append(workpadRoot, 'colors'));
}