Yii2 Sortable
=============

[![Latest Stable Version](https://poser.pugx.org/sjaakp/yii2-sortable-behavior/v/stable)](https://packagist.org/packages/sjaakp/yii2-sortable-behavior)
[![Total Downloads](https://poser.pugx.org/sjaakp/yii2-sortable-behavior/downloads)](https://packagist.org/packages/sjaakp/yii2-sortable-behavior)
[![License](https://poser.pugx.org/sjaakp/yii2-sortable-behavior/license)](https://packagist.org/packages/sjaakp/yii2-sortable-behavior)

This package contains five classes to handle the sorting of ActiveRecords:

- **SortableGridView** - extended GridView widget;
- **SortableListView** - extended ListView widget;
- **Sortable** - ActiveRecord Behavior to handle the sorting of the records themselves, or of the one-to-many related records;
- **PivotRecord** - base class for the ActiveRecord of the pivot table in a many-to-many relation.
- **MMSortable** - ActiveRecord Behavior to handle the sorting of many-to-many related records.

The previous version of **Sortable** was dependent on [jQuery](https://jquery.com/). It now has 
no dependencies at all. It should work in all modern desktop browsers. At the time of writing the new HTML Drag and Drop, and thus **Sortable**,
only works for few
[mobile browsers](https://developer.mozilla.org/en-US/docs/Web/API/Document/dragenter_event#browser_compatibility).

The old [jQuery-dependent widgets](#sortable-with-jquery) are still available.
Some may prefer their esthetics.


A demonstration of the **Sortable** suit is [here](http://www.sjaakpriester.nl/director/index).

## Installation ##

The preferred way to install **Sortable** is through [Composer](https://getcomposer.org/). Either add the following to the `require` section of your `composer.json` file:

`"sjaakp/yii2-sortable-behavior": "*"` 

Or run:

`composer require sjaakp/yii2-sortable-behavior "*"` 

You can manually install **Sortable** by [downloading the source in ZIP-format](https://github.com/sjaakp/yii2-sortable-behavior/archive/master.zip).

## SortableGridView and SortableListView ##

These widgets are derived from the standard GridView and ListView classes, 
but have one extra capability: the items can be moved to another position 
by means of drag and drop (using 
[HTML Drag and Drop](https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API)
functionality). 
If an item is dropped on a new position, a message is posted with the following data:

- `key`: the value of the item's primary key;
- `pos`: the zero-indexed new position of the item.

**SortableGridView** and **SortableListView** have one configurable property:

#### orderUrl ####

`array|string`. The URL which is called after a sorting operation.
The format is that of `yii\helpers\Url::toRoute`.

**SortableGridView** and **SortableListView** don't go together with 
the [Pjax-widget](https://www.yiiframework.com/doc/api/2.0/yii-widgets-pjax).
However, **Sortable** is not very usable with paged data-widgets anyway. 

## Sortable ##

With this Behavior, an ActiveRecord becomes 'sortable'. It has one configurable property and one extra method:

#### orderAttribute ####

`string|array`. The order attribute(s) of the ActiveRecord.

This can take the following values:

 - `string` - the order attribute name;
 - `array` of:
     * `string` - the order attribute name,
     * `foreignKeyName => orderAttrName` - limit ordering to ActiveRecords with the same foreign key value, i.e. of the same owner.

Default is `"ord"`. 

#### order() ####

`public function order( $newPosition, $foreignKeyName = null, where = [] )` 

This method puts the owner on the new position `$newPosition` by manipulating the order attribute. The order attribute is a zero-indexed, contiguous integer.

If `$foreignKeyName` is `null` (default) all the records are ordered. 

If it is a string, the ordering is restricted to the records with the same 
value of `$foreignKeyName`. `$foreignKeyName` must be a key in `orderAttribute`. 
This comes in handy with one-to-many relations.

`$where` is an array of extra conditions in the format used by  
[QueryInterface `where()`](https://www.yiiframework.com/doc/api/2.0/yii-db-queryinterface#where()-detail). May 
be used to add extra foreign key restraints. Default: `[ ]` (empty array).

----------

### Usage scenario 1  ###
## Simple sorting ##

Suppose we have a very simple table of movie titles:

    CREATE TABLE movie (
  	  id int(10) unsigned NOT NULL AUTO_INCREMENT,
  	  ord int(10) unsigned NOT NULL,
  	  title tinytext NOT NULL,
	  PRIMARY KEY (id)
	)

Where `ord` will be our order attribute.

We can make the `Movie` ActiveRecord sortable like this:

	class Movie extends ActiveRecord
	{
	    public function behaviors( ) {
    	    return [
    	        [
    	            'class' => 'sjaakp\sortable\Sortable',
    	        ],
    	    ];
    	}
		...
	}

In the controller we define an `index` action and an `order` action:

	class MovieController extends Controller
	{
		...
	    public function actionIndex( )
    	{
    	    $dataProvider = new ActiveDataProvider( [
    	        'query' => Movie::find( )->orderBy( 'ord' ),	// notice the orderBy clause
    	        'sort' => false,
    	        'pagination' => false
    	    ] );

    	    return $this->render( 'index', [
    	        'dataProvider' => $dataProvider,
    	    ] );
    	}
		...
	    public function actionOrder( )   {
	        $post = Yii::$app->request->post( );
	        if (isset( $post['key'], $post['pos'] ))   {
	            $this->findModel( $post['key'] )->order( $post['pos'] );
	        }
	    }
		...
	}

In the `index` view, we use a **SortableGridView**:

    use sjaakp\sortable\SortableGridView;
	...
    <?= SortableGridView::widget( [
        'dataProvider' => $dataProvider,
        'orderUrl' => ['order'],
        'columns' => [
			...
            'title:ntext',
			...
        ],
		...
    ] ); ?>

And bingo! The list of movie titles is now sortable by drag and drop.

----------

### Usage scenario 2  ###
## One-to-many sorting ##

Suppose we also have a list of directors. Each director has many movies, each movie belongs to one director (thinking of the Coen brothers, I know this is not necessarily true in reality).

We add two columns to our `movie` table:

    CREATE TABLE movie (
  	  id int(10) unsigned NOT NULL AUTO_INCREMENT,
  	  ord int(10) unsigned NOT NULL,
  	  title tinytext NOT NULL,
	  director_id int(10) unsigned NOT NULL,
	  director_ord int(10) unsigned NOT NULL,
	  PRIMARY KEY (id)
	)

Where `director_ord` is the order attribute just for movies belonging to the same director.

In the `Director` model we define a one-to-many relation, like we would normally do (notice the `orderBy` clause): 

	class Director extends ActiveRecord	{
		...
    	public function getMovies( ) {
        	return $this->hasMany( Movie::class, ['director_id' => 'id'] )
        	    ->orderBy( 'director_ord' );
    	}
		...
	}

The `Movie` model is sortable like before, but with another `orderAttribute`:

	class Movie extends ActiveRecord
	{
	    public function behaviors( ) {
    	    return [
    	        [
    	            'class' => 'sjaakp\sortable\Sortable',
	                'orderAttribute' => [
	                    'director_id' => 'director_ord'
    	            ]
    	        ],
    	    ];
    	}
		...
	}

This time, `DirectorController` sports an extra action:

	class DirectorController extends Controller
	{
		...
	    public function actionMovieOrder( )   {
	        $post = Yii::$app->request->post( );
	        if (isset( $post['key'], $post['pos'] ))   {
	            $movie = Movie::findOne( $post['key'] );
	            if ($movie) $movie->order( $post['pos'], 'director_id' );
	        }
	    }
		...
	}

Let's now use a **SortableGridView** to display all the movies of the director in `director/view`:

	use sjaakp\sortable\SortableGridView;
	...
	$movies = new ActiveDataProvider( [
    	'query' => $model->getMovies( ),  // Do not use $model->movies, it returns array of Movies in stead of an ActiveQueryInterface
    	'sort' => false,
    	'pagination' => false
	] );
	...
    <h1><?= Html::encode( $model->name ) ?></h1>
	...
    <?= SortableGridView::widget( [
        'dataProvider' => $movies,
        'orderUrl' => ['movie-order'],
        'columns' => [
			...
            'title:ntext',
			...
        ],
		...
    ] ); ?>
	
Now each director's view shows a sortable list of his or her movies.

It's easy to combine Usage scenario's 1 and 2, so that *all* movies are sortable in `movie/index` and only the director's movies in `director/view`. Just initialize `Movie`'s **Sortable** behavior like this:

	class Movie extends ActiveRecord
	{
	    public function behaviors( ) {
    	    return [
    	        [
    	            'class' => 'sjaakp\sortable\Sortable',
	                'orderAttribute' => [
						'ord',
	                    'director_id' => 'director_ord'
    	            ]
    	        ],
    	    ];
    	}
		...
	}

----------

## PivotRecord ##

This is the base ActiveRecord for the pivot table of two sortable Models in a many-to-many relation.

The ordering information is stored in the pivot table as well.

A pivot table might look something like this:

	CREATE TABLE actor_movie (
	  actor_id int(10) unsigned NOT NULL,	# actor's primary key
	  movie_id int(10) unsigned NOT NULL,	# movie's primary key
	  actor_ord int(10) unsigned NOT NULL,	# actor's order
	  movie_ord int(10) unsigned NOT NULL,	# movie's order
	  PRIMARY KEY (actor_id,movie_id),
	)

Using best practices it means:

- the **table name** is a concatenation of the two related table names in lexicographic order, separated by an underscore (`'_'`);
- the **primary key** column names consist of the related table name followed by `'_id'`;
- the **order** column names consist of the related table name followed by `'_ord'`;

Of course, it would be wise to add some indexes.

A concrete pivot record *has* to be derived  from **PivotRecord**. Two static functions *must* be defined in the derived class:

#### aClass() and bClass() ####

`protected static function aClass( )` 
`protected static function bClass( )` 

These static member functions should return the fully qualified class names of the related Models.

`aClass` and `bClass` are completely equivalent. **PivotRecord** is in any respect a symmetric class.

A complete definition of a pivot record might look like this:

	namespace app\models;
	use sjaakp\sortable\PivotRecord;

	class MovieActor extends PivotRecord    {

	    protected static function aClass( )   {
	        return Movie::class;
	    }

	    protected static function bClass( )   {
	        return Actor::class;
	    }
	}

Notice that you can define some other static values as well, for special cases. Refer to the source code if you need this.

A **PivotRecord**-derived class has the following extra functions.

#### getAs() and getBs() ####

`public static function getAs( ActiveRecord $b )` 

Get the ordered records of `classA` belonging to `classB $b`.

`public static function getBs( ActiveRecord $a )` 

Get the ordered records of `classB` belonging to `classA $a`.

The result is returned as an ActiveQuery, which can be modified further, or used as source of an ActiveDataProvider.

Notice these are **static** functions, not referring to any instantiation of the **PivotRecord**-derived class. 

#### orderA() and orderB() ####

`public function orderA( $newPosition )` 

Place `classA` at `$newPosition` in the list of all `classA`'s belonging to `classB`.

`public function orderB( $newPosition )` 

Place `classB` at `$newPosition` in the list of all `classB`'s belonging to `classA`.

These are **member** functions. The id's of `classA` and `classB` are stored in the current **PivotRecord**.


## MMSortable ##

This is a Behavior of both partner ActiveRecords in a many-to-many relations. **PivotRecord** relies on it. **MMSortable** performs some housekeeping and has no (interesting) member functions. However, two properties *have* to be configured:

#### pivotClass ####

`string`. The fully classified class name of the  pivot class (the **PivotRecord**-derived class).

#### pivotIdAttr ####

`string`. The attribute name of the owner's id in the pivot class. If this is not set, it will be derived from the owner's class name; for instance: if the owner is class `Movie`, `$pivotIdAttr` will be `"movie_id"`.

----------

### Usage scenario 3  ###
## Many-to-many sorting ##

Apart from our `movie` table, we also have an `actor` table. They are linked via an `actor_movie` pivot table: each movie can have many actors, and each actor can have many movies.

First, we define a pivot class, like so: 

	namespace app\models;
	use sjaakp\sortable\PivotRecord;

	class MovieActor extends PivotRecord    {

	    protected static function aClass( )   {
	        return Movie::class;
	    }

	    protected static function bClass( )   {
	        return Actor::class;
	    }
	}

Then, we make sure that both `Movie` and `Actor` have a **MMSortable** Behavior:

	class Movie extends ActiveRecord	{
		...
	    public function behaviors( ) {
	        return [
	            [
	                'class' => 'sjaakp\sortable\MMSortable',
	                'pivotClass' => MovieActor::class
	            ]
	        ];
	    }
		...
	}

For convenience, we add a very simple member function to `Movie`:

	class Movie extends ActiveRecord	{
		...
	    public function getActors( ) {
	        return MovieActor::getBs( $this );
	    }
		...
	}

Define an `order-actor`-action in `MovieController`:

	class MovieController extends Controller
	{
		...
	    public function actionOrderActor( $id )   {
	        $post = Yii::$app->request->post( );
	        if (isset( $post['key'], $post['pos'] ))   {
	            $piv = MovieActor::find( )->where( [
	                'movie_id' => $id,
	                'actor_id' => $post['key']
	            ] )->one( );
	            $piv->orderB( $post['pos'] );
	        }
	    }
		...
	}

Now, in `movie/view`, we can display a **SortableGridView** with all the actors appearing in the movie.

	use sjaakp\sortable\SortableGridView;
	...
	$actors = new ActiveDataProvider( [
    	'query' => $model->getActors( ),
    	'sort' => false,
    	'pagination' => false
	] );
	...
    <h1><?= Html::encode( $model->title ) ?></h1>
	...
    <?= SortableGridView::widget( [
        'dataProvider' => $actors,
        'orderUrl' => ['order-actor', 'id' => $model->getPrimaryKey()],
        'columns' => [
			...
            'name:ntext',
			...
        ],
		...
    ] ); ?>

---------------------

## Sortable with jQuery ##

The previous version of **Sortable** (1.0) used 
[jQuery Draggable and Sortable](https://jqueryui.com/draggable/#sortable). 
The old jQuery-widgets are still available as **SortableGridViewJquery**
and **SortableListViewJquery**. They are exchangeable with their non-jQuery
counterparts.

You may prefer the esthetics of the jQuery variants. Also, the new 
HTML Drag and Drop may not work for all 
[mobile browsers](https://developer.mozilla.org/en-US/docs/Web/API/Document/dragenter_event#browser_compatibility).

**SortableGridViewJquery** and **SortableListViewJquery** have two extra 
configurable properties:

#### sortOptions ####

`array`. The options for the jQuery sortable object. See [https://api.jqueryui.com/sortable/](http://api.jqueryui.com/sortable/).

Notice that the options `'items'`, `'helper'`, and `'update'` will be overwritten.

Default: `[]` (empty array).

#### sortAxis ####

`boolean|string` The `'axis'` option for the jQuery sortable. 
If `false`, it is not set. Default: `'y'`.

For compatibility, **SortableGridView** and **SortableListView** have these
options as well, but they are not functional.

----------------

## Thanks ##

- **mike-kramer** (sortAxis option)
- **menshakov** (use updateAttributes)
- **robsch** (subtle order bug)
- **rubenheymans** (multiple foreign keys idea)
