packery
packery copied to clipboard
Save & restore dragged item positions
First off, thank you very much for this library. It's awesome.
My use case for Packery is having a dashboard of widgets that users can drag and drop to re-arrange their order and layout. The order is persisted to a database. This works great for the most part, but there are cases where the layout changes after refreshing because the whitespace isn't preserved. For example, given the following:
If a user moves widget 'c' to the right, like so:
and then refreshes, widget 'c' ends up back on the left, because all that's persisted is the order and that doesn't actually change between the original layout and dragging 'c' to the right.
Here is a reduced test case demonstrating this. Note that I don't expect the code in this test case to preserve the white space, but am wondering if there is a way to persist the whitespace so that the widgets display in the same layout upon refreshing in which they are left after dragging and dropping.
I think that #179 might help with this, using getItemElements
(or perhaps a new API that includes the dummy elements) to persist the order of the widgets interspersed with dummy elements. Or if there is a way to do this currently, please advise. Thank you!
Thanks for reporting this issue.
Saving and restoring dragged item positions can be complicated, but it's doable. See Save & restore drag position demo, or simpler save & restore demo that uses fixed pixel sizes
This demo saves item positions according to an HTML attribute, in this case data-item-id
<div class="grid-item" data-item-id="1"></div>
<div class="grid-item" data-item-id="2"></div>
<div class="grid-item" data-item-id="3"></div>
Two custom methods are added to Packery, getShiftPositions
and initShiftLayout
.
// get JSON-friendly data for items positions
Packery.prototype.getShiftPositions = function( attrName ) {
attrName = attrName || 'id';
var _this = this;
return this.items.map( function( item ) {
return {
attr: item.element.getAttribute( attrName ),
x: item.rect.x / _this.packer.width
}
});
};
Packery.prototype.initShiftLayout = function( positions, attr ) {
if ( !positions ) {
// if no initial positions, run packery layout
this.layout();
return;
}
// parse string to JSON
if ( typeof positions == 'string' ) {
try {
positions = JSON.parse( positions );
} catch( error ) {
console.error( 'JSON parse error: ' + error );
this.layout();
return;
}
}
attr = attr || 'id'; // default to id attribute
this._resetLayout();
// set item order and horizontal position from saved positions
this.items = positions.map( function( itemPosition ) {
var selector = '[' + attr + '="' + itemPosition.attr + '"]'
var itemElem = this.element.querySelector( selector );
var item = this.getItem( itemElem );
item.rect.x = itemPosition.x * this.packer.width;
return item;
}, this );
this.shiftLayout();
};
Initialize Packery, but disable initLayout
. Then get saved position data, in this case from localStorage
. initShiftLayout
requires position object and the HTML attribute (data-item-id
).
// init Packery
var $grid = $('.grid').packery({
itemSelector: '.grid-item',
columnWidth: '.grid-sizer',
percentPosition: true,
initLayout: false // disable initial layout
});
// get saved dragged positions
var initPositions = localStorage.getItem('dragPositions');
// init layout with saved positions
$grid.packery( 'initShiftLayout', initPositions, 'data-item-id' );
Make items draggable. On dragItemPositioned
, save item position to localStorage. The custom getShiftPositions
requires that HTML attribute.
// make draggable
$grid.find('.grid-item').each( function( i, itemElem ) {
var draggie = new Draggabilly( itemElem );
$grid.packery( 'bindDraggabillyEvents', draggie );
});
// save drag positions on event
$grid.on( 'dragItemPositioned', function() {
// save drag positions
var positions = $grid.packery( 'getShiftPositions', 'data-item-id' );
localStorage.setItem( 'dragPositions', JSON.stringify( positions ) );
});
It works, but it's not 100% operational with the rest of Packery:
- Having different layouts is not supported. You can't have 2 columns for mobile and 4 columns for desktop.
- Adding items via server is not supported.
- Horizontal layouts and right-to-left layouts are not supported.
+1 or add a 👍 reaction to this issue if you'd like to see this functionality properly get added to Packery.
+1
+1
+1
+1
Works great with angular JS - look forward to resolving my other issue about sizing each element and I'm all set :) thank you!
+1 Love I would love to see this!
+1
+1
+1
+1
For those adding +1 here, I am already using the code that @desandro posted above - it works very well for me. Is something missing in it?
@pliablepixels It should work with the demo provided, but there many edge cases it may not be able to handle: i.e. if there's a discrepancy between the current items and the saved position data to start with.
yup, thats true, I am handling that at app level and am using your code for production, but would love to see this as a formal feature 👍
+1
+1
+1
@desandro Thanks for the code snippets, helped me get persistent dashboards much quicker =) ... look forward to state persistence feature at some point.
+1
+1 Adding items via server would be amazing!
+1 I am storing and retrieving the order in a database but would really like to store and retrieve the layout (including white space).
+1
How I solved this: I'm storing the top and left CSS attributes to the database. When loading my dashboard I add a data-top and data-left atribute to my div which is representing a grid element. Then when initializing packery and going trough all grid elements to make it draggable with draggabilly I use packeries fit method together with the top and left value to position each grid element.
$grid.find('.grid-item').each( function( i, gridItem ) {
var top = $(gridItem).attr('data-top'),
left = $(gridItem).attr('data-left'),
draggie = new Draggabilly( gridItem );
$grid.packery('bindDraggabillyEvents', draggie);
$grid.packery('fit', gridItem, left, top);
});
@smeyerSL that won't position them properly when loading them in a different resolution. Which is quite common if you use multiple screens for the same computer.
It's about persisting the exact position of every grid-item relative to the grid container they are in. When you change the screen resolution then you can still allow to reposition the items. But at first I want to persist and restore a specific state. And thats a quite simple way to achieve that in a quick, maybe basic manner.
From @ashubham
So the solution mentioned in #337 seems to fail, when there is a gutter with %age position. http://codepen.io/anon/pen/XKLWJY
Try, make the screen small, drag position and then make the screen bigger and refresh, In some configurations one can find that the positions are now staggered.
The percentage position of the items is not maintained with screen size. There are slight rounding errors like with a gutter of 1%, two column 49.5% items should be, left: 0% and left:50.5% respectively. But its like left:0% and left:50.348% or something similar depending on the screensize.
could this be adapted to save the positions to database rather than local storage? looking at something a little like wordpress behaviour
@Firestorm-Graphics Yes. The demo uses Local storage as an example. So long as you can format the data in the same manner it should work.
@markwebsterUK - hmm I don't seem to have your problem. Here is a video - I'm rearranging both size and order and it works fine (you will note after re-ordering, I exit the app and restart to reload). I'm facing other issues with imagesLoading firing early, but not this.
@pliablepixels yeah, realised it was me messing with the function to try and account for new/missing blocks that broke the ordering. I deleted the comment, but apparently you're too quick off the mark!