Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Page reordering from carousel #4048

Merged
merged 26 commits into from
Jan 21, 2020
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion assets/src/edit-story/app/layout/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Header from '../../components/header';
import Inspector from '../../components/inspector';
import Library from '../../components/library';
import Canvas from '../../components/canvas';
import DropZoneProvider from '../../components/dropzone/dropZoneProvider';
import { LIBRARY_WIDTH, INSPECTOR_WIDTH, HEADER_HEIGHT } from '../../constants';

const Editor = styled.div`
Expand Down Expand Up @@ -44,7 +45,9 @@ function Layout() {
<Library />
</Area>
<Area area="canv">
<Canvas />
<DropZoneProvider>
<Canvas />
</DropZoneProvider>
</Area>
<Area area="insp">
<Inspector />
Expand Down
37 changes: 19 additions & 18 deletions assets/src/edit-story/components/canvas/canvasLayout.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,26 +37,27 @@ const Area = styled.div`
`;

function CanvasLayout() {
// @todo SelectionCanvas should not be there, will be addressed separately.
return (
<SelectionCanvas>
<Background>
<Area area="page">
<Background>
<Area area="page">
<SelectionCanvas>
miina marked this conversation as resolved.
Show resolved Hide resolved
<Page />
</Area>
<Area area="menu">
<PageMenu />
</Area>
<Area area="prev">
<PageNav isNext={ false } />
</Area>
<Area area="next">
<PageNav />
</Area>
<Area area="carrousel">
<Carrousel />
</Area>
</Background>
</SelectionCanvas>
</SelectionCanvas>
</Area>
<Area area="menu">
<PageMenu />
</Area>
<Area area="prev">
<PageNav isNext={ false } />
</Area>
<Area area="next">
<PageNav />
</Area>
<Area area="carrousel">
<Carrousel />
</Area>
</Background>
);
}

Expand Down
45 changes: 40 additions & 5 deletions assets/src/edit-story/components/canvas/carrousel/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import styled from 'styled-components';
* Internal dependencies
*/
import { useStory } from '../../../app';
import DropZone from '../../../components/dropzone';

const List = styled.nav`
display: flex;
Expand All @@ -30,13 +31,47 @@ const Page = styled.a`
`;

function Canvas() {
const { state: { pages, currentPageIndex }, actions: { setCurrentPage } } = useStory();
const handleClickPage = ( page ) => () => setCurrentPage( { pageId: page.id } );
const { state: { pages, currentPageIndex }, actions: { setCurrentPage, arrangePage } } = useStory();
const getArrangeIndex = ( sourceIndex, dstIndex, position ) => {
// If the dropped element is before the dropzone index then we have to deduct
// that from the index to make up for the "lost" element in the row.
const indexAdjustment = sourceIndex < dstIndex ? -1 : 0;
if ( 'left' === position.x ) {
return dstIndex + indexAdjustment;
}
return dstIndex + 1 + indexAdjustment;
};
const handleClickPage = ( page ) => setCurrentPage( { pageId: page.id } );
return (
<List>
{ pages.map( ( page, index ) => (
<Page key={ index } onClick={ handleClickPage( page ) } isActive={ index === currentPageIndex } />
) ) }
{ pages.map( ( page, index ) => {
const onDrop = ( evt, position ) => {
miina marked this conversation as resolved.
Show resolved Hide resolved
const droppedEl = JSON.parse( evt.dataTransfer.getData( 'text' ) );
if ( ! droppedEl || 'page' !== droppedEl.type ) {
return;
}
const arrangedIndex = getArrangeIndex( droppedEl.index, index, position );
// Do nothing if the index didn't change.
if ( droppedEl.index !== arrangedIndex ) {
const pageId = pages[ droppedEl.index ].id;
arrangePage( { pageId, position: arrangedIndex } );
setCurrentPage( { pageId } );
}
};
// @todo Create a Draggable component for setting data and setting "draggable".
const onDragStart = ( evt ) => {
miina marked this conversation as resolved.
Show resolved Hide resolved
const pageData = {
type: 'page',
index,
};
evt.dataTransfer.setData( 'text', JSON.stringify( pageData ) );
};
return (
<DropZone key={ index } onDrop={ onDrop } >
miina marked this conversation as resolved.
Show resolved Hide resolved
<Page draggable="true" onDragStart={ onDragStart } onClick={ () => handleClickPage( page ) } isActive={ index === currentPageIndex } />
</DropZone>
);
} ) }
</List>
);
}
Expand Down
6 changes: 6 additions & 0 deletions assets/src/edit-story/components/dropzone/context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* WordPress dependencies
*/
import { createContext } from '@wordpress/element';

export default createContext( { actions: {}, state: {} } );
91 changes: 91 additions & 0 deletions assets/src/edit-story/components/dropzone/dropzoneProvider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* External dependencies
*/
import PropTypes from 'prop-types';

/**
* WordPress dependencies
*/
import { useCallback, useState } from '@wordpress/element';

/**
* Internal dependencies
*/
import Context from './context';

function DropZoneProvider( { children } ) {
const [ dropZones, setDropZones ] = useState( [] );
const [ hoveredDropZone, setHoveredDropZone ] = useState( null );

const addDropZone = useCallback(
( dropZone ) => {
setDropZones( ( oldDropZones ) => ( [ ...oldDropZones, dropZone ] ) );
}, [] );

const isWithinElementBounds = ( element, x, y ) => {
const rect = element.getBoundingClientRect();
if ( rect.bottom === rect.top || rect.left === rect.right ) {
return false;
}
return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
};

const resetHoverState = () => {
setHoveredDropZone( null );
};

const onDragOver = ( evt ) => {
evt.preventDefault();
// Get the hovered dropzone. // @todo Consider dropzone inside dropzone, will we need this?
const foundDropZones = dropZones.filter( ( dropZone ) => isWithinElementBounds( dropZone.ref, evt.clientX, evt.clientY ) );

// If there was a dropzone before and nothing was found now, reset.
if ( hoveredDropZone && ! foundDropZones.length ) {
resetHoverState();
return;
}
const foundDropZone = foundDropZones[ 0 ];
// If dropzone not found, do nothing.
if ( ! foundDropZone || ! foundDropZone.ref ) {
return;
}
const rect = foundDropZone.ref.getBoundingClientRect();

const position = {
x: evt.clientX - rect.left < rect.right - evt.clientX ? 'left' : 'right',
y: evt.clientY - rect.top < rect.bottom - evt.clientY ? 'top' : 'bottom',
};

setHoveredDropZone( {
ref: foundDropZone.ref,
position,
} );
};

const state = {
state: {
hoveredDropZone,
dropZones,
},
actions: {
addDropZone,
resetHoverState,
},
};
return (
<div onDragOver={ onDragOver }>
<Context.Provider value={ state }>
{ children }
</Context.Provider>
</div>
);
}

DropZoneProvider.propTypes = {
children: PropTypes.oneOfType( [
PropTypes.arrayOf( PropTypes.node ),
PropTypes.node,
] ).isRequired,
};

export default DropZoneProvider;
102 changes: 102 additions & 0 deletions assets/src/edit-story/components/dropzone/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* External dependencies
*/
import PropTypes from 'prop-types';
import styled from 'styled-components';

/**
* WordPress dependencies
*/
import { useRef, useLayoutEffect } from '@wordpress/element';

/**
* Internal dependencies
*/
import useDropZone from './useDropZone';

const DropZoneComponent = styled.div`
display: inherit;
miina marked this conversation as resolved.
Show resolved Hide resolved
position: relative;
${ ( { borderPosition, theme, highlightWidth } ) => borderPosition && `
:after {
height: 100%;
display: block;
position: absolute;
width: ${ highlightWidth }px;
background: ${ theme.colors.action };
content: '';
${ borderPosition }: -${ highlightWidth / 2 }px;
}
` }
`;

function DropZone( { children, onDrop } ) {
miina marked this conversation as resolved.
Show resolved Hide resolved
const dropZoneElement = useRef( null );
const { actions: { addDropZone, resetHoverState }, state: { hoveredDropZone, dropZones } } = useDropZone();

useLayoutEffect( () => {
if ( ! dropZones.some( ( { ref } ) => ref === dropZoneElement.current ) ) {
addDropZone( {
ref: dropZoneElement.current,
miina marked this conversation as resolved.
Show resolved Hide resolved
} );
}
}, [ addDropZone, dropZones ] );
miina marked this conversation as resolved.
Show resolved Hide resolved

const getDragType = ( { dataTransfer } ) => {
if ( dataTransfer ) {
if ( Array.isArray( dataTransfer.types ) ) {
if ( dataTransfer.types.includes( 'Files' ) ) {
return 'file';
}
if ( dataTransfer.types.includes( 'text/html' ) ) {
return 'html';
}
} else {
// For Edge, types is DomStringList and not array.
if ( dataTransfer.types.contains( 'Files' ) ) {
return 'file';
}
if ( dataTransfer.types.contains( 'text/html' ) ) {
return 'html';
}
}
}
return 'default';
};

const onDropHandler = ( evt ) => {
resetHoverState();
if ( dropZoneElement.current ) {
const rect = dropZoneElement.current.getBoundingClientRect();
// Get the relative position of the dropping point based on the dropzone.
const relativePosition = {
x: evt.clientX - rect.left < rect.right - evt.clientX ? 'left' : 'right',
y: evt.clientY - rect.top < rect.bottom - evt.clientY ? 'top' : 'bottom',
};
if ( 'default' === getDragType( evt ) ) {
onDrop( evt, relativePosition );
}
// @todo Support for files when it becomes necessary.
}
evt.preventDefault();
};

const isDropZoneActive = dropZoneElement.current && hoveredDropZone && hoveredDropZone.ref === dropZoneElement.current;
// @todo Currently static, can be adjusted for other use cases.
const highlightWidth = 4;
return (
<DropZoneComponent highlightWidth={ highlightWidth } borderPosition={ isDropZoneActive ? hoveredDropZone.position.x : null } ref={ dropZoneElement } onDrop={ onDropHandler }>
{ children }
</DropZoneComponent>
);
}

DropZone.propTypes = {
children: PropTypes.oneOfType( [
PropTypes.arrayOf( PropTypes.node ),
PropTypes.node,
] ).isRequired,
onDrop: PropTypes.func,
};

export default DropZone;
15 changes: 15 additions & 0 deletions assets/src/edit-story/components/dropzone/useDropZone.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* WordPress dependencies
*/
import { useContext } from '@wordpress/element';

/**
* Internal dependencies
*/
import Context from './context';

function useDropZone() {
return useContext( Context );
}

export default useDropZone;