mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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:
parent
d142171366
commit
af56c9b732
10 changed files with 180 additions and 19 deletions
|
@ -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`;
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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 ? (
|
||||
|
|
|
@ -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) => ({
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue