From 0b3f20279c947a7b514550a2e4af83c979df792b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jir=CC=8Ci=CC=81=20Svoboda?= Date: Mon, 5 Jan 2015 11:27:53 +0100 Subject: [PATCH] initial version --- .gitignore | 3 + Module.php | 31 +++ components/Action.php | 28 +++ components/Behavior.php | 302 +++++++++++++++++++++++++++++ composer.json | 26 +++ composer.lock | 411 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 801 insertions(+) create mode 100644 Module.php create mode 100644 components/Action.php create mode 100644 components/Behavior.php create mode 100644 composer.json create mode 100644 composer.lock diff --git a/.gitignore b/.gitignore index 70f0875..8832773 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ protected/runtime/* !protected/runtime/.gitignore protected/data/*.db themes/classic/views/ +/nbproject/private/ +/nbproject/ +/vendor/ \ No newline at end of file diff --git a/Module.php b/Module.php new file mode 100644 index 0000000..bf30a45 --- /dev/null +++ b/Module.php @@ -0,0 +1,31 @@ + 'Jiri Svoboda', + ]; + } + +} diff --git a/components/Action.php b/components/Action.php new file mode 100644 index 0000000..c55bc6a --- /dev/null +++ b/components/Action.php @@ -0,0 +1,28 @@ +model === null) + { + throw new CException('Model class must be specified.'); + } + + $this->model->setSortOrder(); + } + +} diff --git a/components/Behavior.php b/components/Behavior.php new file mode 100644 index 0000000..d772383 --- /dev/null +++ b/components/Behavior.php @@ -0,0 +1,302 @@ + [ + * 'class' => \dlds\sortable\components\Behavior::classname(), + * 'column' => 'position', + * ], + * + * @author Jiri Svoboda + */ +class Behavior extends \yii\base\Behavior { + + /** + * @var string name of attr to be used as key + */ + public $key; + + /** + * @var mixed restriction array condition + */ + public $restrictions = array(); + + /** + * @var string db table sort column + */ + public $column = 'sortOrder'; + + /** + * @var string post array index of sort items + */ + public $index = 'sortItems'; + + /** + * Before save + */ + public function beforeValidate($event) + { + // TODO: pack this in extension BoostedGridView + if ($this->owner->isNewRecord) + { + $this->owner->{$this->column} = $this->getMaxSortOrder() + 1; + } + + return parent::beforeValidate($event); + } + + /** + * After delete + */ + public function afterDelete($event) + { + $restrictions = []; + + $this->_pullRestrictions($this->owner, $restrictions); + + // TODO: avoid multiple calls of fixSortGaps when deleting multiple models in once + $this->_fixSortGaps($restrictions); + + parent::afterDelete($event); + } + + /** + * Retrieves model sort ID + * @return string model sort ID + */ + public function getOwnerKey() + { + if (isset($this->key)) + { + return $this->owner->{$this->key}; + } + + if (is_array($this->owner->primaryKey)) + { + throw new Exception('GSortableBehavior owner primaryKey is an array - you have to set sortAttrID'); + } + + return $this->owner->primaryKey; + } + + /** + * Retrieves model sort ID attr name + * @return string model sort ID attr name + */ + public function getOwnerKeyAttr() + { + if (isset($this->key)) + { + return $this->key; + } + + if (is_array($this->owner->primaryKey)) + { + throw new Exception('GSortableBehavior owner primaryKey is an array - you have to set sortAttrID'); + } + + return $this->owner->tableSchema->primaryKey; + } + + /** + * Retrieves current owner sortOrder + * @param boolean $reversed if sortOrder should be retrieves in normal or reverse order + * @return int current sortOrder + */ + public function getSortOrder($reversed = false) + { + if ($reversed) + { + $restrictions = []; + + $this->_pullRestrictions($this->owner, $restrictions); + + return ($this->getMaxSortOrder([], $restrictions) + 1) - $this->owner->{$this->column}; + } + + return $this->owner->{$this->column}; + } + + /** + * Retrieves maximum sortOrder value for + * @param $items items which will be included + * @return int max sortOrder + */ + public function getMaxSortOrder($items = [], $restrictions = []) + { + /* @var $connection \yii\db\Connection */ + $command = \Yii::$app->db->createCommand('SELECT MAX(' . $this->column . ')')->from($this->owner->tableName()); + + if (!empty($items)) + { + $command->where(['in', $this->getOwnerKeyAttr(), $items]); + } + + if (!empty($restrictions)) + { + foreach ($restrictions as $column => $values) + { + $command->andWhere(['in', $column, $values]); + } + } + + return (int) $command->queryScalar(); + } + + /** + * Resets sortOrder and sets it to model ID value + * @param array $items defines which models should be included, if is empty it includes all models + * @return int + */ + public function resetSortOrder($items = []) + { + $sql = 'UPDATE ' . $this->owner->tableName() . ' SET ' . $this->column . ' = ' . $this->getOwnerKeyAttr(); + + if (!empty($items)) + { + $sql .= ' WHERE ' . $this->getOwnerKeyAttr() . ' IN (' . implode(', ', $items) . ')'; + } + + return \Yii::$app->db->createCommand($sql)->execute(); + } + + /** + * Sets sort order according to provided data in POST + */ + public function setSortOrder() + { + $itemKeys = Yii::app()->request->getPost($this->index, false); + + if ($itemKeys && is_array($itemKeys)) + { + $transaction = $this->owner->dbConnection->beginTransaction(); + + $maxSortOrder = $this->getMaxSortOrder($itemKeys); + + if (!is_numeric($maxSortOrder) || $maxSortOrder == 0) + { + $this->resetSortOrder($itemKeys); + } + + $currentModels = $this->_getCurrentModels($itemKeys); + + $restrictions = []; + + for ($i = 0; $i < count($itemKeys); $i++) + { + $model = $this->owner->findByAttributes([ + $this->getOwnerKeyAttr() => $itemKeys[$i], + ]); + + $this->_pullRestrictions($model, $restrictions); + + if ($model->{$this->column} != $currentModels[$i]->{$this->column}) + { + $model->{$this->column} = $currentModels[$i]->{$this->column}; + + if (!$model->save()) + { + $transaction->rollback(); + + throw new Exception('Cannot set model sort order.'); + } + } + } + + $transaction->commit(); + + $this->_fixSortGaps($restrictions); + } + } + + /** + * Retrieves all current models + * @param array $items current models keys + * @return array current models + */ + private function _getCurrentModels($items = []) + { + $criteria = new DbCriteria; + $criteria->addInCondition(sprintf('%s.%s', $this->owner->getTableAlias(), $this->getOwnerKeyAttr()), $items); + $criteria->order = $this->column . ' DESC'; + + return $this->owner->find($criteria); + } + + /** + * Pulls restrictions from given model + * @param CModel $model given model + * @param array $restrictions given restrictions + */ + private function _pullRestrictions(CModel $model, &$restrictions) + { + foreach ($this->restrictions as $attr) + { + if (isset($model->{$attr}) && (!isset($restrictions[$attr]) || !in_array($model->{$attr}, $restrictions[$attr]))) + { + $restrictions[$attr][] = $model->{$attr}; + } + } + } + + /** + * Assignes given restrictions to given criteria + * @param array $restrictions given restrictions + * @param CDbCriteria $criteria given criteria + */ + private function _assignRestrictions($restrictions, &$criteria) + { + if (!empty($restrictions)) + { + foreach ($restrictions as $column => $values) + { + $criteria->addInCondition($column, $values); + } + } + } + + /** + * Fixes sort gaps which can occure after delete entry + */ + private function _fixSortGaps($restrictions = array()) + { + $transaction = $this->owner->dbConnection->beginTransaction(); + + $criteria = new CDbCriteria; + $criteria->order = $this->column; + + $this->_assignRestrictions($restrictions, $criteria); + + $models = $this->owner->findAll($criteria); + + $sortOrder = 1; + + foreach ($models as $model) + { + if ($model->{$this->column} != $sortOrder) + { + $model->{$this->column} = $sortOrder; + + $model->save(); + } + + $sortOrder++; + } + + $transaction->commit(); + } + +} + +?> diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..a8750e8 --- /dev/null +++ b/composer.json @@ -0,0 +1,26 @@ +{ + "name": "dlds/yii2-sortable", + "description": "Yii2 sortable module", + "keywords": ["yii2", "sortable", "models"], + "type": "yii2-extension", + "license": "BSD-3-Clause", + "support": { + "issues": "https://github.com/dlds/yii2-sortable/issues", + "wiki": "https://github.com/dlds/yii2-sortable/wiki", + "source": "https://github.com/dlds/yii2-sortable" + }, + "authors": [ + { + "name": "Jiri Svoboda", + "email": "jiri.svoboda@dlds.cz" + } + ], + "require": { + "yiisoft/yii2": "*" + }, + "autoload": { + "psr-4": { + "dlds\\sortable\\": "" + } + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..9c06799 --- /dev/null +++ b/composer.lock @@ -0,0 +1,411 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "hash": "9d0ff368ca21b4be03f8812af8a6943d", + "packages": [ + { + "name": "bower-asset/jquery", + "version": "2.1.3", + "source": { + "type": "git", + "url": "https://github.com/jquery/jquery.git", + "reference": "8f2a9d9272d6ed7f32d3a484740ab342c02541e0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jquery/jquery/zipball/8f2a9d9272d6ed7f32d3a484740ab342c02541e0", + "reference": "8f2a9d9272d6ed7f32d3a484740ab342c02541e0", + "shasum": "" + }, + "require-dev": { + "bower-asset/qunit": "1.14.0", + "bower-asset/requirejs": "2.1.10", + "bower-asset/sinon": "1.8.1", + "bower-asset/sizzle": "2.1.1-patch2" + }, + "type": "bower-asset-library", + "extra": { + "bower-asset-main": "dist/jquery.js", + "bower-asset-ignore": [ + "**/.*", + "build", + "speed", + "test", + "*.md", + "AUTHORS.txt", + "Gruntfile.js", + "package.json" + ] + }, + "license": [ + "MIT" + ], + "keywords": [ + "javascript", + "jquery", + "library" + ] + }, + { + "name": "bower-asset/jquery.inputmask", + "version": "3.1.49", + "source": { + "type": "git", + "url": "https://github.com/RobinHerbots/jquery.inputmask.git", + "reference": "4a0bda47031b9d2074b0c77c3b447df71ed1ca95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/RobinHerbots/jquery.inputmask/zipball/4a0bda47031b9d2074b0c77c3b447df71ed1ca95", + "reference": "4a0bda47031b9d2074b0c77c3b447df71ed1ca95", + "shasum": "" + }, + "require": { + "bower-asset/jquery": ">=1.7" + }, + "type": "bower-asset-library", + "extra": { + "bower-asset-main": [ + "./dist/inputmask/jquery.inputmask.js", + "./dist/inputmask/jquery.inputmask.extensions.js", + "./dist/inputmask/jquery.inputmask.date.extensions.js", + "./dist/inputmask/jquery.inputmask.numeric.extensions.js", + "./dist/inputmask/jquery.inputmask.phone.extensions.js", + "./dist/inputmask/jquery.inputmask.regex.extensions.js" + ], + "bower-asset-ignore": [ + "**/.*", + "qunit/", + "nuget/", + "tools/", + "js/", + "*.md", + "build.properties", + "build.xml", + "jquery.inputmask.jquery.json" + ] + }, + "license": [ + "http://opensource.org/licenses/mit-license.php" + ], + "description": "jquery.inputmask is a jquery plugin which create an input mask.", + "keywords": [ + "form", + "input", + "inputmask", + "jQuery", + "mask", + "plugins" + ] + }, + { + "name": "bower-asset/punycode", + "version": "v1.3.2", + "source": { + "type": "git", + "url": "https://github.com/bestiejs/punycode.js.git", + "reference": "38c8d3131a82567bfef18da09f7f4db68c84f8a3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bestiejs/punycode.js/zipball/38c8d3131a82567bfef18da09f7f4db68c84f8a3", + "reference": "38c8d3131a82567bfef18da09f7f4db68c84f8a3", + "shasum": "" + }, + "type": "bower-asset-library", + "extra": { + "bower-asset-main": "punycode.js", + "bower-asset-ignore": [ + "coverage", + "tests", + ".*", + "component.json", + "Gruntfile.js", + "node_modules", + "package.json" + ] + } + }, + { + "name": "bower-asset/yii2-pjax", + "version": "v2.0.2", + "source": { + "type": "git", + "url": "https://github.com/yiisoft/jquery-pjax.git", + "reference": "fb92be865c0fd6583714475cb7d629020749d73f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/yiisoft/jquery-pjax/zipball/fb92be865c0fd6583714475cb7d629020749d73f", + "reference": "fb92be865c0fd6583714475cb7d629020749d73f", + "shasum": "" + }, + "require": { + "bower-asset/jquery": ">=1.8" + }, + "type": "bower-asset-library", + "extra": { + "bower-asset-main": "./jquery.pjax.js", + "bower-asset-ignore": [ + ".travis.yml", + "Gemfile", + "Gemfile.lock", + "vendor/", + "script/", + "test/" + ] + } + }, + { + "name": "cebe/markdown", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/cebe/markdown.git", + "reference": "9d6c36d6623497523ed421a31d940bc1d7435578" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cebe/markdown/zipball/9d6c36d6623497523ed421a31d940bc1d7435578", + "reference": "9d6c36d6623497523ed421a31d940bc1d7435578", + "shasum": "" + }, + "require": { + "lib-pcre": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "cebe/indent": "*", + "facebook/xhprof": "*@dev", + "phpunit/phpunit": "3.7.*" + }, + "bin": [ + "bin/markdown" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "cebe\\markdown\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Carsten Brandt", + "email": "mail@cebe.cc", + "homepage": "http://cebe.cc/", + "role": "Creator" + } + ], + "description": "A super fast, highly extensible markdown parser for PHP", + "homepage": "https://github.com/cebe/markdown#readme", + "keywords": [ + "extensible", + "fast", + "gfm", + "markdown", + "markdown-extra" + ], + "time": "2014-10-25 16:16:49" + }, + { + "name": "ezyang/htmlpurifier", + "version": "v4.6.0", + "source": { + "type": "git", + "url": "https://github.com/ezyang/htmlpurifier.git", + "reference": "6f389f0f25b90d0b495308efcfa073981177f0fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/6f389f0f25b90d0b495308efcfa073981177f0fd", + "reference": "6f389f0f25b90d0b495308efcfa073981177f0fd", + "shasum": "" + }, + "require": { + "php": ">=5.2" + }, + "type": "library", + "autoload": { + "psr-0": { + "HTMLPurifier": "library/" + }, + "files": [ + "library/HTMLPurifier.composer.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL" + ], + "authors": [ + { + "name": "Edward Z. Yang", + "email": "admin@htmlpurifier.org", + "homepage": "http://ezyang.com", + "role": "Developer" + } + ], + "description": "Standards compliant HTML filter written in PHP", + "homepage": "http://htmlpurifier.org/", + "keywords": [ + "html" + ], + "time": "2013-11-30 08:25:19" + }, + { + "name": "yiisoft/yii2", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/yiisoft/yii2-framework.git", + "reference": "7ed175b4b71ac96eaf86aadc322186ecdc58498d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/yiisoft/yii2-framework/zipball/7ed175b4b71ac96eaf86aadc322186ecdc58498d", + "reference": "7ed175b4b71ac96eaf86aadc322186ecdc58498d", + "shasum": "" + }, + "require": { + "bower-asset/jquery": "2.1.*@stable | 1.11.*@stable", + "bower-asset/jquery.inputmask": "3.1.*", + "bower-asset/punycode": "1.3.*", + "bower-asset/yii2-pjax": ">=2.0.1", + "cebe/markdown": "~1.0.0", + "ext-mbstring": "*", + "ezyang/htmlpurifier": "4.6.*", + "lib-pcre": "*", + "php": ">=5.4.0", + "yiisoft/yii2-composer": "*" + }, + "bin": [ + "yii" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "yii\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Qiang Xue", + "email": "qiang.xue@gmail.com", + "homepage": "http://www.yiiframework.com/", + "role": "Founder and project lead" + }, + { + "name": "Alexander Makarov", + "email": "sam@rmcreative.ru", + "homepage": "http://rmcreative.ru/", + "role": "Core framework development" + }, + { + "name": "Maurizio Domba", + "homepage": "http://mdomba.info/", + "role": "Core framework development" + }, + { + "name": "Carsten Brandt", + "email": "mail@cebe.cc", + "homepage": "http://cebe.cc/", + "role": "Core framework development" + }, + { + "name": "Timur Ruziev", + "email": "resurtm@gmail.com", + "homepage": "http://resurtm.com/", + "role": "Core framework development" + }, + { + "name": "Paul Klimov", + "email": "klimov.paul@gmail.com", + "role": "Core framework development" + } + ], + "description": "Yii PHP Framework Version 2", + "homepage": "http://www.yiiframework.com/", + "keywords": [ + "framework", + "yii2" + ], + "time": "2014-12-07 16:42:41" + }, + { + "name": "yiisoft/yii2-composer", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/yiisoft/yii2-composer.git", + "reference": "7f300dd23b6c4d1e7effc81c962b3889f83e43c0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/yiisoft/yii2-composer/zipball/7f300dd23b6c4d1e7effc81c962b3889f83e43c0", + "reference": "7f300dd23b6c4d1e7effc81c962b3889f83e43c0", + "shasum": "" + }, + "require": { + "composer-plugin-api": "1.0.0" + }, + "type": "composer-plugin", + "extra": { + "class": "yii\\composer\\Plugin", + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "yii\\composer\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Qiang Xue", + "email": "qiang.xue@gmail.com" + } + ], + "description": "The composer plugin for Yii extension installer", + "keywords": [ + "composer", + "extension installer", + "yii2" + ], + "time": "2014-12-07 16:42:41" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +}