Grid.php 20 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007
  1. <?php
  2. namespace Dcat\Admin;
  3. use Closure;
  4. use Dcat\Admin\Contracts\Repository;
  5. use Dcat\Admin\Grid\Column;
  6. use Dcat\Admin\Grid\Concerns;
  7. use Dcat\Admin\Grid\Model;
  8. use Dcat\Admin\Grid\Responsive;
  9. use Dcat\Admin\Grid\Row;
  10. use Dcat\Admin\Grid\Tools;
  11. use Dcat\Admin\Support\Helper;
  12. use Dcat\Admin\Traits\HasBuilderEvents;
  13. use Illuminate\Contracts\Support\Renderable;
  14. use Illuminate\Database\Eloquent\Builder;
  15. use Illuminate\Support\Collection;
  16. use Illuminate\Support\Traits\Macroable;
  17. class Grid
  18. {
  19. use HasBuilderEvents;
  20. use Concerns\HasNames;
  21. use Concerns\HasFilter;
  22. use Concerns\HasTools;
  23. use Concerns\HasActions;
  24. use Concerns\HasPaginator;
  25. use Concerns\HasExporter;
  26. use Concerns\HasComplexHeaders;
  27. use Concerns\HasSelector;
  28. use Concerns\HasQuickCreate;
  29. use Concerns\HasQuickSearch;
  30. use Concerns\CanFixColumns;
  31. use Concerns\CanHidesColumns;
  32. use Macroable {
  33. __call as macroCall;
  34. }
  35. const CREATE_MODE_DEFAULT = 'default';
  36. const CREATE_MODE_DIALOG = 'dialog';
  37. const IFRAME_QUERY_NAME = '_grid_iframe_';
  38. /**
  39. * The grid data model instance.
  40. *
  41. * @var \Dcat\Admin\Grid\Model
  42. */
  43. protected $model;
  44. /**
  45. * Collection of grid columns.
  46. *
  47. * @var \Illuminate\Support\Collection
  48. */
  49. protected $columns;
  50. /**
  51. * Collection of all grid columns.
  52. *
  53. * @var \Illuminate\Support\Collection
  54. */
  55. protected $allColumns;
  56. /**
  57. * Collection of all data rows.
  58. *
  59. * @var \Illuminate\Support\Collection
  60. */
  61. protected $rows;
  62. /**
  63. * Rows callable fucntion.
  64. *
  65. * @var \Closure[]
  66. */
  67. protected $rowsCallback = [];
  68. /**
  69. * All column names of the grid.
  70. *
  71. * @var array
  72. */
  73. protected $columnNames = [];
  74. /**
  75. * Grid builder.
  76. *
  77. * @var \Closure
  78. */
  79. protected $builder;
  80. /**
  81. * Mark if the grid is built.
  82. *
  83. * @var bool
  84. */
  85. protected $built = false;
  86. /**
  87. * All variables in grid view.
  88. *
  89. * @var array
  90. */
  91. protected $variables = [];
  92. /**
  93. * Resource path of the grid.
  94. *
  95. * @var
  96. */
  97. protected $resourcePath;
  98. /**
  99. * Default primary key name.
  100. *
  101. * @var string
  102. */
  103. protected $keyName = 'id';
  104. /**
  105. * View for grid to render.
  106. *
  107. * @var string
  108. */
  109. protected $view = 'admin::grid.table';
  110. /**
  111. * @var Closure
  112. */
  113. protected $header;
  114. /**
  115. * @var Closure
  116. */
  117. protected $footer;
  118. /**
  119. * @var Closure
  120. */
  121. protected $wrapper;
  122. /**
  123. * @var Responsive
  124. */
  125. protected $responsive;
  126. /**
  127. * @var bool
  128. */
  129. protected $addNumberColumn = false;
  130. /**
  131. * @var string
  132. */
  133. protected $tableId = 'grid-table';
  134. /**
  135. * @var Grid\Tools\RowSelector
  136. */
  137. protected $rowSelector;
  138. /**
  139. * Options for grid.
  140. *
  141. * @var array
  142. */
  143. protected $options = [
  144. 'show_pagination' => true,
  145. 'show_filter' => true,
  146. 'show_actions' => true,
  147. 'show_quick_edit_button' => false,
  148. 'show_edit_button' => true,
  149. 'show_view_button' => true,
  150. 'show_delete_button' => true,
  151. 'show_row_selector' => true,
  152. 'show_create_button' => true,
  153. 'show_bordered' => false,
  154. 'table_collapse' => true,
  155. 'show_toolbar' => true,
  156. 'create_mode' => self::CREATE_MODE_DEFAULT,
  157. 'dialog_form_area' => ['700px', '670px'],
  158. 'table_class' => ['table', 'custom-data-table', 'data-table'],
  159. ];
  160. /**
  161. * @var \Illuminate\Http\Request
  162. */
  163. protected $request;
  164. /**
  165. * Create a new grid instance.
  166. *
  167. * Grid constructor.
  168. *
  169. * @param Repository|\Illuminate\Database\Eloquent\Model|Builder|null $repository
  170. * @param null|\Closure $builder
  171. */
  172. public function __construct($repository = null, ?\Closure $builder = null, $request = null)
  173. {
  174. $this->model = new Model(request(), $repository);
  175. $this->columns = new Collection();
  176. $this->allColumns = new Collection();
  177. $this->rows = new Collection();
  178. $this->builder = $builder;
  179. $this->request = $request ?: request();
  180. if ($repository = $this->model->repository()) {
  181. $this->setKeyName($repository->getKeyName());
  182. }
  183. $this->model->setGrid($this);
  184. $this->setupTools();
  185. $this->setupFilter();
  186. $this->callResolving();
  187. }
  188. /**
  189. * Get table ID.
  190. *
  191. * @return string
  192. */
  193. public function getTableId()
  194. {
  195. return $this->tableId;
  196. }
  197. /**
  198. * Set primary key name.
  199. *
  200. * @param string $name
  201. *
  202. * @return $this
  203. */
  204. public function setKeyName(string $name)
  205. {
  206. $this->keyName = $name;
  207. return $this;
  208. }
  209. /**
  210. * Get or set primary key name.
  211. *
  212. * @return string|void
  213. */
  214. public function getKeyName()
  215. {
  216. return $this->keyName ?: 'id';
  217. }
  218. /**
  219. * Add column to Grid.
  220. *
  221. * @param string $name
  222. * @param string $label
  223. *
  224. * @return Column
  225. */
  226. public function column($name, $label = '')
  227. {
  228. return $this->addColumn($name, $label);
  229. }
  230. /**
  231. * Add number column.
  232. *
  233. * @param null|string $label
  234. *
  235. * @return Column
  236. */
  237. public function number(?string $label = null)
  238. {
  239. return $this->addColumn('#', $label ?: '#');
  240. }
  241. /**
  242. * Batch add column to grid.
  243. *
  244. * @example
  245. * 1.$grid->columns(['name' => 'Name', 'email' => 'Email' ...]);
  246. * 2.$grid->columns('name', 'email' ...)
  247. *
  248. * @param array $columns
  249. *
  250. * @return Collection|Column[]|void
  251. */
  252. public function columns($columns = null)
  253. {
  254. if ($columns === null) {
  255. return $this->columns;
  256. }
  257. if (func_num_args() == 1 && is_array($columns)) {
  258. foreach ($columns as $column => $label) {
  259. $this->column($column, $label);
  260. }
  261. return;
  262. }
  263. foreach (func_get_args() as $column) {
  264. $this->column($column);
  265. }
  266. }
  267. /**
  268. * @return Collection|Column[]
  269. */
  270. public function allColumns()
  271. {
  272. return $this->allColumns;
  273. }
  274. /**
  275. * Add column to grid.
  276. *
  277. * @param string $field
  278. * @param string $label
  279. *
  280. * @return Column
  281. */
  282. protected function addColumn($field = '', $label = '')
  283. {
  284. $column = $this->newColumn($field, $label);
  285. $this->columns->put($field, $column);
  286. $this->allColumns->put($field, $column);
  287. return $column;
  288. }
  289. /**
  290. * @param string $field
  291. * @param string $label
  292. *
  293. * @return Column
  294. */
  295. public function prependColumn($field = '', $label = '')
  296. {
  297. $column = $this->newColumn($field, $label);
  298. $this->columns->prepend($column, $field);
  299. $this->allColumns->prepend($column, $field);
  300. return $column;
  301. }
  302. /**
  303. * @param string $field
  304. * @param string $label
  305. *
  306. * @return Column
  307. */
  308. public function newColumn($field = '', $label = '')
  309. {
  310. $column = new Column($field, $label);
  311. $column->setGrid($this);
  312. return $column;
  313. }
  314. /**
  315. * Get Grid model.
  316. *
  317. * @return Model
  318. */
  319. public function model()
  320. {
  321. return $this->model;
  322. }
  323. /**
  324. * @return array
  325. */
  326. public function getColumnNames()
  327. {
  328. return $this->columnNames;
  329. }
  330. /**
  331. * Apply column filter to grid query.
  332. */
  333. protected function applyColumnFilter()
  334. {
  335. $this->columns->each->bindFilterQuery($this->model());
  336. }
  337. /**
  338. * @param string|array $class
  339. *
  340. * @return $this
  341. */
  342. public function addTableClass($class)
  343. {
  344. $this->options['table_class'] = array_merge((array) $this->options['table_class'], (array) $class);
  345. return $this;
  346. }
  347. public function formatTableClass()
  348. {
  349. if ($this->options['show_bordered']) {
  350. $this->addTableClass(['table-bordered', 'complex-headers', 'data-table']);
  351. }
  352. return implode(' ', array_unique((array) $this->options['table_class']));
  353. }
  354. /**
  355. * Build the grid.
  356. *
  357. * @return void
  358. */
  359. public function build()
  360. {
  361. if ($this->built) {
  362. return;
  363. }
  364. $collection = $this->processFilter(false);
  365. $data = $collection->toArray();
  366. $this->prependRowSelectorColumn();
  367. $this->appendActionsColumn();
  368. Column::setOriginalGridModels($collection);
  369. $this->columns->map(function (Column $column) use (&$data) {
  370. $column->fill($data);
  371. $this->columnNames[] = $column->getName();
  372. });
  373. $this->buildRows($data);
  374. if ($data && $this->responsive) {
  375. $this->responsive->build();
  376. }
  377. $this->sortHeaders();
  378. }
  379. /**
  380. * @return void
  381. */
  382. public function callBuilder()
  383. {
  384. if ($this->builder && ! $this->built) {
  385. call_user_func($this->builder, $this);
  386. }
  387. $this->built = true;
  388. }
  389. /**
  390. * Build the grid rows.
  391. *
  392. * @param array $data
  393. *
  394. * @return void
  395. */
  396. protected function buildRows(array $data)
  397. {
  398. $this->rows = collect($data)->map(function ($model) {
  399. return new Row($this, $model);
  400. });
  401. if ($this->rowsCallback) {
  402. foreach ($this->rowsCallback as $value) {
  403. $value($this->rows);
  404. }
  405. }
  406. }
  407. /**
  408. * Set grid row callback function.
  409. *
  410. * @param Closure $callable
  411. *
  412. * @return Collection|void
  413. */
  414. public function rows(Closure $callable = null)
  415. {
  416. if (is_null($callable)) {
  417. return $this->rows;
  418. }
  419. $this->rowsCallback[] = $callable;
  420. }
  421. /**
  422. * Get create url.
  423. *
  424. * @return string
  425. */
  426. public function getCreateUrl()
  427. {
  428. $queryString = '';
  429. if ($constraints = $this->model()->getConstraints()) {
  430. $queryString = http_build_query($constraints);
  431. }
  432. return sprintf(
  433. '%s/create%s',
  434. $this->resource(),
  435. $queryString ? ('?'.$queryString) : ''
  436. );
  437. }
  438. /**
  439. * @param \Closure $closure
  440. *
  441. * @return Grid\Tools\RowSelector
  442. */
  443. public function rowSelector()
  444. {
  445. return $this->rowSelector ?: ($this->rowSelector = new Grid\Tools\RowSelector($this));
  446. }
  447. /**
  448. * Prepend checkbox column for grid.
  449. *
  450. * @return void
  451. */
  452. protected function prependRowSelectorColumn()
  453. {
  454. if (! $this->options['show_row_selector']) {
  455. return;
  456. }
  457. $rowSelector = $this->rowSelector();
  458. $keyName = $this->getKeyName();
  459. $this->prependColumn(
  460. Grid\Column::SELECT_COLUMN_NAME,
  461. $rowSelector->renderHeader()
  462. )->display(function () use ($rowSelector, $keyName) {
  463. return $rowSelector->renderColumn($this, $this->{$keyName});
  464. });
  465. }
  466. /**
  467. * @param string $width
  468. * @param string $height
  469. *
  470. * @return $this
  471. */
  472. public function setDialogFormDimensions(string $width, string $height)
  473. {
  474. $this->options['dialog_form_area'] = [$width, $height];
  475. return $this;
  476. }
  477. /**
  478. * Render create button for grid.
  479. *
  480. * @return string
  481. */
  482. public function renderCreateButton()
  483. {
  484. if (! $this->options['show_create_button']) {
  485. return '';
  486. }
  487. return (new Tools\CreateButton($this))->render();
  488. }
  489. /**
  490. * @param bool $value
  491. *
  492. * @return $this
  493. */
  494. public function withBorder(bool $value = true)
  495. {
  496. $this->options['show_bordered'] = $value;
  497. return $this;
  498. }
  499. /**
  500. * @param bool $value
  501. *
  502. * @return $this
  503. */
  504. public function tableCollapse(bool $value = true)
  505. {
  506. $this->options['table_collapse'] = $value;
  507. return $this;
  508. }
  509. /**
  510. * Set grid header.
  511. *
  512. * @param Closure|string|Renderable $content
  513. *
  514. * @return $this|Closure
  515. */
  516. public function header($content = null)
  517. {
  518. if (! $content) {
  519. return $this->header;
  520. }
  521. $this->header = $content;
  522. return $this;
  523. }
  524. /**
  525. * Render grid header.
  526. *
  527. * @return string
  528. */
  529. public function renderHeader()
  530. {
  531. if (! $this->header) {
  532. return '';
  533. }
  534. $content = Helper::render($this->header, [$this->processFilter(false)]);
  535. if (empty($content)) {
  536. return '';
  537. }
  538. return <<<HTML
  539. <div class="card-header clearfix" style="border-bottom: 0;background: transparent;padding: 0">{$content}</div>
  540. HTML;
  541. }
  542. /**
  543. * Set grid footer.
  544. *
  545. * @param Closure|string|Renderable $content
  546. *
  547. * @return $this|Closure
  548. */
  549. public function footer($content = null)
  550. {
  551. if (! $content) {
  552. return $this->footer;
  553. }
  554. $this->footer = $content;
  555. return $this;
  556. }
  557. /**
  558. * Render grid footer.
  559. *
  560. * @return string
  561. */
  562. public function renderFooter()
  563. {
  564. if (! $this->footer) {
  565. return '';
  566. }
  567. $content = Helper::render($this->footer, [$this->processFilter(false)]);
  568. if (empty($content)) {
  569. return '';
  570. }
  571. return <<<HTML
  572. <div class="box-footer clearfix">{$content}</div>
  573. HTML;
  574. }
  575. /**
  576. * Get or set option for grid.
  577. *
  578. * @param string $key
  579. * @param mixed $value
  580. *
  581. * @return $this|mixed
  582. */
  583. public function option($key, $value = null)
  584. {
  585. if (is_null($value)) {
  586. return $this->options[$key] ?? null;
  587. }
  588. $this->options[$key] = $value;
  589. return $this;
  590. }
  591. protected function setUpOptions()
  592. {
  593. if ($this->options['show_bordered']) {
  594. $this->tableCollapse(false);
  595. }
  596. }
  597. /**
  598. * Disable row selector.
  599. *
  600. * @return $this
  601. */
  602. public function disableRowSelector(bool $disable = true)
  603. {
  604. $this->tools->disableBatchActions($disable);
  605. return $this->option('show_row_selector', ! $disable);
  606. }
  607. /**
  608. * Show row selector.
  609. *
  610. * @return $this
  611. */
  612. public function showRowSelector(bool $val = true)
  613. {
  614. return $this->disableRowSelector(! $val);
  615. }
  616. /**
  617. * Remove create button on grid.
  618. *
  619. * @return $this
  620. */
  621. public function disableCreateButton(bool $disable = true)
  622. {
  623. return $this->option('show_create_button', ! $disable);
  624. }
  625. /**
  626. * Show create button.
  627. *
  628. * @return $this
  629. */
  630. public function showCreateButton(bool $val = true)
  631. {
  632. return $this->disableCreateButton(! $val);
  633. }
  634. /**
  635. * If allow creation.
  636. *
  637. * @return bool
  638. */
  639. public function allowCreateButton()
  640. {
  641. return $this->options['show_create_button'];
  642. }
  643. /**
  644. * @param string $mode
  645. *
  646. * @return $this
  647. */
  648. public function createMode(string $mode)
  649. {
  650. return $this->option('create_mode', $mode);
  651. }
  652. /**
  653. * @return $this
  654. */
  655. public function enableDialogCreate()
  656. {
  657. return $this->createMode(self::CREATE_MODE_DIALOG);
  658. }
  659. /**
  660. * Get or set resource path.
  661. *
  662. * @param string $path
  663. *
  664. * @return $this|string
  665. */
  666. public function resource(string $path = null)
  667. {
  668. if ($path === null) {
  669. return $this->resourcePath ?: (
  670. $this->resourcePath = url(app('request')->getPathInfo())
  671. );
  672. }
  673. if (! empty($path)) {
  674. $this->resourcePath = admin_url($path);
  675. }
  676. return $this;
  677. }
  678. /**
  679. * Create a grid instance.
  680. *
  681. * @param mixed ...$params
  682. *
  683. * @return $this
  684. */
  685. public static function make(...$params)
  686. {
  687. return new static(...$params);
  688. }
  689. /**
  690. * Enable responsive tables.
  691. *
  692. * @see https://github.com/nadangergeo/RWD-Table-Patterns
  693. *
  694. * @return Responsive
  695. *
  696. * @deprecated 即将在2.0版本中废弃
  697. */
  698. public function responsive()
  699. {
  700. if (! $this->responsive) {
  701. $this->responsive = new Responsive($this);
  702. }
  703. return $this->responsive;
  704. }
  705. /**
  706. * @return bool
  707. */
  708. public function allowResponsive()
  709. {
  710. return $this->responsive ? true : false;
  711. }
  712. /**
  713. * @param Closure $closure
  714. *
  715. * @return $this;
  716. */
  717. public function wrap(\Closure $closure)
  718. {
  719. $this->wrapper = $closure;
  720. return $this;
  721. }
  722. /**
  723. * @return bool
  724. */
  725. public function hasWrapper()
  726. {
  727. return $this->wrapper ? true : false;
  728. }
  729. /**
  730. * Add variables to grid view.
  731. *
  732. * @param array $variables
  733. *
  734. * @return $this
  735. */
  736. public function with($variables = [])
  737. {
  738. $this->variables = $variables;
  739. return $this;
  740. }
  741. /**
  742. * Get all variables will used in grid view.
  743. *
  744. * @return array
  745. */
  746. protected function variables()
  747. {
  748. $this->variables['grid'] = $this;
  749. $this->variables['tableId'] = $this->getTableId();
  750. return $this->variables;
  751. }
  752. /**
  753. * Set a view to render.
  754. *
  755. * @param string $view
  756. * @param array $variables
  757. */
  758. public function view($view, $variables = [])
  759. {
  760. if (! empty($variables)) {
  761. $this->with($variables);
  762. }
  763. $this->view = $view;
  764. }
  765. /**
  766. * Set grid title.
  767. *
  768. * @param string $title
  769. *
  770. * @return $this
  771. */
  772. public function title($title)
  773. {
  774. $this->variables['title'] = $title;
  775. return $this;
  776. }
  777. /**
  778. * Set grid description.
  779. *
  780. * @param string $description
  781. *
  782. * @return $this
  783. */
  784. public function description($description)
  785. {
  786. $this->variables['description'] = $description;
  787. return $this;
  788. }
  789. /**
  790. * Set resource path for grid.
  791. *
  792. * @param string $path
  793. *
  794. * @return $this
  795. */
  796. public function setResource($path)
  797. {
  798. $this->resourcePath = $path;
  799. return $this;
  800. }
  801. /**
  802. * Get the string contents of the grid view.
  803. *
  804. * @return string
  805. */
  806. public function render()
  807. {
  808. try {
  809. $this->callComposing();
  810. $this->build();
  811. $this->applyFixColumns();
  812. $this->setUpOptions();
  813. } catch (\Throwable $e) {
  814. return $this->handleException($e);
  815. }
  816. return $this->doWrap();
  817. }
  818. /**
  819. * @return string
  820. */
  821. protected function doWrap()
  822. {
  823. $view = view($this->view, $this->variables());
  824. if (! $wrapper = $this->wrapper) {
  825. return $view->render();
  826. }
  827. return $wrapper($view);
  828. }
  829. protected function handleException(\Throwable $e)
  830. {
  831. return Admin::makeExceptionHandler()->handle($e);
  832. }
  833. /**
  834. * Add column to grid.
  835. *
  836. * @param string $name
  837. *
  838. * @return Column
  839. */
  840. public function __get($name)
  841. {
  842. return $this->addColumn($name);
  843. }
  844. /**
  845. * Dynamically add columns to the grid view.
  846. *
  847. * @param $method
  848. * @param $arguments
  849. *
  850. * @return Column
  851. */
  852. public function __call($method, $arguments)
  853. {
  854. if (static::hasMacro($method)) {
  855. return $this->macroCall($method, $arguments);
  856. }
  857. return $this->addColumn($method, $arguments[0] ?? null);
  858. }
  859. }