Builder.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910
  1. <?php
  2. namespace Dcat\Admin\Form;
  3. use Closure;
  4. use Dcat\Admin\Admin;
  5. use Dcat\Admin\Contracts\UploadField;
  6. use Dcat\Admin\Form;
  7. use Dcat\Admin\Form\Field\Hidden;
  8. use Dcat\Admin\Form\Step\Builder as StepBuilder;
  9. use Dcat\Admin\IFrameGrid;
  10. use Dcat\Admin\Support\Helper;
  11. use Dcat\Admin\Widgets\DialogForm;
  12. use Illuminate\Contracts\Support\Renderable;
  13. use Illuminate\Support\Arr;
  14. use Illuminate\Support\Collection;
  15. use Illuminate\Support\Facades\URL;
  16. use Illuminate\Support\Str;
  17. /**
  18. * Class Builder.
  19. */
  20. class Builder
  21. {
  22. /**
  23. * 上个页面URL保存的key.
  24. */
  25. const PREVIOUS_URL_KEY = '_previous_';
  26. /**
  27. * 构建时需要忽略的字段.
  28. */
  29. const BUILD_IGNORE = 'build-ignore';
  30. /**
  31. * Modes constants.
  32. */
  33. const MODE_EDIT = 'edit';
  34. const MODE_CREATE = 'create';
  35. const MODE_DELETE = 'delete';
  36. /**
  37. * @var mixed
  38. */
  39. protected $id;
  40. /**
  41. * @var Form
  42. */
  43. protected $form;
  44. /**
  45. * @var
  46. */
  47. protected $action;
  48. /**
  49. * @var Collection
  50. */
  51. protected $fields;
  52. /**
  53. * @var array
  54. */
  55. protected $options = [];
  56. /**
  57. * Form action mode, could be create|view|edit.
  58. *
  59. * @var string
  60. */
  61. protected $mode = self::MODE_CREATE;
  62. /**
  63. * @var array
  64. */
  65. protected $hiddenFields = [];
  66. /**
  67. * @var Tools
  68. */
  69. protected $tools;
  70. /**
  71. * @var Footer
  72. */
  73. protected $footer;
  74. /**
  75. * Width for label and field.
  76. *
  77. * @var array
  78. */
  79. protected $width = [
  80. 'label' => 2,
  81. 'field' => 8,
  82. ];
  83. /**
  84. * View for this form.
  85. *
  86. * @var string
  87. */
  88. protected $view = 'admin::form.container';
  89. /**
  90. * Form title.
  91. *
  92. * @var string
  93. */
  94. protected $title;
  95. /**
  96. * @var BlockForm[]
  97. */
  98. protected $multipleForms = [];
  99. /**
  100. * @var Layout
  101. */
  102. protected $layout;
  103. /**
  104. * @var int
  105. */
  106. protected $defaultBlockWidth = 12;
  107. /**
  108. * @var string
  109. */
  110. protected $elementId;
  111. /**
  112. * @var \Closure
  113. */
  114. protected $wrapper;
  115. /**
  116. * @var bool
  117. */
  118. protected $showHeader = true;
  119. /**
  120. * @var bool
  121. */
  122. protected $showFooter = true;
  123. /**
  124. * @var StepBuilder
  125. */
  126. protected $stepBuilder;
  127. /**
  128. * Builder constructor.
  129. *
  130. * @param Form $form
  131. */
  132. public function __construct(Form $form)
  133. {
  134. $this->form = $form;
  135. $this->fields = new Collection();
  136. $this->layout = new Layout($form);
  137. $this->tools = new Tools($this);
  138. $this->footer = new Footer($this);
  139. }
  140. /**
  141. * @param \Closure $closure
  142. *
  143. * @return Layout
  144. */
  145. public function layout($closure = null)
  146. {
  147. if ($closure) {
  148. $closure($this->layout);
  149. }
  150. return $this->layout;
  151. }
  152. /**
  153. * @param Closure $closure
  154. *
  155. * @return $this;
  156. */
  157. public function wrap(Closure $closure)
  158. {
  159. $this->wrapper = $closure;
  160. return $this;
  161. }
  162. /**
  163. * @return bool
  164. */
  165. public function hasWrapper()
  166. {
  167. return $this->wrapper ? true : false;
  168. }
  169. /**
  170. * @param int $width
  171. *
  172. * @return $this
  173. */
  174. public function setDefaultBlockWidth(int $width)
  175. {
  176. $this->defaultBlockWidth = $width;
  177. return $this;
  178. }
  179. /**
  180. * @param BlockForm $form
  181. */
  182. public function addForm(BlockForm $form)
  183. {
  184. $this->multipleForms[] = $form;
  185. $form->disableResetButton();
  186. $form->disableSubmitButton();
  187. $form->disableFormTag();
  188. return $this;
  189. }
  190. /**
  191. * Get form tools instance.
  192. *
  193. * @return Tools
  194. */
  195. public function tools()
  196. {
  197. return $this->tools;
  198. }
  199. /**
  200. * Get form footer instance.
  201. *
  202. * @return Footer
  203. */
  204. public function footer()
  205. {
  206. return $this->footer;
  207. }
  208. /**
  209. * @param \Closure|StepForm[]|null $builder
  210. *
  211. * @return StepBuilder
  212. */
  213. public function multipleSteps($builder = null)
  214. {
  215. if (! $this->stepBuilder) {
  216. $this->view = 'admin::form.steps';
  217. $this->stepBuilder = new StepBuilder($this->form);
  218. }
  219. if ($builder) {
  220. if ($builder instanceof \Closure) {
  221. $builder($this->stepBuilder);
  222. } elseif (is_array($builder)) {
  223. $this->stepBuilder->add($builder);
  224. }
  225. }
  226. return $this->stepBuilder;
  227. }
  228. /**
  229. * @return StepBuilder
  230. */
  231. public function stepBuilder()
  232. {
  233. return $this->stepBuilder;
  234. }
  235. /**
  236. * Set the builder mode.
  237. *
  238. * @param string $mode
  239. *
  240. * @return void|string
  241. */
  242. public function mode(string $mode = null)
  243. {
  244. if ($mode === null) {
  245. return $this->mode;
  246. }
  247. $this->mode = $mode;
  248. }
  249. /**
  250. * Returns builder is $mode.
  251. *
  252. * @param $mode
  253. *
  254. * @return bool
  255. */
  256. public function isMode($mode)
  257. {
  258. return $this->mode == $mode;
  259. }
  260. /**
  261. * Check if is creating resource.
  262. *
  263. * @return bool
  264. */
  265. public function isCreating()
  266. {
  267. return $this->isMode(static::MODE_CREATE);
  268. }
  269. /**
  270. * Check if is editing resource.
  271. *
  272. * @return bool
  273. */
  274. public function isEditing()
  275. {
  276. return $this->isMode(static::MODE_EDIT);
  277. }
  278. /**
  279. * Check if is deleting resource.
  280. *
  281. * @return bool
  282. */
  283. public function isDeleting()
  284. {
  285. return $this->isMode(static::MODE_DELETE);
  286. }
  287. /**
  288. * Set resource Id.
  289. *
  290. * @param $id
  291. *
  292. * @return mixed|void
  293. */
  294. public function setResourceId($id)
  295. {
  296. $this->id = $id;
  297. }
  298. /**
  299. * @return mixed
  300. */
  301. public function getResourceId()
  302. {
  303. return $this->id;
  304. }
  305. /**
  306. * @return string
  307. */
  308. public function getResource($slice = null)
  309. {
  310. if ($this->mode == self::MODE_CREATE) {
  311. return $this->form->getResource(-1);
  312. }
  313. if ($slice !== null) {
  314. return $this->form->getResource($slice);
  315. }
  316. return $this->form->getResource();
  317. }
  318. /**
  319. * @param int $field
  320. * @param int $label
  321. *
  322. * @return $this
  323. */
  324. public function width($field = 8, $label = 2)
  325. {
  326. $this->width = [
  327. 'label' => $label,
  328. 'field' => $field,
  329. ];
  330. return $this;
  331. }
  332. /**
  333. * Get label and field width.
  334. *
  335. * @return array
  336. */
  337. public function getWidth()
  338. {
  339. return $this->width;
  340. }
  341. /**
  342. * Get or set action for form.
  343. *
  344. * @return string|void
  345. */
  346. public function action($action = null)
  347. {
  348. if ($action !== null) {
  349. $this->action = $action;
  350. return;
  351. }
  352. if ($this->action) {
  353. return $this->action;
  354. }
  355. if ($this->isMode(static::MODE_EDIT)) {
  356. return $this->form->getResource().'/'.$this->id;
  357. }
  358. if ($this->isMode(static::MODE_CREATE)) {
  359. return $this->form->getResource(-1);
  360. }
  361. return '';
  362. }
  363. /**
  364. * Set view for this form.
  365. *
  366. * @param string $view
  367. *
  368. * @return $this
  369. */
  370. public function view($view)
  371. {
  372. $this->view = $view;
  373. return $this;
  374. }
  375. /**
  376. * Get or set title for form.
  377. *
  378. * @param string $title
  379. *
  380. * @return $this|string
  381. */
  382. public function title($title = null)
  383. {
  384. if ($title !== null) {
  385. $this->title = $title;
  386. return $this;
  387. }
  388. if ($this->title) {
  389. return $this->title;
  390. }
  391. if ($this->mode == static::MODE_CREATE) {
  392. return trans('admin.create');
  393. }
  394. if ($this->mode == static::MODE_EDIT) {
  395. return trans('admin.edit');
  396. }
  397. return '';
  398. }
  399. /**
  400. * Get fields of this builder.
  401. *
  402. * @return Collection
  403. */
  404. public function fields()
  405. {
  406. return $this->fields;
  407. }
  408. /**
  409. * Get specify field.
  410. *
  411. * @param string|Field $name
  412. *
  413. * @return Field|null
  414. */
  415. public function field($name)
  416. {
  417. return $this->fields->first(function (Field $field) use ($name) {
  418. return $field === $name || $field->column() == $name;
  419. });
  420. }
  421. /**
  422. * @param string $name
  423. *
  424. * @return Field|null
  425. */
  426. public function stepField($name)
  427. {
  428. if (! $builder = $this->stepBuilder()) {
  429. return;
  430. }
  431. foreach ($builder->all() as $step) {
  432. if ($field = $step->field($name)) {
  433. return $field;
  434. }
  435. }
  436. }
  437. /**
  438. * @return Field[]|Collection
  439. */
  440. public function stepFields()
  441. {
  442. $fields = new Collection();
  443. if (! $builder = $this->stepBuilder()) {
  444. return $fields;
  445. }
  446. foreach ($builder->all() as $step) {
  447. $fields = $fields->merge($step->fields());
  448. }
  449. return $fields;
  450. }
  451. /**
  452. * @param $column
  453. *
  454. * @return void
  455. */
  456. public function removeField($column)
  457. {
  458. $this->fields = $this->fields->filter(function (Field $field) use ($column) {
  459. return $field->column() != $column;
  460. });
  461. }
  462. /**
  463. * If the parant form has rows.
  464. *
  465. * @return bool
  466. */
  467. public function hasRows()
  468. {
  469. return ! empty($this->form->rows());
  470. }
  471. /**
  472. * Get field rows of form.
  473. *
  474. * @return array
  475. */
  476. public function rows()
  477. {
  478. return $this->form->rows();
  479. }
  480. /**
  481. * @return Form
  482. */
  483. public function form()
  484. {
  485. return $this->form;
  486. }
  487. /**
  488. * @return array
  489. */
  490. public function hiddenFields()
  491. {
  492. return $this->hiddenFields;
  493. }
  494. /**
  495. * @param Field $field
  496. *
  497. * @return void
  498. */
  499. public function addHiddenField(Field $field)
  500. {
  501. $this->hiddenFields[] = $field;
  502. }
  503. /**
  504. * Add or get options.
  505. *
  506. * @param array $options
  507. *
  508. * @return array|null
  509. */
  510. public function options($options = [])
  511. {
  512. if (empty($options)) {
  513. return $this->options;
  514. }
  515. $this->options = array_merge($this->options, $options);
  516. }
  517. /**
  518. * Get or set option.
  519. *
  520. * @param string $option
  521. * @param mixed $value
  522. *
  523. * @return void
  524. */
  525. public function option($option, $value = null)
  526. {
  527. if (func_num_args() == 1) {
  528. return Arr::get($this->options, $option);
  529. }
  530. $this->options[$option] = $value;
  531. }
  532. /**
  533. * @param bool $disable
  534. *
  535. * @return void
  536. */
  537. public function disableHeader(bool $disable = true)
  538. {
  539. $this->showHeader = ! $disable;
  540. }
  541. /**
  542. * @param bool $disable
  543. *
  544. * @return void
  545. */
  546. public function disableFooter(bool $disable = true)
  547. {
  548. $this->showFooter = ! $disable;
  549. }
  550. /**
  551. * @param $id
  552. *
  553. * @return void
  554. */
  555. public function setElementId($id)
  556. {
  557. $this->elementId = $id;
  558. }
  559. /**
  560. * @return string
  561. */
  562. public function getElementId()
  563. {
  564. return $this->elementId ?: ($this->elementId = 'form-'.Str::random(8));
  565. }
  566. /**
  567. * Determine if form fields has files.
  568. *
  569. * @return bool
  570. */
  571. public function hasFile()
  572. {
  573. foreach ($this->fields() as $field) {
  574. if (
  575. $field instanceof UploadField
  576. || $field instanceof Form\Field\BootstrapFile
  577. ) {
  578. return true;
  579. }
  580. }
  581. return false;
  582. }
  583. /**
  584. * Add field for store redirect url after update or store.
  585. *
  586. * @return void
  587. */
  588. protected function addRedirectUrlField()
  589. {
  590. $previous = URL::previous();
  591. if (! $previous || $previous == URL::current()) {
  592. return;
  593. }
  594. if (
  595. Str::contains($previous, url($this->getResource()))
  596. && ! Helper::urlHasQuery($previous, [IFrameGrid::QUERY_NAME, DialogForm::QUERY_NAME])
  597. ) {
  598. $this->addHiddenField(
  599. (new Hidden(static::PREVIOUS_URL_KEY))->value($previous)
  600. );
  601. }
  602. }
  603. /**
  604. * Open up a new HTML form.
  605. *
  606. * @param array $options
  607. *
  608. * @return string
  609. */
  610. public function open($options = [])
  611. {
  612. $attributes = [];
  613. if ($this->isMode(static::MODE_EDIT)) {
  614. $this->addHiddenField((new Hidden('_method'))->value('PUT'));
  615. }
  616. $this->addRedirectUrlField();
  617. $attributes['id'] = $this->getElementId();
  618. $attributes['action'] = $this->action();
  619. $attributes['method'] = Arr::get($options, 'method', 'post');
  620. $attributes['accept-charset'] = 'UTF-8';
  621. $attributes['data-toggle'] = 'validator';
  622. $attributes['class'] = Arr::get($options, 'class');
  623. if ($this->hasFile()) {
  624. $attributes['enctype'] = 'multipart/form-data';
  625. }
  626. $html = [];
  627. foreach ($attributes as $name => $value) {
  628. $html[] = "$name=\"$value\"";
  629. }
  630. return '<form '.implode(' ', $html).' pjax-container>';
  631. }
  632. /**
  633. * Close the current form.
  634. *
  635. * @return string
  636. */
  637. public function close()
  638. {
  639. $this->form = null;
  640. $this->fields = null;
  641. return '</form>';
  642. }
  643. /**
  644. * 移除需要忽略的字段.
  645. *
  646. * @return void
  647. */
  648. protected function removeIgnoreFields()
  649. {
  650. $this->fields = $this->fields()->reject(function (Field $field) {
  651. return $field->hasAttribute(static::BUILD_IGNORE);
  652. });
  653. }
  654. /**
  655. * Remove reserved fields like `id` `created_at` `updated_at` in form fields.
  656. *
  657. * @return void
  658. */
  659. protected function removeReservedFields()
  660. {
  661. if (! $this->isMode(static::MODE_CREATE)) {
  662. return;
  663. }
  664. $reservedColumns = [
  665. $this->form->keyName(),
  666. $this->form->createdAtColumn(),
  667. $this->form->updatedAtColumn(),
  668. ];
  669. $this->fields = $this->fields()->reject(function (Field $field) use (&$reservedColumns) {
  670. return in_array($field->column(), $reservedColumns)
  671. && $field instanceof Form\Field\Display;
  672. });
  673. }
  674. /**
  675. * Render form header tools.
  676. *
  677. * @return string
  678. */
  679. public function renderTools()
  680. {
  681. return $this->tools->render();
  682. }
  683. /**
  684. * Render form footer.
  685. *
  686. * @return string
  687. */
  688. public function renderFooter()
  689. {
  690. if (! $this->showFooter) {
  691. return;
  692. }
  693. return $this->footer->render();
  694. }
  695. /**
  696. * Render form.
  697. *
  698. * @return string
  699. */
  700. public function render()
  701. {
  702. $this->removeIgnoreFields();
  703. $this->removeReservedFields();
  704. $tabObj = $this->form->getTab();
  705. if (! $tabObj->isEmpty()) {
  706. $this->setupTabScript();
  707. }
  708. if ($this->form->allowAjaxSubmit() && empty($this->stepBuilder)) {
  709. $this->setupSubmitScript();
  710. }
  711. $open = $this->open(['class' => 'form-horizontal']);
  712. $data = [
  713. 'form' => $this,
  714. 'tabObj' => $tabObj,
  715. 'width' => $this->width,
  716. 'elementId' => $this->getElementId(),
  717. 'showHeader' => $this->showHeader,
  718. 'steps' => $this->stepBuilder,
  719. ];
  720. $this->layout->prepend(
  721. $this->defaultBlockWidth,
  722. $this->doWrap(view($this->view, $data))
  723. );
  724. return <<<EOF
  725. {$open} {$this->layout->build()} {$this->close()}
  726. EOF;
  727. }
  728. /**
  729. * @param Renderable $view
  730. *
  731. * @return string
  732. */
  733. protected function doWrap(Renderable $view)
  734. {
  735. if ($wrapper = $this->wrapper) {
  736. return $wrapper($view);
  737. }
  738. return "<div class='card dcat-box'>{$view->render()}</div>";
  739. }
  740. /**
  741. * @return void
  742. */
  743. protected function setupSubmitScript()
  744. {
  745. Admin::script(
  746. <<<JS
  747. $('#{$this->getElementId()}').form({
  748. validate: true,
  749. });
  750. JS
  751. );
  752. }
  753. /**
  754. * @return void
  755. */
  756. protected function setupTabScript()
  757. {
  758. $elementId = $this->getElementId();
  759. $script = <<<JS
  760. (function () {
  761. var hash = document.location.hash;
  762. if (hash) {
  763. $('#$elementId .nav-tabs a[href="' + hash + '"]').tab('show');
  764. }
  765. // Change hash for page-reload
  766. $('#$elementId .nav-tabs a').on('shown.bs.tab', function (e) {
  767. history.pushState(null,null, e.target.hash);
  768. });
  769. if ($('#$elementId .has-error').length) {
  770. $('#$elementId .has-error').each(function () {
  771. var tabId = '#'+$(this).closest('.tab-pane').attr('id');
  772. $('li a[href="'+tabId+'"] i').removeClass('hide');
  773. });
  774. var first = $('#$elementId .has-error:first').closest('.tab-pane').attr('id');
  775. $('li a[href="#'+first+'"]').tab('show');
  776. }
  777. })();
  778. JS;
  779. Admin::script($script);
  780. }
  781. }