How to use a long-press to trigger re-ordering a list in Ionic Framework, instead of Ionic’s default implementation, which requires a button to switch modes.
Ionic Framework is a great framework for building cross-platform mobile applications. It ships with a number of user interface tools for building a native-like, touch-friendly experience. One of those is ion-list, which supports re-ordering. The users clicks a button to enter re-order mode, and can move items up and down before clicking the button again to finish.
The default re-ordering experience is pretty good, but it doesn’t match the experience we were looking for, and is different from the re-ordering experience in the native iOS app that we were trying to port to HTML5. In the iOS version, there is no button to enter re-order mode - instead the user simply long-presses an item to move it. The item then appears to raise up from the list, and can be dragged up and down to reposition it. It also provides feedback by dynamically moving the other list items as you drag up and down.
To illustrate the difference, here are a couple of screen-captures of the two approaches:
You can also try both versions for yourself here (looks best in a narrow window like a phone):
Click button to reorder | Long-press to Reorder |
I spent some time searching for other options for re-ordering lists - there a few that support touch. However, they all start the re-order operation on a touch-down event, rather than a long press. That wouldn’t work in this case, because the list also needs to scroll using touch.
Creating a sortable directive
For the purpose of this blog article, I’ve built a simple Ionic application with a sortable list. The full project is available on GitHub. Note that this project also uses jQuery to make a few things a bit easier, but it should be possible to eliminate jQuery if required.
I wanted to make an ion-list
sortable by adding some extra attributes, so the markup would look like this:
<ion-list sortable draggable=".card" sorted="onReorder($fromIndex, $toIndex)">
<ion-item ng-repeat="contact in contacts">
...
</ion-item>
</ion-list>
The sortable
attribute invokes the directive. We can also tell it which elements we’re allowed to re-order by specifying the draggable
attribute, and provide a callback function for when the moved item is dropped.
Here’s the bare-bones structure of the new directive:
The above code sets up the directive’s scope for binding the draggable
and sorted
attributes. Then it adds handlers for the Ionic gestures hold
, touchmove
/mousemove
and release
.
In touchHold
, it looks for the element that the user is about to start dragging.
In touchMove
, it calls stopPropagation()
to prevent anything else from handling the event, because we don’t want the list’s normal scroll behaviour to activate when the user drags up and down.
Switching to re-order mode
When the user starts dragging an item, it needs to switch that item to absolute
positioning, and replace it with a placeholder. Add this to the touchHold()
function:
Adding the draggable
class name means we can adjust its appearance to make it seem to lift up from the list of cards:
.dragging {
-moz-transform: scale(1.05, 1.05);
-ms-transform: scale(1.05, 1.05);
-o-transform: scale(1.05, 1.05);
-webkit-transform: scale(1.05, 1.05);
transform: scale(1.05, 1.05);
-moz-box-shadow: 0 0 3px rgba(0, 0, 0, 0.1);
-webkit-box-shadow: 0 0 3px rgba(0, 0, 0, 0.1);
box-shadow: 0 0 3px rgba(0, 0, 0, 0.1);
}
Dragging up and down the list
As the user drags the item up and down the list, it needs to move the dragged item with the touch point. As the position changes in the list, the placeholder should be moved to the new location. Add this to touchMove()
:
Repositioning the item
Finally, when the user releases the item, we need to revert it back to its normal position, and if the position has changed call the callback function. The controller’s implementation of the function should re-order the items in the source data, which will cause Angular to refresh the DOM. Add this to touchRelease()
:
Finishing touches
At this point, we have a basic implementation but we’re missing some finishing touches.
Auto-scroll main window
When the user drags to near the top or bottom of the screen, it should automatically scroll the main window up and down.
I achieved this by setting up a timer interval (setInterval
) during the re-order operation, that looks at the current touch position and scrolls if it’s within a certain distance of the top or bottom. See the autoScroll()
function in the final solution for more details of the implementation.
Animating the items during re-ordering
For a little bit more polish, it would be nice if the items animated to their new positions as the user drags up and down.
This actually gets a little bit fiddly. My solution uses two placeholder
elements as the position changes. The new placeholder starts at height zero and grows to full height, while the old placeholder shrinks to zero. There are some problems with this approach though:
One problem is that the cards have a top and bottom margin. While the old placeholder shrinks down to zero height, it still leaves an extra margin between the two items that are moving together - which means the animation doesn’t quite get them close enough. My solution was to read the topMargin()
of the dragged item, and use that as a negative margin on the placeholder element, while adding the same amount to its height. That way, when the height shrinks to zero, it includes the extra margin space.
A related problem is that some browsers allow space for a zero-height element with a margin, while others don’t. This leads to inconsistent behaviour. I worked around it by adding an additional pixel to the topMargin()
adjustment above, then animating to 1px height instead.
Efficient animations using CSS transitions
Finally, to make sure the animations are smooth, it’s best to make them css transitions, since many browsers are able to optimise those - some even using hardware acceleration.
This is actually pretty easy to do - we just need to make sure the placeholder
class has the transition setting applied:
.placeholder {
-moz-transition: all 200ms ease-in-out;
-o-transition: all 200ms ease-in-out;
-webkit-transition: all 200ms ease-in-out;
transition: all 200ms ease-in-out;
}
With that done, we can set the height of the element directly, and wait for the animation to complete. Please see the final solution for more details of the implementation.
Touch-hold threshold
The default threshold for the hold
event in Ionic is 1. That means that if the touch point moves by more than a pixel, the hold event will be cancelled. In practice that can be quite hard for users to do (and may vary between devices).
The default threshold can be adjusted by adding this line to the start of app.js
:
Conclusion
The replacement to Ionic’s default list reordering functionality is a stand-alone directive that’s easy to drop in to any project. The new process is easy and natural, and looks great.