diff --git a/src/state/action-creators.js b/src/state/action-creators.js index 6d5232a8ec..15b5bd3019 100644 --- a/src/state/action-creators.js +++ b/src/state/action-creators.js @@ -223,12 +223,12 @@ export const drop = (args: DropArgs) => ({ export type DropPendingAction = {| type: 'DROP_PENDING', - payload: null, + payload: DropReason, |} -export const dropPending = (): DropPendingAction => ({ +export const dropPending = (reason: DropReason): DropPendingAction => ({ type: 'DROP_PENDING', - payload: null, + payload: reason, }); export type DropAnimationFinishedAction = {| @@ -253,6 +253,7 @@ export type Action = MoveForwardAction | CrossAxisMoveForwardAction | CrossAxisMoveBackwardAction | + DropPendingAction | DropAction | DropAnimateAction | DropAnimationFinishedAction | diff --git a/src/state/auto-scroller/auto-scroller-types.js b/src/state/auto-scroller/auto-scroller-types.js index b638b9ad3d..eab5c8b4ef 100644 --- a/src/state/auto-scroller/auto-scroller-types.js +++ b/src/state/auto-scroller/auto-scroller-types.js @@ -1,6 +1,10 @@ // @flow -import type { State } from '../../types'; +import type { DraggingState, BulkCollectionState } from '../../types'; + +type UserDragState = DraggingState | BulkCollectionState; export type AutoScroller = {| - onStateChange: (previous: State, current: State) => void, + cancel: () => void, + jumpScroll: (state: UserDragState) => void, + fluidScroll: (state: UserDragState) => void, |} diff --git a/src/state/auto-scroller/index.js b/src/state/auto-scroller/index.js index 1e8f70370f..924f1af7e9 100644 --- a/src/state/auto-scroller/index.js +++ b/src/state/auto-scroller/index.js @@ -37,34 +37,10 @@ export default ({ scrollDroppable, }); - const onStateChange = (previous: State, current: State): void => { - // now dragging - - if (current.phase === 'DRAGGING' || current.phase === 'BULK_COLLECTING') { - if (current.autoScrollMode === 'FLUID') { - fluidScroll(current); - return; - } - - // autoScrollMode == 'JUMP' - - if (!current.scrollJumpRequest) { - return; - } - - jumpScroll(current); - return; - } - - // Not currently dragging - // Was previously dragging - if (previous.phase === 'DRAGGING' || previous.phase === 'BULK_COLLECTING') { - fluidScroll.cancel(); - } - }; - const marshal: AutoScroller = { - onStateChange, + cancel: fluidScroll.cancel, + fluidScroll, + jumpScroll, }; return marshal; diff --git a/src/state/create-store.js b/src/state/create-store.js index fa69386d29..653a97b46a 100644 --- a/src/state/create-store.js +++ b/src/state/create-store.js @@ -7,6 +7,8 @@ import style from './middleware/style'; import drop from './middleware/drop'; import dropAnimationFinish from './middleware/drop-animation-finish'; import dimensionMarshalStopper from './middleware/dimension-marshal-stopper'; +import autoScroll from './middleware/auto-scroll'; +import pendingDrop from './middleware/pending-drop'; import type { DimensionMarshal } from './dimension-marshal/dimension-marshal-types'; import type { StyleMarshal } from '../view/style-marshal/style-marshal-types'; import type { Store, Hooks } from '../types'; @@ -33,6 +35,14 @@ export default ({ reducer, composeEnhancers( applyMiddleware( + // ## Debug middleware + // > uncomment to use + // debugging logger + require('./debug-middleware/log-middleware').default, + // debugging timer + // require('./debug-middleware/timing-middleware').default, + // average action timer + // require('./debug-middleware/timing-average-middleware').default(20), // ## Application middleware // Style updates do not cause more actions. It is important to update styles // before hooks are called: specifically the onDragEnd hook. We need to clear @@ -53,18 +63,12 @@ export default ({ drop, // When a drop animation finishes - fire a drop complete dropAnimationFinish, + pendingDrop, + autoScroll, // TODO: where should this go? // hooks(getHooks), - // ## Debug middleware - // > uncomment to use - // debugging logger - // require('./debug-middleware/log-middleware'), - // debugging timer - // require('./debug-middleware/timing-middleware').default, - // average action timer - // require('./debug-middleware/timing-average-middleware').default(20), ), ), ); diff --git a/src/state/dimension-marshal/collector.js b/src/state/dimension-marshal/collector.js index e6626ac66f..3d1f50173a 100644 --- a/src/state/dimension-marshal/collector.js +++ b/src/state/dimension-marshal/collector.js @@ -44,6 +44,8 @@ export default ({ getEntries, }: Args): Collector => { let frameId: ?AnimationFrameID = null; + // tmep + let timerId: ?TimeoutID = null; const collectFromDOM = (windowScroll: Position, options: CollectOptions): DimensionMap => { const { collection, includeCritical } = options; @@ -133,6 +135,7 @@ export default ({ const collect = (options: CollectOptions) => { abortFrame(); + clearTimeout(timerId); // Perform DOM collection in next frame frameId = requestAnimationFrame(() => { @@ -143,14 +146,16 @@ export default ({ // Perform publish in next frame frameId = requestAnimationFrame(() => { - timings.start('Bulk dimension publish'); - bulkReplace({ - dimensions, - viewport, - shouldReplaceCritical: options.includeCritical, - }); - timings.finish('Bulk dimension publish'); - + console.log('waiting a really long time for publish'); + timerId = setTimeout(() => { + timings.start('Bulk dimension publish'); + bulkReplace({ + dimensions, + viewport, + shouldReplaceCritical: options.includeCritical, + }); + timings.finish('Bulk dimension publish'); + }, 2000); frameId = null; }); }); diff --git a/src/state/middleware/auto-scroll.js b/src/state/middleware/auto-scroll.js new file mode 100644 index 0000000000..86d7034d9d --- /dev/null +++ b/src/state/middleware/auto-scroll.js @@ -0,0 +1,61 @@ + +// @flow +import { bindActionCreators } from 'redux'; +import createAutoScroller from '../auto-scroller'; +import type { AutoScroller } from '../auto-scroller/auto-scroller-types'; +import { + move, + updateDroppableScroll, +} from '../action-creators'; +import scrollWindow from '../../view/window/scroll-window'; +import isDragEnding from './util/is-drag-ending'; +import type { + Store, + State, + Action, +} from '../../types'; + +export default (store: Store) => { + const scroller: AutoScroller = createAutoScroller({ + ...bindActionCreators({ + scrollDroppable: updateDroppableScroll, + move, + }, store.dispatch), + scrollWindow, + }); + return (next: (Action) => mixed) => (action: Action): mixed => { + // Need to cancel any pending auto scrolling when drag is ending + if (isDragEnding(action)) { + scroller.cancel(); + next(action); + return; + } + + // auto scroll happens in response to state changes + // releasing all actions to the reducer first + next(action); + + const state: State = store.getState(); + + // Only want to auto scroll in the dragging phase + // Not allowing auto scrolling while bulk collecting + // This is to avoid a mismatch in scroll between the captured + // viewport in one frame and published in the next + // Also, jump scrolling would not occur during a BULK_COLLECTION + // as no changes to the impact are permitted in that time + if (state.phase !== 'DRAGGING') { + return; + } + + if (state.autoScrollMode === 'FLUID') { + scroller.fluidScroll(state); + return; + } + + if (!state.scrollJumpRequest) { + return; + } + + scroller.jumpScroll(state); + }; +}; diff --git a/src/state/middleware/dimension-marshal-stopper.js b/src/state/middleware/dimension-marshal-stopper.js index e43c572a15..f8b6b39ce9 100644 --- a/src/state/middleware/dimension-marshal-stopper.js +++ b/src/state/middleware/dimension-marshal-stopper.js @@ -1,11 +1,11 @@ // @flow -import type { Store, State, Action } from '../../types'; +import isDragEnding from './util/is-drag-ending'; +import type { Action } from '../../types'; import type { DimensionMarshal } from '../dimension-marshal/dimension-marshal-types'; export default (getMarshal: () => DimensionMarshal) => () => (next: (Action) => mixed) => (action: Action): mixed => { - if (action.type === 'CLEAN' || action.type === 'DROP_ANIMATE' || action.type === 'DROP_COMPLETE') { - console.log('telling the marshal to stop'); + if (isDragEnding(action)) { const marshal: DimensionMarshal = getMarshal(); marshal.stopPublishing(); } diff --git a/src/state/middleware/drop.js b/src/state/middleware/drop.js index 4a3fe3fb00..cddb4940eb 100644 --- a/src/state/middleware/drop.js +++ b/src/state/middleware/drop.js @@ -39,7 +39,7 @@ const getScrollDisplacement = ( export default ({ getState, dispatch }: Store) => (next: (Action) => mixed) => (action: Action): mixed => { - // TODO: pending drop flushing + // TODO: pending drop flushing const state: State = getState(); @@ -58,16 +58,29 @@ export default ({ getState, dispatch }: Store) => return; } - invariant(state.phase === 'DRAGGING' || state.phase === 'BULK_COLLECTING', - `Cannot drop in phase: ${state.phase}`); + invariant( + state.phase === 'DRAGGING' || + state.phase === 'BULK_COLLECTING' || + state.phase === 'DROP_PENDING', + `Cannot drop in phase: ${state.phase}` + ); + + const reason: DropReason = action.payload.reason; // Still waiting for a bulk collection to publish + // We are now shifting the application into the 'DROP_PENDING' phase if (state.phase === 'BULK_COLLECTING') { - dispatch(dropPending()); + dispatch(dropPending(reason)); return; } - const reason: DropReason = action.payload.reason; + // Still waiting for our drop pending to end + if (state.phase === 'DROP_PENDING' && state.isWaiting) { + return; + } + + // We are now in the DRAGGING or DROP_PENDING phase + const critical: Critical = state.critical; const dimensions: DimensionMap = state.dimensions; const impact: DragImpact = reason === 'DROP' ? state.impact : noImpact; diff --git a/src/state/middleware/pending-drop.js b/src/state/middleware/pending-drop.js new file mode 100644 index 0000000000..2ce9a1a907 --- /dev/null +++ b/src/state/middleware/pending-drop.js @@ -0,0 +1,33 @@ + +// @flow +import { drop } from '../action-creators'; +import type { + Store, + State, + Action, +} from '../../types'; + +export default (store: Store) => (next: (Action) => mixed) => (action: Action): mixed => { + next(action); + + if (action.type !== 'BULK_REPLACE') { + return; + } + + // A bulk replace occurred - check if + // 1. there was a pending drop + // 2. that the pending drop is no longer waiting + + const postActionState: State = store.getState(); + + if (postActionState.phase !== 'DROP_PENDING') { + return; + } + + if (!postActionState.isWaiting) { + console.log('ending a pending drop'); + store.dispatch(drop({ + reason: postActionState.reason, + })); + } +}; diff --git a/src/state/middleware/util/is-drag-ending.js b/src/state/middleware/util/is-drag-ending.js new file mode 100644 index 0000000000..a3b210ae97 --- /dev/null +++ b/src/state/middleware/util/is-drag-ending.js @@ -0,0 +1,8 @@ +// @flow +import type { Action } from '../../../types'; + +export default (action: Action): boolean => + action.type === 'CLEAN' || + action.type === 'DROP_ANIMATE' || + action.type === 'DROP_COMPLETE' || + action.type === 'DROP_PENDING'; diff --git a/src/state/reducer.js b/src/state/reducer.js index 5fb115d573..3271de15c2 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -11,7 +11,7 @@ import type { DraggableDescriptor, DraggableDimensionMap, DroppableDimensionMap, - DropResult, + DropReason, DraggableLocation, LiftRequest, PendingDrop, @@ -73,12 +73,23 @@ const move = ({ windowDetails, impact, scrollJumpRequest, -}: MoveArgs): BulkCollectionState | DraggingState => { +}: MoveArgs): BulkCollectionState | DraggingState | DropPendingState => { // BULK_COLLECTING: can update position but cannot update impact // DRAGGING: can update position and impact - - invariant(state.phase === 'BULK_COLLECTING' || state.phase === 'DRAGGING', - `Attempting to move in an unsupported phase ${state.phase}`); + // TODO: DROP_PENDING: no movements should occur + invariant( + state.phase === 'DRAGGING' || + state.phase === 'BULK_COLLECTING' || + state.phase === 'DROP_PENDING', + `Attempting to move in an unsupported phase ${state.phase}` + ); + + // No longer accepting any movements + // This might happen as we have not told the + // drag handles that the drag has ended yet + if (state.phase === 'DROP_PENDING') { + return state; + } const client: ItemPositions = (() => { const offset: Position = subtract(clientSelection, state.initial.client.selection); @@ -241,18 +252,33 @@ export default (state: State = clean(), action: Action): State => { }, }; - // We are now moving out of the BULK_PUBLISH phase into DRAGGING - const newState: State = { - phase: 'DRAGGING', + // Moving into the DRAGGING phase + if (state.phase === 'BULK_COLLECTING') { + return { + // appeasing flow + phase: 'DRAGGING', + ...state, + // eslint-disable-next-line + phase: 'DRAGGING', + impact, + dimensions, + window: windowDetails, + }; + } + + // There was a pending drop + return { + // appeasing flow + phase: 'DROP_PENDING', ...state, // eslint-disable-next-line - phase: 'DRAGGING', + phase: 'DROP_PENDING', impact, dimensions, window: windowDetails, + // No longer waiting + isWaiting: false, }; - - return newState; } if (action.type === 'MOVE') { @@ -499,15 +525,30 @@ export default (state: State = clean(), action: Action): State => { }); } + if (action.type === 'DROP_PENDING') { + const reason: DropReason = action.payload; + invariant(state.phase === 'BULK_COLLECTING', + 'Can only move into the DROP_PENDING phase from the BULK_COLLECTING phase'); + + const newState: DropPendingState = { + // appeasing flow + phase: 'DROP_PENDING', + ...state, + // eslint-disable-next-line + phase: 'DROP_PENDING', + isWaiting: true, + reason, + }; + return newState; + } + if (action.type === 'DROP_ANIMATE') { const pending: PendingDrop = action.payload; + invariant(state.phase === 'DRAGGING' || state.phase === 'DROP_PENDING', + `Cannot animate drop from phase ${state.phase}` + ); - // TODO: pending drop? - if (state.phase !== 'DRAGGING') { - console.error('Cannot animate drop while not dragging', action); - return clean(); - } - + // Moving into a new phase const result: DropAnimatingState = { phase: 'DROP_ANIMATING', pending, diff --git a/src/types.js b/src/types.js index fee2ab7226..ee0170d8f3 100644 --- a/src/types.js +++ b/src/types.js @@ -293,6 +293,8 @@ export type BulkCollectionState = {| export type DropPendingState = {| ...DraggingState, phase: 'DROP_PENDING', + isWaiting: boolean, + reason: DropReason, |} // An optional phase for animating the drop / cancel if it is needed diff --git a/src/view/animation.js b/src/view/animation.js index bfed8c2bca..d4af0878dc 100644 --- a/src/view/animation.js +++ b/src/view/animation.js @@ -3,8 +3,8 @@ import type { SpringHelperConfig } from 'react-motion/lib/Types'; export const physics = (() => { const base = { - stiffness: 1000, // fast - // stiffness: 100, // slow + // stiffness: 1000, // fast + stiffness: 100, // slow (for debugging) damping: 60, // precision: 0.5, precision: 0.99, diff --git a/src/view/draggable/connected-draggable.js b/src/view/draggable/connected-draggable.js index 474ff4b06f..b6c9c08c25 100644 --- a/src/view/draggable/connected-draggable.js +++ b/src/view/draggable/connected-draggable.js @@ -122,7 +122,9 @@ export const makeMapStateToProps = (): Selector => { const draggingSelector = (state: State, ownProps: OwnProps): ?MapProps => { // Dragging - if (state.phase === 'DRAGGING' || state.phase === 'BULK_COLLECTING') { + if (state.phase === 'DRAGGING' || + state.phase === 'BULK_COLLECTING' || + state.phase === 'DROP_PENDING') { // not the dragging item if (state.critical.draggable.id !== ownProps.draggableId) { return null; @@ -178,7 +180,10 @@ export const makeMapStateToProps = (): Selector => { const movingOutOfTheWaySelector = (state: State, ownProps: OwnProps): ?MapProps => { // Dragging - if (state.phase === 'DRAGGING' || state.phase === 'BULK_COLLECTING') { + if ( + state.phase === 'DRAGGING' || + state.phase === 'BULK_COLLECTING' || + state.phase === 'DROP_PENDING') { // we do not care about the dragging item if (state.critical.draggable.id === ownProps.draggableId) { return null;