packery icon indicating copy to clipboard operation
packery copied to clipboard

Save & restore dragged item positions

Open chrisbreiding opened this issue 9 years ago • 61 comments

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:

original layout

If a user moves widget 'c' to the right, like so:

after dragging c to right

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!

chrisbreiding avatar Feb 22 '16 16:02 chrisbreiding

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:

  1. Having different layouts is not supported. You can't have 2 columns for mobile and 4 columns for desktop.
  2. Adding items via server is not supported.
  3. 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.

desandro avatar Feb 22 '16 18:02 desandro

+1

iliyaZelenko avatar Feb 22 '16 20:02 iliyaZelenko

+1

GarrisonJ avatar Mar 01 '16 21:03 GarrisonJ

+1

DannyR83 avatar Mar 01 '16 22:03 DannyR83

+1

pliablepixels avatar Mar 02 '16 17:03 pliablepixels

Works great with angular JS - look forward to resolving my other issue about sizing each element and I'm all set :) thank you!

pliablepixels avatar Mar 06 '16 21:03 pliablepixels

+1 Love I would love to see this!

maclof avatar Mar 12 '16 16:03 maclof

+1

mihanshilov avatar Mar 21 '16 12:03 mihanshilov

+1

sameerpanjwani avatar Apr 13 '16 06:04 sameerpanjwani

+1

creisman avatar Apr 15 '16 03:04 creisman

+1

coolwebs avatar Apr 18 '16 05:04 coolwebs

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 avatar Apr 18 '16 13:04 pliablepixels

@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.

desandro avatar Apr 18 '16 14:04 desandro

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 👍

pliablepixels avatar Apr 18 '16 15:04 pliablepixels

+1

alvarotrigo avatar Apr 21 '16 16:04 alvarotrigo

+1

umbertoo avatar May 03 '16 12:05 umbertoo

+1

mbeaudru avatar May 25 '16 12:05 mbeaudru

@desandro Thanks for the code snippets, helped me get persistent dashboards much quicker =) ... look forward to state persistence feature at some point.

afreeland avatar May 26 '16 16:05 afreeland

+1

elira avatar Jun 14 '16 07:06 elira

+1 Adding items via server would be amazing!

iciodesign avatar Jun 29 '16 08:06 iciodesign

+1 I am storing and retrieving the order in a database but would really like to store and retrieve the layout (including white space).

jeffaltman avatar Jun 29 '16 17:06 jeffaltman

+1

AliceIW avatar Jul 06 '16 12:07 AliceIW

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

ghost avatar Jul 22 '16 03:07 ghost

@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.

alvarotrigo avatar Jul 22 '16 09:07 alvarotrigo

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.

ghost avatar Jul 22 '16 15:07 ghost

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.

desandro avatar Aug 26 '16 18:08 desandro

could this be adapted to save the positions to database rather than local storage? looking at something a little like wordpress behaviour

Firestorm-Graphics avatar Aug 26 '16 19:08 Firestorm-Graphics

@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.

desandro avatar Aug 28 '16 15:08 desandro

@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 avatar Oct 12 '16 16:10 pliablepixels

@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!

markwebsterUK avatar Oct 12 '16 16:10 markwebsterUK