Form.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815
  1. <?php
  2. namespace Dcat\Admin\Widgets;
  3. use Closure;
  4. use Dcat\Admin\Admin;
  5. use Dcat\Admin\Form\Concerns\HandleCascadeFields;
  6. use Dcat\Admin\Form\Concerns\HasRows;
  7. use Dcat\Admin\Form\Concerns\HasTabs;
  8. use Dcat\Admin\Form\Field;
  9. use Dcat\Admin\Form\Layout;
  10. use Dcat\Admin\Support\Helper;
  11. use Dcat\Admin\Traits\HasAuthorization;
  12. use Dcat\Admin\Traits\HasFormResponse;
  13. use Dcat\Admin\Traits\HasHtmlAttributes;
  14. use Illuminate\Contracts\Support\Arrayable;
  15. use Illuminate\Contracts\Support\Renderable;
  16. use Illuminate\Http\Request;
  17. use Illuminate\Support\Arr;
  18. use Illuminate\Support\Collection;
  19. use Illuminate\Support\Fluent;
  20. use Illuminate\Support\MessageBag;
  21. use Illuminate\Support\Str;
  22. use Illuminate\Support\Traits\Macroable;
  23. use Illuminate\Validation\Validator;
  24. /**
  25. * Class Form.
  26. *
  27. * @method Field\Text text($column, $label = '')
  28. * @method Field\Checkbox checkbox($column, $label = '')
  29. * @method Field\Radio radio($column, $label = '')
  30. * @method Field\Select select($column, $label = '')
  31. * @method Field\MultipleSelect multipleSelect($column, $label = '')
  32. * @method Field\Textarea textarea($column, $label = '')
  33. * @method Field\Hidden hidden($column, $label = '')
  34. * @method Field\Id id($column, $label = '')
  35. * @method Field\Ip ip($column, $label = '')
  36. * @method Field\Url url($column, $label = '')
  37. * @method Field\Email email($column, $label = '')
  38. * @method Field\Mobile mobile($column, $label = '')
  39. * @method Field\Slider slider($column, $label = '')
  40. * @method Field\Map map($latitude, $longitude, $label = '')
  41. * @method Field\Editor editor($column, $label = '')
  42. * @method Field\Date date($column, $label = '')
  43. * @method Field\Datetime datetime($column, $label = '')
  44. * @method Field\Time time($column, $label = '')
  45. * @method Field\Year year($column, $label = '')
  46. * @method Field\Month month($column, $label = '')
  47. * @method Field\DateRange dateRange($start, $end, $label = '')
  48. * @method Field\DateTimeRange datetimeRange($start, $end, $label = '')
  49. * @method Field\TimeRange timeRange($start, $end, $label = '')
  50. * @method Field\Number number($column, $label = '')
  51. * @method Field\Currency currency($column, $label = '')
  52. * @method Field\SwitchField switch($column, $label = '')
  53. * @method Field\Display display($column, $label = '')
  54. * @method Field\Rate rate($column, $label = '')
  55. * @method Field\Divide divider()
  56. * @method Field\Password password($column, $label = '')
  57. * @method Field\Decimal decimal($column, $label = '')
  58. * @method Field\Html html($html, $label = '')
  59. * @method Field\Tags tags($column, $label = '')
  60. * @method Field\Icon icon($column, $label = '')
  61. * @method Field\Embeds embeds($column, $label = '')
  62. * @method Field\Captcha captcha($column, $label = '')
  63. * @method Field\Listbox listbox($column, $label = '')
  64. * @method Field\SelectResource selectResource($column, $label = '')
  65. * @method Field\File file($column, $label = '')
  66. * @method Field\Image image($column, $label = '')
  67. * @method Field\MultipleFile multipleFile($column, $label = '')
  68. * @method Field\MultipleImage multipleImage($column, $label = '')
  69. * @method Field\HasMany hasMany($column, \Closure $callback)
  70. * @method Field\Tree tree($column, $label = '')
  71. * @method Field\Table table($column, $callback)
  72. * @method Field\ListField list($column, $label = '')
  73. * @method Field\Timezone timezone($column, $label = '')
  74. * @method Field\KeyValue keyValue($column, $label = '')
  75. * @method Field\Tel tel($column, $label = '')
  76. * @method Field\Markdown markdown($column, $label = '')
  77. * @method Field\Range range($start, $end, $label = '')
  78. */
  79. class Form implements Renderable
  80. {
  81. use HasHtmlAttributes,
  82. HasFormResponse,
  83. HasAuthorization,
  84. HandleCascadeFields,
  85. HasRows,
  86. HasTabs,
  87. Macroable {
  88. __call as macroCall;
  89. }
  90. const REQUEST_NAME = '_form_';
  91. /**
  92. * @var string
  93. */
  94. protected $view = 'admin::widgets.form';
  95. /**
  96. * @var Field[]|Collection
  97. */
  98. protected $fields;
  99. /**
  100. * @var Layout
  101. */
  102. protected $layout;
  103. /**
  104. * @var array
  105. */
  106. protected $variables = [];
  107. /**
  108. * @var bool
  109. */
  110. protected $useAjaxSubmit = true;
  111. /**
  112. * @var Fluent
  113. */
  114. protected $data;
  115. /**
  116. * @var mixed
  117. */
  118. protected $primaryKey;
  119. /**
  120. * Available buttons.
  121. *
  122. * @var array
  123. */
  124. protected $buttons = ['reset' => true, 'submit' => true];
  125. /**
  126. * @var bool
  127. */
  128. protected $useFormTag = true;
  129. /**
  130. * @var string
  131. */
  132. protected $elementId;
  133. /**
  134. * @var array
  135. */
  136. protected $width = [
  137. 'label' => 2,
  138. 'field' => 8,
  139. ];
  140. /**
  141. * @var array
  142. */
  143. protected $confirm = [];
  144. /**
  145. * Form constructor.
  146. *
  147. * @param array $data
  148. * @param mixed $key
  149. */
  150. public function __construct($data = [], $key = null)
  151. {
  152. if ($data) {
  153. $this->fill($data);
  154. }
  155. $this->setKey($key);
  156. $this->initFields();
  157. $this->initFormAttributes();
  158. }
  159. /**
  160. * Initialize the form fields.
  161. */
  162. protected function initFields()
  163. {
  164. $this->fields = new Collection();
  165. }
  166. /**
  167. * Initialize the form attributes.
  168. */
  169. protected function initFormAttributes()
  170. {
  171. $this->setHtmlAttribute([
  172. 'method' => 'POST',
  173. 'action' => '',
  174. 'class' => 'form-horizontal',
  175. 'accept-charset' => 'UTF-8',
  176. 'pjax-container' => true,
  177. ]);
  178. }
  179. /**
  180. * Action uri of the form.
  181. *
  182. * @param string $action
  183. *
  184. * @return $this|string
  185. */
  186. public function action($action = null)
  187. {
  188. if ($action === null) {
  189. return $this->getHtmlAttribute('action');
  190. }
  191. return $this->setHtmlAttribute('action', admin_url($action));
  192. }
  193. /**
  194. * Method of the form.
  195. *
  196. * @param string $method
  197. *
  198. * @return $this
  199. */
  200. public function method(string $method = 'POST')
  201. {
  202. return $this->setHtmlAttribute('method', strtoupper($method));
  203. }
  204. /**
  205. * @param string $title
  206. * @param string $content
  207. *
  208. * @return $this
  209. */
  210. public function confirm(?string $title = null, ?string $content = null)
  211. {
  212. $this->confirm['title'] = $title;
  213. $this->confirm['content'] = $content;
  214. return $this;
  215. }
  216. /**
  217. * Set primary key.
  218. *
  219. * @param mixed $value
  220. *
  221. * @return $this
  222. */
  223. public function setKey($value)
  224. {
  225. $this->primaryKey = $value;
  226. return $this;
  227. }
  228. /**
  229. * Get primary key.
  230. *
  231. * @return mixed
  232. */
  233. public function getKey()
  234. {
  235. return $this->primaryKey;
  236. }
  237. /**
  238. * @param array|Arrayable|Closure $data
  239. *
  240. * @return Fluent
  241. */
  242. public function data()
  243. {
  244. if (! $this->data) {
  245. $this->fill([]);
  246. }
  247. return $this->data;
  248. }
  249. /**
  250. * @param array|Arrayable|Closure $data
  251. *
  252. * @return $this
  253. */
  254. public function fill($data)
  255. {
  256. $this->data = new Fluent(Helper::array($data));
  257. return $this;
  258. }
  259. /**
  260. * @return Fluent
  261. */
  262. public function model()
  263. {
  264. return $this->data();
  265. }
  266. /**
  267. * Add a fieldset to form.
  268. *
  269. * @param string $title
  270. * @param Closure $setCallback
  271. *
  272. * @return Field\Fieldset
  273. */
  274. public function fieldset(string $title, Closure $setCallback)
  275. {
  276. $fieldset = new Field\Fieldset();
  277. $this->html($fieldset->start($title))->plain();
  278. $setCallback($this);
  279. $this->html($fieldset->end())->plain();
  280. return $fieldset;
  281. }
  282. /**
  283. * Get specify field.
  284. *
  285. * @param string|Field $name
  286. *
  287. * @return Field|null
  288. */
  289. public function field($name)
  290. {
  291. foreach ($this->fields as $field) {
  292. if ($field === $name || $field->column() === $name) {
  293. return $field;
  294. }
  295. }
  296. }
  297. /**
  298. * @return Field[]|Collection
  299. */
  300. public function fields()
  301. {
  302. return $this->fields;
  303. }
  304. /**
  305. * @param int|float $width
  306. * @param Closure $callback
  307. *
  308. * @return $this
  309. */
  310. public function column($width, \Closure $callback)
  311. {
  312. $this->layout()->onlyColumn($width, function () use ($callback) {
  313. $callback($this);
  314. });
  315. return $this;
  316. }
  317. /**
  318. * @return Layout
  319. */
  320. public function layout()
  321. {
  322. return $this->layout ?: ($this->layout = new Layout($this));
  323. }
  324. /**
  325. * Validate this form fields.
  326. *
  327. * @param Request $request
  328. *
  329. * @return bool|MessageBag
  330. */
  331. public function validate(Request $request)
  332. {
  333. if (method_exists($this, 'form')) {
  334. $this->form();
  335. }
  336. $failedValidators = [];
  337. /** @var \Dcat\Admin\Form\Field $field */
  338. foreach ($this->fields() as $field) {
  339. if (! $validator = $field->getValidator($request->all())) {
  340. continue;
  341. }
  342. if (($validator instanceof Validator) && ! $validator->passes()) {
  343. $failedValidators[] = $validator;
  344. }
  345. }
  346. $message = $this->mergeValidationMessages($failedValidators);
  347. return $message->any() ? $message : false;
  348. }
  349. /**
  350. * Merge validation messages from input validators.
  351. *
  352. * @param \Illuminate\Validation\Validator[] $validators
  353. *
  354. * @return MessageBag
  355. */
  356. protected function mergeValidationMessages($validators)
  357. {
  358. $messageBag = new MessageBag();
  359. foreach ($validators as $validator) {
  360. $messageBag = $messageBag->merge($validator->messages());
  361. }
  362. return $messageBag;
  363. }
  364. /**
  365. * Disable Pjax.
  366. *
  367. * @return $this
  368. */
  369. public function disablePjax()
  370. {
  371. $this->forgetHtmlAttribute('pjax-container');
  372. return $this;
  373. }
  374. /**
  375. * Disable form tag.
  376. *
  377. * @return $this;
  378. */
  379. public function disableFormTag()
  380. {
  381. $this->useFormTag = false;
  382. return $this;
  383. }
  384. /**
  385. * Disable reset button.
  386. *
  387. * @return $this
  388. */
  389. public function disableResetButton()
  390. {
  391. $this->buttons['reset'] = false;
  392. return $this;
  393. }
  394. /**
  395. * Disable submit button.
  396. *
  397. * @return $this
  398. */
  399. public function disableSubmitButton()
  400. {
  401. $this->buttons['submit'] = false;
  402. return $this;
  403. }
  404. /**
  405. * Set field and label width in current form.
  406. *
  407. * @param int $fieldWidth
  408. * @param int $labelWidth
  409. *
  410. * @return $this
  411. */
  412. public function width($fieldWidth = 8, $labelWidth = 2)
  413. {
  414. $this->width = [
  415. 'label' => $labelWidth,
  416. 'field' => $fieldWidth,
  417. ];
  418. $this->fields->each(function ($field) use ($fieldWidth, $labelWidth) {
  419. /* @var Field $field */
  420. $field->width($fieldWidth, $labelWidth);
  421. });
  422. return $this;
  423. }
  424. /**
  425. * Find field class with given name.
  426. *
  427. * @param string $method
  428. *
  429. * @return bool|string
  430. */
  431. public static function findFieldClass($method)
  432. {
  433. $class = Arr::get(\Dcat\Admin\Form::extensions(), $method);
  434. if (class_exists($class)) {
  435. return $class;
  436. }
  437. return false;
  438. }
  439. /**
  440. * Add a form field to form.
  441. *
  442. * @param Field $field
  443. *
  444. * @return $this
  445. */
  446. public function pushField(Field $field)
  447. {
  448. $this->fields->push($field);
  449. if ($this->layout) {
  450. $this->layout->addField($field);
  451. }
  452. $field->setForm($this);
  453. $field->width($this->width['field'], $this->width['label']);
  454. if ($field instanceof Field\File) {
  455. $formData = [static::REQUEST_NAME => get_called_class()];
  456. $field->url(route(admin_api_route('form.upload')));
  457. $field->deleteUrl(route(admin_api_route('form.destroy-file'), $formData));
  458. $field->withFormData($formData);
  459. }
  460. $field::collectAssets();
  461. return $this;
  462. }
  463. /**
  464. * Get variables for render form.
  465. *
  466. * @return array
  467. */
  468. protected function variables()
  469. {
  470. $this->setHtmlAttribute('id', $this->getElementId());
  471. $this->fillFields($this->model()->toArray());
  472. return array_merge([
  473. 'start' => $this->open(),
  474. 'end' => $this->close(),
  475. 'fields' => $this->fields,
  476. 'method' => $this->getHtmlAttribute('method'),
  477. 'buttons' => $this->buttons,
  478. 'rows' => $this->rows(),
  479. 'layout' => $this->layout,
  480. ], $this->variables);
  481. }
  482. public function addVariables(array $variables)
  483. {
  484. $this->variables = array_merge($this->variables, $variables);
  485. return $this;
  486. }
  487. public function fillFields(array $data)
  488. {
  489. foreach ($this->fields as $field) {
  490. $field->fill($data);
  491. }
  492. }
  493. /**
  494. * @return string
  495. */
  496. protected function open()
  497. {
  498. return <<<HTML
  499. <form {$this->formatHtmlAttributes()}>
  500. HTML;
  501. }
  502. /**
  503. * @return string
  504. */
  505. protected function close()
  506. {
  507. return '</form>';
  508. }
  509. /**
  510. * Determine if form fields has files.
  511. *
  512. * @return bool
  513. */
  514. public function hasFile()
  515. {
  516. foreach ($this->fields as $field) {
  517. if ($field instanceof Field\File) {
  518. return true;
  519. }
  520. }
  521. return false;
  522. }
  523. /**
  524. * @param $id
  525. *
  526. * @return $this
  527. */
  528. public function setFormId($id)
  529. {
  530. $this->elementId = $id;
  531. return $this;
  532. }
  533. /**
  534. * @return string
  535. */
  536. public function getElementId()
  537. {
  538. return $this->elementId ?: ($this->elementId = 'form-'.Str::random(8));
  539. }
  540. /**
  541. * Generate a Field object and add to form builder if Field exists.
  542. *
  543. * @param string $method
  544. * @param array $arguments
  545. *
  546. * @return Field|null
  547. */
  548. public function __call($method, $arguments)
  549. {
  550. if ($className = static::findFieldClass($method)) {
  551. $name = Arr::get($arguments, 0, '');
  552. $element = new $className($name, array_slice($arguments, 1));
  553. $this->pushField($element);
  554. return $element;
  555. }
  556. if (static::hasMacro($method)) {
  557. return $this->macroCall($method, $arguments);
  558. }
  559. throw new \BadMethodCallException("Field [{$method}] does not exist.");
  560. }
  561. /**
  562. * Disable submit with ajax.
  563. *
  564. * @param bool $disable
  565. *
  566. * @return $this
  567. */
  568. public function disableAjaxSubmit(bool $disable = true)
  569. {
  570. $this->useAjaxSubmit = ! $disable;
  571. return $this;
  572. }
  573. /**
  574. * @return bool
  575. */
  576. public function allowAjaxSubmit()
  577. {
  578. return $this->useAjaxSubmit === true;
  579. }
  580. /**
  581. * @return void
  582. */
  583. protected function setUpSubmitScript()
  584. {
  585. $confirm = json_encode($this->confirm);
  586. Admin::script(
  587. <<<JS
  588. $('#{$this->getElementId()}').form({
  589. validate: true,
  590. confirm: {$confirm},
  591. success: function (data) {
  592. {$this->buildSuccessScript()}
  593. {$this->addSavedScript()}
  594. },
  595. error: function (response) {
  596. {$this->buildErrorScript()}
  597. {$this->addErrorScript()}
  598. }
  599. });
  600. JS
  601. );
  602. }
  603. /**
  604. * @return string|void
  605. *
  606. * @deprecated 将在2.0版本中废弃,请用 addSavedScript 代替
  607. */
  608. protected function buildSuccessScript()
  609. {
  610. }
  611. /**
  612. * @return string|void
  613. */
  614. protected function addSavedScript()
  615. {
  616. }
  617. /**
  618. * @return string|void
  619. *
  620. * @deprecated 将在2.0版本中废弃,请用 addErrorScript 代替
  621. */
  622. protected function buildErrorScript()
  623. {
  624. }
  625. /**
  626. * @return string|void
  627. */
  628. protected function addErrorScript()
  629. {
  630. }
  631. /**
  632. * @param array $input
  633. *
  634. * @return array
  635. */
  636. public function sanitize(array $input)
  637. {
  638. Arr::forget($input, [static::REQUEST_NAME, '_token', '_current_']);
  639. return $input;
  640. }
  641. protected function prepareForm()
  642. {
  643. if (method_exists($this, 'form')) {
  644. $this->form();
  645. }
  646. if (! $this->data && method_exists($this, 'default')) {
  647. $data = $this->default();
  648. if (is_array($data)) {
  649. $this->fill($data);
  650. }
  651. }
  652. }
  653. protected function prepareHandler()
  654. {
  655. if (method_exists($this, 'handle')) {
  656. $this->method('POST');
  657. $this->action(route(admin_api_route('form')));
  658. $this->hidden(static::REQUEST_NAME)->default(get_called_class());
  659. $this->hidden('_current_')->default($this->getCurrentUrl());
  660. }
  661. }
  662. /**
  663. * Render the form.
  664. *
  665. * @return string
  666. */
  667. public function render()
  668. {
  669. $this->prepareForm();
  670. $this->prepareHandler();
  671. if ($this->allowAjaxSubmit()) {
  672. $this->setUpSubmitScript();
  673. }
  674. $tabObj = $this->getTab();
  675. if (! $tabObj->isEmpty()) {
  676. $tabObj->addScript();
  677. }
  678. $this->addVariables(['tabObj' => $tabObj]);
  679. return view($this->view, $this->variables())->render();
  680. }
  681. /**
  682. * Output as string.
  683. *
  684. * @return string
  685. */
  686. public function __toString()
  687. {
  688. return $this->render();
  689. }
  690. /**
  691. * @param mixed ...$params
  692. *
  693. * @return $this
  694. */
  695. public static function make(...$params)
  696. {
  697. return new static(...$params);
  698. }
  699. }