Tree gridview

Общие вопросы по использованию второй версии фреймворка. Если не знаете как что-то сделать и это про Yii 2, вам сюда.
Ответить
drag0n
Сообщения: 208
Зарегистрирован: 2017.04.28, 08:37

Tree gridview

Сообщение drag0n »

Добрый день.

в Yii2 кто-нибудь знает есть ли что-то на подобие
http://jqgrid-php.net/examples/?render=jqOutTree
с поддержкой Ajax
Аватара пользователя
futbolim
Сообщения: 2051
Зарегистрирован: 2012.07.08, 19:28

Re: Tree gridview

Сообщение futbolim »

jstree делал для категорий. Если интересно, могу поделиться
drag0n
Сообщения: 208
Зарегистрирован: 2017.04.28, 08:37

Re: Tree gridview

Сообщение drag0n »

Спасибо, если можно то хотел бы взглянуть
Аватара пользователя
futbolim
Сообщения: 2051
Зарегистрирован: 2012.07.08, 19:28

Re: Tree gridview

Сообщение futbolim »

View

Код: Выделить всё

<?php
use yii\helpers\Html;

/* @var $this yii\web\View */
/* @var $config [] */

$this->title = 'Дерево категорий';
$this->params['breadcrumbs'][] = 'Категории';

\backend\assets\JstreeAsset::register($this);
?>
<div class="category-index">

    <h1><?= Html::encode($this->title) ?></h1>

    <div id="explorer"></div>
</div>

<?php
$this->registerJs("
    $('#explorer').jstree({
        'core': {
            'data' : {
                'url' : '" . url(['/category/explorer-request']) . "',
                'data' : function (node) {
                    return {'id': node.id};
                }
            },
            'check_callback' : true
        },
        'types': {
            'default' : {
                'icon' : 'glyphicon glyphicon-flash'
            }
        },
        'contextmenu':{         
            'items': function(node) {
                var tree = $('#explorer').jstree(true);
                return {
                    'Create': {
                        'label': 'Добавить дочернюю',
                        'action': function (obj) { 
                            node = tree.create_node(node);
                        }
                    },
                    'Rename': {
                        label: 'Переименовать',
                        icon: 'glyphicon glyphicon-pencil',
                        action: function (obj) { 
                            tree.edit(node);
                        }
                    },                         
                    'Remove': {
                        label: 'Удалить',
                        icon: 'glyphicon glyphicon-remove',
                        action: function (obj) { 
                            tree.delete_node(node);
                        }
                    }
                };
            }
        },
        'state' : { 'key' : 'state_demo' },
        'plugins' : [
            'contextmenu', 
            'dnd', 
            'state',
//            'types' // При этом жутко тормозит создание ноды
        ]
    }).on('create_node.jstree', function(event, data) {
        $.post(
            '" . url(['/category/create-node']) . "',
            {parent: data.node.parent},
            function(response){
                if(response.status == 'success') {
                    $('#explorer').jstree(true).set_id(data.node, response.id);
                    $('#explorer').jstree(true).edit(data.node);
                }
            }
        );
    }).on('rename_node.jstree', function(event, data) {
        $.post(
            '" . url(['/category/rename-node']) . "',
            {id: data.node.id, name: data.node.text},
            function(data){}
        );
    }).on('delete_node.jstree', function(event, data) {
        $.post(
            '" . url(['/category/delete-node']) . "',
            {id: data.node.id},
            function(data){}
        );
    }).on('move_node.jstree', function(event, data) {
        $.post(
            '" . url(['/category/move-node']) . "',
            {
                id: data.node.id,
                prev_id: $('#' + data.node.id).prev().attr('id'),
                parent_id: data.node.parent
            },
            function(data){}
        );
    });
");
Controller

Код: Выделить всё

<?php
namespace backend\controllers;

use Yii;
use common\models\Category;
use backend\models\search\CategorySearch;
use yii\filters\ContentNegotiator;
use yii\web\NotFoundHttpException;
use yii\filters\VerbFilter;
use yii\web\Response;

/**
 * Class CategoryController
 * @package backend\controllers
 */
class CategoryController extends BackendController
{
    /**
     * @inheritdoc
     */
    public function behaviors()
    {
        $behaviors = parent::behaviors();

        $behaviors['verbs'] = [
            'class' => VerbFilter::className(),
            'actions' => [
                'delete' => ['POST'],
            ],
        ];
        $behaviors['negotiator'] = [
            'class' => ContentNegotiator::className(),
            'only' => ['explorer-request', 'create-node', 'rename-node', 'delete-node', 'move-node'],
            'formats' => [
                'application/json' => Response::FORMAT_JSON,
            ],
        ];

        return $behaviors;
    }

    /**
     * @return string
     */
    public function actionExplorer()
    {
        return $this->render('explorer');
    }

    /**
     * Ajax запрос на смену состояние проводника
     * @link https://github.com/vakata/jstree
     * @param $id
     * @return array|string
     */
    public function actionExplorerRequest($id = '#')
    {
        $data = [];

        if($id == '#') {
            $root = Category::getRoot();
            $children = $root->children(1)->asArray()->all();

            foreach($children as $child) {
                $data[] = [
                    'id' => $child['id'],
                    'text' => $child['name'],
                    'children' => true,
                ];
            }
        }
        else {
            $root = Category::findOne($id);
            $data['id'] = $id;
            $data['text'] = $root['name'];
            $data['children'] = [];
            $children = $root->children(1)->asArray()->all();
            foreach($children as $child) {
                $data['children'][] = [
                    'id' => $child['id'],
                    'text' => $child['name'],
                    'children' => true,
                ];
            }
        }

        return $data;
    }

    /**
     * Ajax запрос на создание категории из проводника
     * @return array
     */
    public function actionCreateNode() {
        $parent = Yii::$app->request->post('parent');
        $root = Category::findOne($parent);
        if($root) {
            $category = new Category(['name' => 'New node']);
            if($category->appendTo($root)) {
                return [
                    'status' => 'success',
                    'id' => $category->id
                ];
            }
        }

        return [
            'status' => 'error'
        ];
    }

    /**
     * Ajax запрос на переименование категории из проводника
     * @return array
     */
    public function actionRenameNode() {
        $id = Yii::$app->request->post('id');
        $name = Yii::$app->request->post('name');
        $category = Category::findOne($id);
        $category->name = $name;
        if($category->save()) {
            return ['response' => 'success'];
        }
        return ['response' => 'error'];
    }

    /**
     * Ajax запрос на удаление категории из проводника
     * @return array
     */
    public function actionDeleteNode() {
        $id = Yii::$app->request->post('id');
        $category = Category::findOne($id);
        if($category && $category->deleteWithChildren()) {
            return ['response' => 'success'];
        }
        return ['response' => 'error'];
    }

    /**
     * Ajax запрос на перемещение категории из проводника
     * @return array
     */
    public function actionMoveNode() {
        $id = Yii::$app->request->post('id');
        $parent_id = Yii::$app->request->post('parent_id');
        $prev_id = Yii::$app->request->post('prev_id');
        $category = Category::findOne($id);

        if($prev_id === null) { // Значит мы засовываем вначало списка у $parent_id
            $root = Category::findOne($parent_id);
            $category->prependTo($root);
        }
        else { // Значит мы вставляем за $prev_id
            $sibling = Category::findOne($prev_id);
            $category->insertAfter($sibling);
        }

        return ['response' => 'success'];
    }

    /**
     * Lists all Category models.
     * @return mixed
     */
    public function actionIndex()
    {
        $searchModel = new CategorySearch();
        $dataProvider = $searchModel->search(Yii::$app->request->queryParams);

        return $this->render('index', [
            'searchModel' => $searchModel,
            'dataProvider' => $dataProvider,
        ]);
    }

    /**
     * Displays a single Category model.
     * @param integer $id
     * @return mixed
     */
    public function actionView($id)
    {
        return $this->render('view', [
            'model' => $this->findModel($id),
        ]);
    }

    /**
     * @return string|\yii\web\Response
     * @throws NotFoundHttpException
     */
    public function actionCreate()
    {
        $model = new Category();
        $root = Category::getRoot();

        if ($model->load(Yii::$app->request->post())) {
            if($model->parent_id) {
                $root = Category::findOne($model->parent_id);
            }
            $model->appendTo($root);
            Yii::$app->session->setFlash('success', 'Категория добавлена');
            return $this->redirect(['index']);
        }

        return $this->render('create', [
            'model' => $model,
        ]);
    }

    /**
     * Updates an existing Category model.
     * If update is successful, the browser will be redirected to the 'view' page.
     * @param integer $id
     * @return mixed
     */
    public function actionUpdate($id)
    {
        $model = $this->findModel($id);
        $root = Category::getRoot();

        if ($model->load(Yii::$app->request->post())) {
            if($model->parent_id) {
                $root = Category::findOne($model->parent_id);
            }
            $model->appendTo($root);
            Yii::$app->session->setFlash('success', 'Категория сохранена');
            return $this->redirect(['index']);
        } else {
            return $this->render('update', [
                'model' => $model,
            ]);
        }
    }

    /**
     * Deletes an existing Category model.
     * If deletion is successful, the browser will be redirected to the 'index' page.
     * @param integer $id
     * @return mixed
     */
    public function actionDelete($id)
    {
        $model = $this->findModel($id);
        if($model->parent_id == null) {
            $model->deleteWithChildren();
        }
        else {
            $model->delete();
        }

        return $this->redirect(Yii::$app->request->referrer);
    }

    /**
     * Finds the Category model based on its primary key value.
     * If the model is not found, a 404 HTTP exception will be thrown.
     * @param integer $id
     * @return Category the loaded model
     * @throws NotFoundHttpException if the model cannot be found
     */
    protected function findModel($id)
    {
        if (($model = Category::findOne($id)) !== null) {
            return $model;
        } else {
            throw new NotFoundHttpException('The requested page does not exist.');
        }
    }
}
drag0n
Сообщения: 208
Зарегистрирован: 2017.04.28, 08:37

Re: Tree gridview

Сообщение drag0n »

:shock:
Спасибо
yura1976
Сообщения: 134
Зарегистрирован: 2012.08.06, 13:24

Re: Tree gridview

Сообщение yura1976 »

futbolim,
был бы очень благодарен, если бы Вы еще модель показали
Аватара пользователя
futbolim
Сообщения: 2051
Зарегистрирован: 2012.07.08, 19:28

Re: Tree gridview

Сообщение futbolim »

yura1976 писал(а): 2019.03.25, 17:20 futbolim,
был бы очень благодарен, если бы Вы еще модель показали

Код: Выделить всё

<?php
namespace common\models;

use common\models\queries\CategoryQuery;
use common\traits\TranslationsMethodTrait;
use creocoder\nestedsets\NestedSetsBehavior;
use yii\behaviors\SluggableBehavior;
use yii\db\ActiveRecord;

/**
 * @property integer $id
 * @property integer $tree
 * @property integer $lft
 * @property integer $rgt
 * @property integer $depth
 * @property string $name
 * @property string $slug
 *
 * @property NewsArticle[] $newsArticles
 * @property Translation[] $translations
 */
class Category extends ActiveRecord
{
    use TranslationsMethodTrait;

    // helper fields
    public $parent_id;

    /**
     * @return CategoryQuery
     */
    public static function find()
    {
        return new CategoryQuery(get_called_class());
    }

    /**
     * @inheritdoc
     */
    public static function tableName()
    {
        return '{{%category}}';
    }

    /**
     * @return array
     */
    public function behaviors()
    {
        return [
            'tree' => ['class' => NestedSetsBehavior::className(), 'treeAttribute' => 'tree'],
            'slug' => ['class' => SluggableBehavior::className(), 'attribute' => 'name'],
        ];
    }

    /**
     * @return array
     */
    public function transactions()
    {
        return [
            self::SCENARIO_DEFAULT => self::OP_ALL,
        ];
    }

    /**
     * @inheritdoc
     */
    public function rules()
    {
        return [
            ['slug', 'trim'],
//            ['slug', 'required'],
            ['slug', 'string', 'max' => 255],

            ['name', 'trim'],
            ['name', 'required'],
            ['name', 'string', 'max' => 255],

            ['parent_id', 'integer'],

            ['translations_field', 'safe'],
        ];
    }

    /**
     * @inheritdoc
     */
    public function attributeLabels()
    {
        return [
            'id' => 'ID',
            'name' => t('Name'),
            'slug' => t('Seo slug'),
            'parent_id' => t('Parent category'),
            'translations_field' => t('Translations'),
        ];
    }

    /**
     * @param bool $dropDown
     * @param null $without_id
     * @param string $prefix
     * @param string $name
     * @return array
     */
    public static function getTree($dropDown = false, $without_id = null, $prefix = '—', $name = 'Product categories')
    {
        $root = self::findOne(['name' => $name]);
        $children = $root->children()->asArray()->all();

        if($dropDown) {
            $result = [];
            foreach ($children as $child) {
                if($without_id && $without_id == $child['id']) {
                    continue;
                }
                $result[$child['id']] = str_repeat($prefix, $child['depth'] - 1) . ' ' . $child['name'];
            }
            return $result;
        }

        return $children;
    }

    /**
     * @return array
     */
    public static function getTreeWithGroups()
    {
        $root = self::findOne(['name' => 'Product categories']);
        $children = $root->children(2)->asArray()->all();

        $result = [];
        $parent_name = '';
        foreach ($children as $child) {
            if($child['depth'] == 1) {
                $parent_name = $child['name'];
                $result[$parent_name] = [];
            }
            if($child['depth'] == 2) {
                $result[$parent_name][$child['id']] = $child['name'];
            }
        }

//        d($result);

        return $result;
    }

    /**
     * @param null $query
     * @param string $name
     * @return array
     */
    public static function getNestedTree($query = null, $name = 'Product categories')
    {
        $root = self::findOne(['name' => $name]);
        $categories = $root->children()->asArray()->all();
        $result = [];
        $first_level_i = $second_level_i = 0;

        foreach ($categories as $i => $category) {
            if($category['depth'] == 1) {
                $result[$i] = $category;
                $first_level_i = $i;
                $result[$i]['need'] = ($query && mb_stripos($category['name'], $query) === false) ? false : true;
            }
            if($category['depth'] == 2) {
                $second_level_i = $i;
                $result[$first_level_i]['children'][$second_level_i] = $category;
                if($query && mb_stripos($category['name'], $query) === false) {
                    $result[$first_level_i]['children'][$second_level_i]['need'] = false;
                }
                else {
                    $result[$first_level_i]['need'] = true;
                    $result[$first_level_i]['children'][$second_level_i]['need'] = true;
                }
            }
            if($category['depth'] == 3) {
                $result[$first_level_i]['children'][$second_level_i]['children'][$i] = $category;
                if($query && mb_stripos($category['name'], $query) === false) {
                    $result[$first_level_i]['children'][$second_level_i]['children'][$i]['need'] = false;
                }
                else {
                    $result[$first_level_i]['need'] = true;
                    $result[$first_level_i]['children'][$second_level_i]['need'] = true;
                    $result[$first_level_i]['children'][$second_level_i]['children'][$i]['need'] = true;
                }
            }
        }

        if($query !== null) {

            // 1 level
            foreach($result as $i => $cat1) {
                if(!$cat1['need']) {
                    unset($result[$i]);
                }
            }

            // 2 level
            foreach($result as $i => $cat1) {
                if(isset($cat1['children']) && count($cat1['children'])) {
                    foreach ($cat1['children'] as $j => $cat2) {
                        if(!$cat2['need']) {
                            unset($result[$i]['children'][$j]);
                        }
                    }
                }
            }

            // 3 level
            foreach($result as $i => $cat1) {
                if(isset($cat1['children']) && count($cat1['children'])) {
                    foreach ($cat1['children'] as $j => $cat2) {
                        if(isset($cat2['children']) && count($cat2['children'])) {
                            foreach ($cat2['children'] as $k => $cat3) {
                                if(!$cat3['need']) {
                                    unset($result[$i]['children'][$j]['children'][$k]);
                                }
                            }
                        }
                    }
                }
            }
        }

        return $result;
    }

    /**
     * @return \yii\db\ActiveQuery
     */
    public function getNewsArticles()
    {
        return $this->hasMany(NewsArticle::className(), ['category_id' => 'id']);
    }

    /**
     * @param bool $insert
     * @param array $changedAttributes
     */
    public function afterSave($insert, $changedAttributes)
    {
        parent::afterSave($insert, $changedAttributes);

        if($this->translations_field && count($this->translations_field)) {
            Translation::deleteAll([
                'class_name' => self::className(),
                'entity_id' => $this->id,
            ]);

            foreach ($this->translations_field as $language_code => $translation_field) {
                (new Translation([
                    'class_name' => self::className(),
                    'entity_id' => $this->id,
                    'language_code' => $language_code,
                    'title' => $translation_field['title'],
                ]))->save();
            }
        }
    }

    /**
     * @param $name
     * @return static[]
     */
    public static function findList($name)
    {
        return static::find()
            // @todo lower index
            ->where(['like', 'lower(name)', mb_strtolower($name)])
            ->limit(10)
            ->asArray()
            ->all();
    }
}
Дело было миилион лет назад. Код можно улучшить. Просто как пример.
Аватара пользователя
futbolim
Сообщения: 2051
Зарегистрирован: 2012.07.08, 19:28

Re: Tree gridview

Сообщение futbolim »

А вот так можно с бесконечным уровнем вложенности делать дерево:

Код: Выделить всё

<?php
namespace common\helpers;

use Yii;

/**
 * Class ReviewHelper
 * @package common\helpers
 */
class ReviewHelper
{
    /**
     * @param $models
     * @return mixed
     */
    public static function buildNestedTree($models)
    {
        $max_depth = 1;
        foreach($models as $model) {
            if($model['depth'] > $max_depth) {
                $max_depth = $model['depth'];
            }
        }

        if($max_depth == 1) {
            return $models;
        }

        for ($depth = $max_depth; $depth >= 2; $depth--) { // глубина от самой большой к малой
            $models = array_values($models);
            for ($i = count($models) - 1; $i >= 0; $i--) { // все подряд от конца к началу
                if(isset($models[$i]) && $models[$i]['depth'] == $depth) {
                    for($j = $i - 1; $j >= 0; $j--) { // только предыдущие (ищем родителя)
                        if(isset($models[$j]['depth']) && isset($models[$i]['depth']) && $models[$j]['depth'] < $models[$i]['depth']) {
                            if(!isset($models[$j]['children'])) {
                                $models[$j]['children'] = [];
                            }
                            array_unshift($models[$j]['children'], $models[$i]);
                            unset($models[$i]);
                            break;
                        }
                    }
                }
            }
        }

        return $models;
    }
}
Ответить