diff --git a/src/.stories/Storybook.scss b/src/.stories/Storybook.scss index 84d8eb9ed..3decb2b6a 100644 --- a/src/.stories/Storybook.scss +++ b/src/.stories/Storybook.scss @@ -72,11 +72,18 @@ flex-shrink: 0; align-items: center; justify-content: center; - width: 200px; + width: 100px; border-right: 1px solid #EFEFEF; border-bottom: 0; } - +.verticalMargins { + margin-top: 5px; + margin-bottom: 10px; +} +.horizontalMargins { + margin-left: 5px; + margin-right: 15px; +} // Grid .grid { display: block; diff --git a/src/.stories/index.js b/src/.stories/index.js index 11a2f0783..c8c19b842 100644 --- a/src/.stories/index.js +++ b/src/.stories/index.js @@ -482,6 +482,32 @@ storiesOf('Customization', module) ); }) + .add('Horizontal with Margins', () => { + return ( +
+ +
+ ); + }) + .add('Vertical with Margins', () => { + return ( +
+ +
+ ); + }) .add('Transition duration', () => { return (
diff --git a/src/SortableContainer/index.js b/src/SortableContainer/index.js index f666cce2f..a995e8f89 100644 --- a/src/SortableContainer/index.js +++ b/src/SortableContainer/index.js @@ -148,6 +148,13 @@ export default function sortableContainer(WrappedComponent, config = {withRef: f } } } + componentWillUpdate(nextProps, nextState) { + if (this.cleanupTimeout) { + clearTimeout(this.cleanupTimeout); + this.cleanupTimeout = null; + this.performCleanup(); + } + } handleStart = event => { const {distance, shouldCancelStart} = this.props; @@ -310,8 +317,8 @@ export default function sortableContainer(WrappedComponent, config = {withRef: f this.helper.style.boxSizing = 'border-box'; this.helper.style.pointerEvents = 'none'; + this.sortableGhost = node; if (hideSortableGhost) { - this.sortableGhost = node; node.style.visibility = 'hidden'; node.style.opacity = 0; } @@ -372,13 +379,33 @@ export default function sortableContainer(WrappedComponent, config = {withRef: f } }; + _handleSortMove = event => { + this.animateNodes(); + this.autoscroll(); + + if (window.requestAnimationFrame) + this.sortMoveAF = null; + else setTimeout(() =>{ + this.sortMoveAF = null; + }, 1000/60); // aim for 60 fps + }; + handleSortMove = event => { const {onSortMove} = this.props; event.preventDefault(); // Prevent scrolling on mobile + if (this.sortMoveAF) { + return; + } + this.updatePosition(event); - this.animateNodes(); - this.autoscroll(); + + if (window.requestAnimationFrame) { + this.sortMoveAF = window.requestAnimationFrame(this._handleSortMove); + } else { + this.sortMoveAF = true; + this._handleSortMove(); // call inner function now if no animation frame + } if (onSortMove) { onSortMove(event); @@ -386,9 +413,15 @@ export default function sortableContainer(WrappedComponent, config = {withRef: f }; handleSortEnd = event => { - const {hideSortableGhost, onSortEnd} = this.props; + const {hideSortableGhost, transitionDuration, onSortEnd} = this.props; const {collection} = this.manager.active; + // Remove the move handler if there's a frame that hasn't run yet. + if (window.cancelAnimationFrame && this.sortMoveAF){ + window.cancelAnimationFrame(this.sortMoveAF); + this.sortMoveAF = null; + } + // Remove the event listeners if the node is still in the DOM if (this.listenerNode) { events.move.forEach(eventName => @@ -400,34 +433,41 @@ export default function sortableContainer(WrappedComponent, config = {withRef: f this.listenerNode.removeEventListener(eventName, this.handleSortEnd)); } - // Remove the helper from the DOM - this.helper.parentNode.removeChild(this.helper); + this.updatePosition(event); // Make sure we have the right position for the helper - if (hideSortableGhost && this.sortableGhost) { - this.sortableGhost.style.visibility = ''; - this.sortableGhost.style.opacity = ''; - } - - const nodes = this.manager.refs[collection]; - for (let i = 0, len = nodes.length; i < len; i++) { - const node = nodes[i]; - const el = node.node; + // This function might be pre-empted if the parent calls a re-render quickly. + this.cleanupTimeout = setTimeout(this.performCleanup, transitionDuration); - // Clear the cached offsetTop / offsetLeft value - node.edgeOffset = null; - - // Remove the transforms / transitions - el.style[`${vendorPrefix}Transform`] = ''; - el.style[`${vendorPrefix}TransitionDuration`] = ''; + // Remove helper after a transition back to place from the DOM + setTimeout((helper => { + if (this.props.hideSortableGhost) { + this.sortableGhost.style.visibility = ''; + this.sortableGhost.style.opacity = ''; + } + helper.parentNode.removeChild(helper); + }).bind(this, this.helper), transitionDuration*2); + + // For now, transition the helper to a position over the ghost. + if (transitionDuration) { + if (this.sortableGhost) { + // remove transition off of the ghost BEFORE repositioning helper + this.sortableGhost.style[`${vendorPrefix}TransitionDuration`] = ''; + } + this.helper.style[`${vendorPrefix}TransitionDuration`] = `${transitionDuration}ms`; + + const helperStart = this.helper.getBoundingClientRect(); + const helperDestination = this.sortableGhost.getBoundingClientRect(); + this.helper.style[`${vendorPrefix}Transform`] = `translate3d(${ + helperDestination.left - (helperStart.left - this.translate.x) + }px,${ + helperDestination.top - (helperStart.top - this.translate.y) + }px,0)`; } // Stop autoscroll clearInterval(this.autoscrollInterval); this.autoscrollInterval = null; - // Update state - this.manager.active = null; - this.setState({ sorting: false, sortingIndex: null, @@ -444,8 +484,33 @@ export default function sortableContainer(WrappedComponent, config = {withRef: f ); } + if (this.sortableGhost) { + // remove transform off of the ghost AFTER onSortEnd is called. + this.sortableGhost.style[`${vendorPrefix}Transform`] = ''; + } + + this._touched = false; }; + performCleanup = () => { + if (!this.manager.active) return; + if (this.cleanupTimeout) this.cleanupTimeout = null; + + // Remove styles as part of this cleanup (will be called before rerendering) + this.manager.refs[this.manager.active.collection].forEach((node) =>{ + node.edgeOffset = null; + + node.node.style.zIndex = ''; + if (node.node === this.sortableGhost) { + return; // For the ghost node, we'll do it when we remove the helper + } + node.node.style[`${vendorPrefix}Transform`] = ''; + node.node.style[`${vendorPrefix}TransitionDuration`] = ''; + }); + + // Update manager state + this.manager.active = null; + }; getLockPixelOffsets() { const {width, height} = this; @@ -533,7 +598,17 @@ export default function sortableContainer(WrappedComponent, config = {withRef: f top: (window.pageYOffset - this.initialWindowScroll.top), left: (window.pageXOffset - this.initialWindowScroll.left), }; + + const ghostOffset = nodes[this.index].edgeOffset || + (nodes[this.index].edgeOffset = getEdgeOffset(nodes[this.index].node)); + + const ghostTranslate = { + x: 0, + y: 0, + }; + const prevIndex = this.newIndex; + this.newIndex = null; for (let i = 0, len = nodes.length; i < len; i++) { @@ -541,6 +616,7 @@ export default function sortableContainer(WrappedComponent, config = {withRef: f const index = node.sortableInfo.index; const width = node.offsetWidth; const height = node.offsetHeight; + const margin = getElementMargin(node); const offset = { width: this.width > width ? width / 2 : this.width / 2, height: this.height > height ? height / 2 : this.height / 2, @@ -567,14 +643,22 @@ export default function sortableContainer(WrappedComponent, config = {withRef: f nextNode.edgeOffset = getEdgeOffset(nextNode.node, this.container); } + if (transitionDuration) { + node.style[ + `${vendorPrefix}TransitionDuration` + ] = `${transitionDuration}ms`; + } + + node.style.zIndex = '1'; // If the node is the one we're currently animating, skip it if (index === this.index) { + node.style.zIndex = '0'; if (hideSortableGhost) { /* - * With windowing libraries such as `react-virtualized`, the sortableGhost - * node may change while scrolling down and then back up (or vice-versa), - * so we need to update the reference to the new node just to be safe. - */ + * With windowing libraries such as `react-virtualized`, the sortableGhost + * node may change while scrolling down and then back up (or vice-versa), + * so we need to update the reference to the new node just to be safe. + */ this.sortableGhost = node; node.style.visibility = 'hidden'; node.style.opacity = 0; @@ -582,12 +666,6 @@ export default function sortableContainer(WrappedComponent, config = {withRef: f continue; } - if (transitionDuration) { - node.style[ - `${vendorPrefix}TransitionDuration` - ] = `${transitionDuration}ms`; - } - if (this.axis.x) { if (this.axis.y) { // Calculations for a grid setup @@ -614,6 +692,8 @@ export default function sortableContainer(WrappedComponent, config = {withRef: f } if (this.newIndex === null) { this.newIndex = index; + ghostTranslate.x = edgeOffset.left - ghostOffset.left + this.margin.left - margin.left; + ghostTranslate.y = edgeOffset.top - ghostOffset.top + this.margin.left - margin.left; } } else if ( index > this.index && @@ -637,6 +717,8 @@ export default function sortableContainer(WrappedComponent, config = {withRef: f translate.y = prevNode.edgeOffset.top - edgeOffset.top; } this.newIndex = index; + ghostTranslate.x = edgeOffset.left - ghostOffset.left; + ghostTranslate.y = edgeOffset.top - ghostOffset.top; } } else { if ( @@ -645,6 +727,7 @@ export default function sortableContainer(WrappedComponent, config = {withRef: f ) { translate.x = -(this.width + this.marginOffset.x); this.newIndex = index; + ghostTranslate.x = edgeOffset.left - ghostOffset.left + node.offsetWidth - this.width; } else if ( index < this.index && (sortingOffset.left + windowScrollDelta.left) <= edgeOffset.left + offset.width @@ -652,6 +735,7 @@ export default function sortableContainer(WrappedComponent, config = {withRef: f translate.x = this.width + this.marginOffset.x; if (this.newIndex == null) { this.newIndex = index; + ghostTranslate.x = edgeOffset.left - ghostOffset.left; } } } @@ -662,6 +746,7 @@ export default function sortableContainer(WrappedComponent, config = {withRef: f ) { translate.y = -(this.height + this.marginOffset.y); this.newIndex = index; + ghostTranslate.y = edgeOffset.top - ghostOffset.top + node.offsetHeight - this.height; } else if ( index < this.index && (sortingOffset.top + windowScrollDelta.top) <= edgeOffset.top + offset.height @@ -669,6 +754,7 @@ export default function sortableContainer(WrappedComponent, config = {withRef: f translate.y = this.height + this.marginOffset.y; if (this.newIndex == null) { this.newIndex = index; + ghostTranslate.y = edgeOffset.top - ghostOffset.top; } } } @@ -679,6 +765,10 @@ export default function sortableContainer(WrappedComponent, config = {withRef: f this.newIndex = this.index; } + if (this.sortableGhost) { + this.sortableGhost.style[`${vendorPrefix}Transform`] = + `translate3d(${ghostTranslate.x}px,${ghostTranslate.y}px,0)`; + } if (onSortOver && this.newIndex !== prevIndex) { onSortOver({ newIndex: this.newIndex, @@ -748,7 +838,7 @@ export default function sortableContainer(WrappedComponent, config = {withRef: f config.withRef, 'To access the wrapped instance, you need to pass in {withRef: true} as the second argument of the SortableContainer() call' ); - + return this.refs.wrappedInstance; }