diff --git a/package.json b/package.json index 8c5fea39b8d4d..fe9d8cd085981 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "prop-types": "^15.6.1", "raw-loader": "0.5.1", "react": "^16.2.0", + "react-beautiful-dnd": "^8.0.7", "react-datetime": "^2.14.0", "react-dom": "^16.2.0", "react-dropzone": "^4.2.9", @@ -135,4 +136,4 @@ "sinon": "^4.5.0", "through2": "^2.0.3" } -} \ No newline at end of file +} diff --git a/public/components/page_manager/page_controls.js b/public/components/page_manager/page_controls.js deleted file mode 100644 index 2826f74451e78..0000000000000 --- a/public/components/page_manager/page_controls.js +++ /dev/null @@ -1,72 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { EuiFlexGroup, EuiFlexItem, EuiButtonIcon } from '@elastic/eui'; - -export const PageControls = ({ pageId, onDelete, onDuplicate, movePage }) => { - const handleDuplicate = ev => { - ev.preventDefault(); - onDuplicate(pageId); - }; - - const handleDelete = ev => { - ev.preventDefault(); - onDelete(pageId); - }; - - const handleMove = position => ev => { - ev.preventDefault(); - movePage(pageId, position); - }; - - return ( -
- - - { - handleMove(-1)(ev); - }} - /> - - - - - - - - - { - handleMove(+1)(ev); - }} - /> - - -
- ); -}; - -PageControls.propTypes = { - pageId: PropTypes.string.isRequired, - pageNumber: PropTypes.number.isRequired, - onDelete: PropTypes.func.isRequired, - onDuplicate: PropTypes.func.isRequired, - movePage: PropTypes.func.isRequired, -}; diff --git a/public/components/page_manager/page_manager.js b/public/components/page_manager/page_manager.js index 37d3aec35d6ae..8309c7ab851e9 100644 --- a/public/components/page_manager/page_manager.js +++ b/public/components/page_manager/page_manager.js @@ -1,10 +1,10 @@ import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; import { EuiIcon, EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui'; +import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd'; import { ConfirmModal } from '../confirm_modal'; import { Link } from '../link'; import { PagePreview } from '../page_preview'; -import { PageControls } from './page_controls'; export class PageManager extends React.PureComponent { static propTypes = { @@ -12,6 +12,7 @@ export class PageManager extends React.PureComponent { workpadId: PropTypes.string.isRequired, addPage: PropTypes.func.isRequired, movePage: PropTypes.func.isRequired, + previousPage: PropTypes.func.isRequired, duplicatePage: PropTypes.func.isRequired, removePage: PropTypes.func.isRequired, selectedPage: PropTypes.string, @@ -19,6 +20,51 @@ export class PageManager extends React.PureComponent { setDeleteId: PropTypes.func.isRequired, }; + state = { + showTrayPop: true, + }; + + componentDidMount() { + // gives the tray pop animation time to finish + setTimeout(() => { + this.scrollToActivePage(); + this.setState({ showTrayPop: false }); + }, 1000); + } + + componentDidUpdate(prevProps) { + // scrolls to the active page on the next tick, otherwise new pages don't scroll completely into view + if (prevProps.selectedPage !== this.props.selectedPage) setTimeout(this.scrollToActivePage, 0); + } + + scrollToActivePage = () => { + if (this.activePageRef && this.pageListRef) { + const pageOffset = this.activePageRef.offsetLeft; + const { + left: pageLeft, + right: pageRight, + width: pageWidth, + } = this.activePageRef.getBoundingClientRect(); + const { + left: listLeft, + right: listRight, + width: listWidth, + } = this.pageListRef.getBoundingClientRect(); + + if (pageLeft < listLeft) + this.pageListRef.scrollTo({ + left: pageOffset, + behavior: 'smooth', + }); + if (pageRight > listRight) { + this.pageListRef.scrollTo({ + left: pageOffset - listWidth + pageWidth, + behavior: 'smooth', + }); + } + } + }; + confirmDelete = pageId => { this.props.setDeleteId(pageId); }; @@ -26,8 +72,21 @@ export class PageManager extends React.PureComponent { resetDelete = () => this.props.setDeleteId(null); doDelete = () => { + const { previousPage, removePage, deleteId, selectedPage } = this.props; this.resetDelete(); - this.props.removePage(this.props.deleteId); + if (deleteId === selectedPage) previousPage(); + removePage(deleteId); + }; + + onDragEnd = ({ draggableId: pageId, source, destination }) => { + // dropped outside the list + if (!destination) { + return; + } + + const position = destination.index - source.index; + + this.props.movePage(pageId, position); }; renderPage = (page, i) => { @@ -35,58 +94,77 @@ export class PageManager extends React.PureComponent { const pageNumber = i + 1; return ( - - - - - {pageNumber} - - - - - - - - - - + + {provided => ( +
{ + if (page.id === selectedPage) this.activePageRef = el; + provided.innerRef(el); + }} + {...provided.draggableProps} + {...provided.dragHandleProps} + > + + + + {pageNumber} + + + + + + + + +
+ )} +
); }; render() { const { pages, addPage, deleteId } = this.props; + const { showTrayPop } = this.state; return ( -
- - {pages.map(this.renderPage)} - -
+ + + {provided => ( +
{ + this.pageListRef = el; + provided.innerRef(el); + }} + {...provided.droppableProps} + > + {pages.map(this.renderPage)} + {provided.placeholder} +
+ )} +
+
div { + .canvasPageManager--trayPop > div { animation: trayPop $euiAnimSpeedNormal $euiAnimSlightResistance; opacity: 0; animation-fill-mode: forwards; } - - @for $i from 1 through 10 { - .canvasPageManager__pageList > div:nth-child(#{$i}n) { + @for $i from 1 through 20 { + .canvasPageManager--trayPop > div:nth-child(#{$i}n) { animation-delay: #{$i * 0.05}s; } } @@ -39,7 +38,7 @@ opacity: 0; animation: buttonPop $euiAnimSpeedNormal $euiAnimSlightResistance; animation-fill-mode: forwards; - height: 160px; + height: 144px; } .canvasPageManager__addPageTip { @@ -50,11 +49,12 @@ .canvasPageManager__page { padding: $euiSize $euiSize $euiSize $euiSizeS; color: inherit; - min-height: 144px; - max-height: 160px; + min-height: 100px; + max-height: 144px; - &:focus, &-isActive { - background-color: darken($euiColorLightestShade, 10%); + &:focus, + &-isActive { + background-color: transparentize(darken($euiColorLightestShade, 30%), 0.5); outline: none; text-decoration: none; } @@ -65,22 +65,18 @@ } } - &:hover { + &:hover, + &:focus { text-decoration: none; .canvasPageManager__pagePreview { - @include euiBottomShadowMedium($opacity: .3); + @include euiBottomShadowMedium($opacity: 0.3); } .canvasPageManager__controls { visibility: visible; opacity: 1; } - - .canvasPageManager__removeIcon { - visibility: visible; - opacity: 1; - } } &-isActive { @@ -122,11 +118,14 @@ } .canvasPageManager__controls { - margin-top: $euiSizeS; + position: absolute; + right: $euiSizeS; + top: $euiSizeS; visibility: hidden; opacity: 0; transition: opacity $euiAnimSpeedFast $euiAnimSlightResistance; - transition-delay: $euiAnimSpeedNormal; + transition-delay: $euiAnimSpeedExtraSlow; + background: transparentize($euiColorGhost, 0.5); border-radius: $euiBorderRadius; } } diff --git a/public/components/page_preview/page_controls.js b/public/components/page_preview/page_controls.js new file mode 100644 index 0000000000000..a331b57f97a1d --- /dev/null +++ b/public/components/page_preview/page_controls.js @@ -0,0 +1,50 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { EuiFlexGroup, EuiFlexItem, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; + +export const PageControls = ({ pageId, onDelete, onDuplicate }) => { + const handleDuplicate = ev => { + ev.preventDefault(); + onDuplicate(pageId); + }; + + const handleDelete = ev => { + ev.preventDefault(); + onDelete(pageId); + }; + + return ( + + + + + + + + + + + + + ); +}; + +PageControls.propTypes = { + pageId: PropTypes.string.isRequired, + pageNumber: PropTypes.number.isRequired, + onDelete: PropTypes.func.isRequired, + onDuplicate: PropTypes.func.isRequired, +}; diff --git a/public/components/page_preview/page_preview.js b/public/components/page_preview/page_preview.js index 6e8a135252e56..21e46f0b503e9 100644 --- a/public/components/page_preview/page_preview.js +++ b/public/components/page_preview/page_preview.js @@ -1,18 +1,21 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { DomPreview } from '../dom_preview'; +import { PageControls } from './page_controls'; export class PagePreview extends PureComponent { static propTypes = { page: PropTypes.object.isRequired, height: PropTypes.number.isRequired, pageNumber: PropTypes.number.isRequired, - width: PropTypes.number, - setWidth: PropTypes.func, + width: PropTypes.number.isRequired, + setWidth: PropTypes.func.isRequired, + duplicatePage: PropTypes.func.isRequired, + confirmDelete: PropTypes.func.isRequired, }; render() { - const { page, pageNumber, height, width, setWidth } = this.props; + const { page, pageNumber, height, width, setWidth, duplicatePage, confirmDelete } = this.props; return (
setWidth(width)} /> +
); diff --git a/public/components/toolbar/toolbar.js b/public/components/toolbar/toolbar.js index fe56480014113..7b622702858ea 100644 --- a/public/components/toolbar/toolbar.js +++ b/public/components/toolbar/toolbar.js @@ -55,7 +55,7 @@ export const Toolbar = props => { ); const trays = { - pageManager: , + pageManager: , workpadloader: workpadLoader, expression: !elementIsSelected ? null : , };