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;
}