<?php namespace Dcat\Admin; use Closure; use Dcat\Admin\Contracts\TreeRepository; use Dcat\Admin\Exception\InvalidArgumentException; use Dcat\Admin\Repositories\EloquentRepository; use Dcat\Admin\Support\Helper; use Dcat\Admin\Traits\HasBuilderEvents; use Dcat\Admin\Traits\HasVariables; use Dcat\Admin\Tree\AbstractTool; use Dcat\Admin\Tree\Actions; use Dcat\Admin\Tree\Tools; use Illuminate\Contracts\Support\Htmlable; use Illuminate\Contracts\Support\Renderable; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Str; use Illuminate\Support\Traits\Macroable; /** * Class Tree. * * @see https://github.com/dbushell/Nestable */ class Tree implements Renderable { use HasBuilderEvents; use HasVariables; use Macroable; const SAVE_ORDER_NAME = '_order'; /** * @var array */ protected $items = []; /** * @var string */ protected $elementId = 'tree-'; /** * @var TreeRepository */ protected $repository; /** * @var \Closure */ protected $queryCallback; /** * View of tree to render. * * @var string */ protected $view = 'admin::tree.container'; /** * @var string */ protected $branchView = 'admin::tree.branch'; /** * @var \Closure */ protected $callback; /** * @var null */ protected $branchCallback = null; /** * @var string */ public $path; /** * @var string */ public $url; /** * @var bool */ public $useCreate = true; /** * @var bool */ public $expand = true; /** * @var bool */ public $useQuickCreate = true; /** * @var array */ public $dialogFormDimensions = ['700px', '670px']; /** * @var bool */ public $useSave = true; /** * @var bool */ public $useRefresh = true; /** * @var array */ protected $nestableOptions = []; /** * Header tools. * * @var Tools */ public $tools; /** * @var string */ protected $actionsClass; /** * @var \Closure[] */ protected $actionCallbacks = []; /** * @var Closure */ protected $wrapper; /** * Menu constructor. * * @param Model|TreeRepository|string|null $model */ public function __construct($repository = null, ?\Closure $callback = null) { $this->repository = $this->makeRepository($repository); $this->path = $this->path ?: request()->getPathInfo(); $this->url = url($this->path); $this->elementId .= Str::random(8); $this->setUpTools(); if ($callback instanceof \Closure) { call_user_func($callback, $this); } $this->callResolving(); } /** * Setup tree tools. */ public function setUpTools() { $this->tools = new Tools($this); } /** * @param $repository * @return TreeRepository */ public function makeRepository($repository) { if (is_string($repository)) { $repository = new $repository(); } if ($repository instanceof Model || $repository instanceof Builder) { $repository = EloquentRepository::make($repository); } if (! $repository instanceof TreeRepository) { $class = get_class($repository); throw new InvalidArgumentException("The class [{$class}] must be a type of [".TreeRepository::class.'].'); } return $repository; } /** * Initialize branch callback. * * @return void */ protected function setDefaultBranchCallback() { if (is_null($this->branchCallback)) { $this->branchCallback = function ($branch) { $key = $branch[$this->repository->getPrimaryKeyColumn()]; $title = $branch[$this->repository->getTitleColumn()]; return "$key - $title"; }; } } /** * Set branch callback. * * @param \Closure $branchCallback * @return $this */ public function branch(\Closure $branchCallback) { $this->branchCallback = $branchCallback; return $this; } /** * Set query callback this tree. * * @return $this */ public function query(\Closure $callback) { $this->queryCallback = $callback; return $this; } /** * number of levels an item can be nested (default 5). * * @see https://github.com/dbushell/Nestable * * @param int $max * @return $this */ public function maxDepth(int $max) { return $this->nestable(['maxDepth' => $max]); } /** * Set nestable options. * * @param array $options * @return $this */ public function nestable($options = []) { $this->nestableOptions = array_merge($this->nestableOptions, $options); return $this; } /** * @param bool $value * @return void */ public function expand(bool $value = true) { $this->expand = $value; } /** * Disable create. * * @param bool $value * @return void */ public function disableCreateButton(bool $value = true) { $this->useCreate = ! $value; } public function showCreateButton(bool $value = true) { return $this->disableCreateButton(! $value); } public function disableQuickCreateButton(bool $value = true) { $this->useQuickCreate = ! $value; } public function showQuickCreateButton(bool $value = true) { return $this->disableQuickCreateButton(! $value); } /** * @param string $width * @param string $height * @return $this */ public function setDialogFormDimensions(string $width, string $height) { $this->dialogFormDimensions = [$width, $height]; return $this; } /** * Disable save. * * @param bool $value * @return void */ public function disableSaveButton(bool $value = true) { $this->useSave = ! $value; } public function showSaveButton(bool $value = true) { return $this->disableSaveButton(! $value); } /** * Disable refresh. * * @param bool $value * @return void */ public function disableRefreshButton(bool $value = true) { $this->useRefresh = ! $value; } public function showRefreshButton(bool $value = true) { return $this->disableRefreshButton(! $value); } public function disableQuickEditButton(bool $value = true) { $this->actions(function (Actions $actions) use ($value) { $actions->disableQuickEdit($value); }); } public function showQuickEditButton(bool $value = true) { return $this->disableQuickEditButton(! $value); } public function disableEditButton(bool $value = true) { $this->actions(function (Actions $actions) use ($value) { $actions->disableEdit($value); }); } public function showEditButton(bool $value = true) { return $this->disableEditButton(! $value); } public function disableDeleteButton(bool $value = true) { $this->actions(function (Actions $actions) use ($value) { $actions->disableDelete($value); }); } public function showDeleteButton(bool $value = true) { return $this->disableDeleteButton(! $value); } /** * @param Closure $closure * @return $this; */ public function wrap(\Closure $closure) { $this->wrapper = $closure; return $this; } /** * @return bool */ public function hasWrapper() { return $this->wrapper ? true : false; } /** * Save tree order from a input. * * @param string $serialize * @return bool */ public function saveOrder($serialize) { $tree = json_decode($serialize, true); if (json_last_error() != JSON_ERROR_NONE) { throw new InvalidArgumentException(json_last_error_msg()); } $this->repository->saveOrder($tree); return true; } /** * Set view of tree. * * @param string $view * @return $this */ public function view($view) { $this->view = $view; return $this; } /** * @param string $view * @return $this */ public function branchView($view) { $this->branchView = $view; return $this; } /** * @return \Closure */ public function resolveAction() { return function ($branch) { $class = $this->actionsClass ?: Actions::class; $action = new $class(); $action->setParent($this); $action->setRow($branch); $this->callActionCallbacks($action); return $action->render(); }; } protected function callActionCallbacks(Actions $actions) { foreach ($this->actionCallbacks as $callback) { $callback->call($actions->row, $actions); } } /** * 自定义行操作类. * * @param string $actionClass * @return $this */ public function setActionClass(string $actionClass) { $this->actionsClass = $actionClass; return $this; } /** * 设置行操作回调. * * @param \Closure|array $callback * @return $this */ public function actions($callback) { if ($callback instanceof \Closure) { $this->actionCallbacks[] = $callback; } else { $this->actionCallbacks[] = function (Actions $actions) use ($callback) { if (! is_array($callback)) { $callback = [$callback]; } foreach ($callback as $value) { $actions->append(clone $value); } }; } return $this; } /** * Return all items of the tree. * * @param array $items */ public function getItems() { return $this->repository->withQuery($this->queryCallback)->toTree(); } /** * Variables in tree template. * * @return array */ public function defaultVariables() { return [ 'id' => $this->elementId, 'tools' => $this->tools->render(), 'items' => $this->getItems(), 'useCreate' => $this->useCreate, 'useQuickCreate' => $this->useQuickCreate, 'useSave' => $this->useSave, 'useRefresh' => $this->useRefresh, 'createButton' => $this->renderCreateButton(), 'nestableOptions' => $this->nestableOptions, 'url' => $this->url, 'resolveAction' => $this->resolveAction(), 'expand' => $this->expand, ]; } /** * @return mixed */ public function getKeyName() { return $this->repository->getKeyName(); } /** * @return string */ public function resource() { return $this->url; } /** * Set resource path. * * @param string $path * @return $this */ public function setResource($path) { $this->url = admin_url($path); return $this; } /** * Setup tools. * * @param Closure|array|AbstractTool|Renderable|Htmlable|string $callback * @return $this|Tools */ public function tools($callback = null) { if ($callback === null) { return $this->tools; } if ($callback instanceof \Closure) { call_user_func($callback, $this->tools); return $this; } if (! is_array($callback)) { $callback = [$callback]; } foreach ($callback as $tool) { $this->tools->add($tool); } return $this; } /** * @return string */ protected function renderCreateButton() { if (! $this->useQuickCreate && ! $this->useCreate) { return ''; } $url = $this->url.'/create'; $new = trans('admin.new'); $quickBtn = $btn = ''; if ($this->useCreate) { $btn = "<a href='{$url}' class='btn btn-sm btn-primary'><i class='feather icon-plus'></i><span class='d-none d-sm-inline'> {$new}</span></a>"; } if ($this->useQuickCreate) { $text = $this->useCreate ? '<i class=\' fa fa-clone\'></i>' : "<i class='feather icon-plus'></i><span class='d-none d-sm-inline'> $new</span>"; $quickBtn = "<button data-url='$url' class='btn btn-sm btn-primary tree-quick-create'>$text</button>"; } return " <div class='btn-group pull-right' style='margin-right:3px'>{$btn}{$quickBtn}</div>"; } /** * @return void */ protected function renderQuickCreateButton() { if ($this->useQuickCreate) { [$width, $height] = $this->dialogFormDimensions; Form::dialog(trans('admin.new')) ->click('.tree-quick-create') ->success('Dcat.reload()') ->dimensions($width, $height); } } /** * Render a tree. * * @return \Illuminate\Http\JsonResponse|string */ public function render() { $this->callResolving(); $this->setDefaultBranchCallback(); $this->renderQuickCreateButton(); view()->share([ 'currentUrl' => $this->url, 'keyName' => $this->getKeyName(), 'branchView' => $this->branchView, 'branchCallback' => $this->branchCallback, ]); return $this->doWrap(); } /** * @return string */ protected function doWrap() { $view = view($this->view, $this->variables()); if (! $wrapper = $this->wrapper) { $html = Admin::resolveHtml($view->render())['html']; return "<div class='card'>{$html}</div>"; } return Admin::resolveHtml(Helper::render($wrapper($view)))['html']; } /** * Get the string contents of the grid view. * * @return string */ public function __toString() { return $this->render(); } /** * Create a tree instance. * * @param mixed ...$param * @return $this */ public static function make(...$param) { return new static(...$param); } }