HasMany.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720
  1. <?php
  2. namespace Dcat\Admin\Form\Field;
  3. use Dcat\Admin\Admin;
  4. use Dcat\Admin\Form;
  5. use Dcat\Admin\Form\Field;
  6. use Dcat\Admin\Form\NestedForm;
  7. use Illuminate\Database\Eloquent\Relations\HasMany as Relation;
  8. use Illuminate\Database\Eloquent\Relations\MorphMany;
  9. use Illuminate\Support\Arr;
  10. use Illuminate\Support\Facades\Validator;
  11. use Illuminate\Support\Str;
  12. /**
  13. * Class HasMany.
  14. */
  15. class HasMany extends Field
  16. {
  17. /**
  18. * Relation name.
  19. *
  20. * @var string
  21. */
  22. protected $relationName = '';
  23. /**
  24. * Relation key name.
  25. *
  26. * @var string
  27. */
  28. protected $relationKeyName = 'id';
  29. /**
  30. * Form builder.
  31. *
  32. * @var \Closure
  33. */
  34. protected $builder = null;
  35. /**
  36. * Form data.
  37. *
  38. * @var array
  39. */
  40. protected $value = [];
  41. /**
  42. * View Mode.
  43. *
  44. * Supports `default` and `tab` currently.
  45. *
  46. * @var string
  47. */
  48. protected $viewMode = 'default';
  49. /**
  50. * Available views for HasMany field.
  51. *
  52. * @var array
  53. */
  54. protected $views = [
  55. 'default' => 'admin::form.hasmany',
  56. 'tab' => 'admin::form.hasmanytab',
  57. 'table' => 'admin::form.hasmanytable',
  58. ];
  59. /**
  60. * Options for template.
  61. *
  62. * @var array
  63. */
  64. protected $options = [
  65. 'allowCreate' => true,
  66. 'allowDelete' => true,
  67. ];
  68. /**
  69. * Create a new HasMany field instance.
  70. *
  71. * @param $relationName
  72. * @param array $arguments
  73. */
  74. public function __construct($relationName, $arguments = [])
  75. {
  76. $this->relationName = $relationName;
  77. $this->column = $relationName;
  78. if (count($arguments) == 1) {
  79. $this->label = $this->formatLabel();
  80. $this->builder = $arguments[0];
  81. }
  82. if (count($arguments) == 2) {
  83. list($this->label, $this->builder) = $arguments;
  84. }
  85. }
  86. /**
  87. * Get validator for this field.
  88. *
  89. * @param array $input
  90. *
  91. * @return bool|Validator
  92. */
  93. public function getValidator(array $input)
  94. {
  95. if (!array_key_exists($this->column, $input)) {
  96. return false;
  97. }
  98. $input = Arr::only($input, $this->column);
  99. $form = $this->buildNestedForm($this->column, $this->builder);
  100. $rules = $attributes = $messages = [];
  101. /* @var Field $field */
  102. foreach ($form->fields() as $field) {
  103. if (!$fieldRules = $field->getRules()) {
  104. continue;
  105. }
  106. $column = $field->column();
  107. if (is_array($column)) {
  108. foreach ($column as $key => $name) {
  109. $rules[$name.$key] = $fieldRules;
  110. }
  111. $this->resetInputKey($input, $column);
  112. } else {
  113. $rules[$column] = $fieldRules;
  114. }
  115. $attributes = array_merge(
  116. $attributes,
  117. $this->formatValidationAttribute($input, $field->label(), $column)
  118. );
  119. $messages = array_merge(
  120. $messages,
  121. $this->formatValidationMessages($input, $field->getValidationMessages())
  122. );
  123. }
  124. Arr::forget($rules, NestedForm::REMOVE_FLAG_NAME);
  125. if (empty($rules)) {
  126. return false;
  127. }
  128. $newRules = [];
  129. $newInput = [];
  130. foreach ($rules as $column => $rule) {
  131. foreach (array_keys($input[$this->column]) as $key) {
  132. if ($input[$this->column][$key][NestedForm::REMOVE_FLAG_NAME]) {
  133. continue;
  134. }
  135. $newRules["{$this->column}.$key.$column"] = $rule;
  136. if (isset($input[$this->column][$key][$column]) &&
  137. is_array($input[$this->column][$key][$column])) {
  138. foreach ($input[$this->column][$key][$column] as $vkey => $value) {
  139. $newInput["{$this->column}.$key.{$column}$vkey"] = $value;
  140. }
  141. }
  142. }
  143. }
  144. if (empty($newInput)) {
  145. $newInput = $input;
  146. }
  147. return Validator::make($newInput, $newRules, array_merge($this->getValidationMessages(), $messages), $attributes);
  148. }
  149. /**
  150. * Format validation messages.
  151. *
  152. * @param array $input
  153. * @param array $messages
  154. *
  155. * @return array
  156. */
  157. protected function formatValidationMessages(array $input, array $messages)
  158. {
  159. $result = [];
  160. foreach ($input[$this->column] as $key => &$value) {
  161. $newKey = $this->column.'.'.$key;
  162. foreach ($messages as $k => $message) {
  163. $result[$newKey.'.'.$k] = $message;
  164. }
  165. }
  166. return $result;
  167. }
  168. /**
  169. * Format validation attributes.
  170. *
  171. * @param array $input
  172. * @param string $label
  173. * @param string $column
  174. *
  175. * @return array
  176. */
  177. protected function formatValidationAttribute($input, $label, $column)
  178. {
  179. $new = $attributes = [];
  180. if (is_array($column)) {
  181. foreach ($column as $index => $col) {
  182. $new[$col.$index] = $col;
  183. }
  184. }
  185. foreach (array_keys(Arr::dot($input)) as $key) {
  186. if (is_string($column)) {
  187. if (Str::endsWith($key, ".$column")) {
  188. $attributes[$key] = $label;
  189. }
  190. } else {
  191. foreach ($new as $k => $val) {
  192. if (Str::endsWith($key, ".$k")) {
  193. $attributes[$key] = $label."[$val]";
  194. }
  195. }
  196. }
  197. }
  198. return $attributes;
  199. }
  200. /**
  201. * Reset input key for validation.
  202. *
  203. * @param array $input
  204. * @param array $column $column is the column name array set
  205. *
  206. * @return void.
  207. */
  208. protected function resetInputKey(array &$input, array $column)
  209. {
  210. /**
  211. * flip the column name array set.
  212. *
  213. * for example, for the DateRange, the column like as below
  214. *
  215. * ["start" => "created_at", "end" => "updated_at"]
  216. *
  217. * to:
  218. *
  219. * [ "created_at" => "start", "updated_at" => "end" ]
  220. */
  221. $column = array_flip($column);
  222. /**
  223. * $this->column is the inputs array's node name, default is the relation name.
  224. *
  225. * So... $input[$this->column] is the data of this column's inputs data
  226. *
  227. * in the HasMany relation, has many data/field set, $set is field set in the below
  228. */
  229. foreach ($input[$this->column] as $index => $set) {
  230. /*
  231. * foreach the field set to find the corresponding $column
  232. */
  233. foreach ($set as $name => $value) {
  234. /*
  235. * if doesn't have column name, continue to the next loop
  236. */
  237. if (!array_key_exists($name, $column)) {
  238. continue;
  239. }
  240. /**
  241. * example: $newKey = created_atstart.
  242. *
  243. * Σ( ° △ °|||)︴
  244. *
  245. * I don't know why a form need range input? Only can imagine is for range search....
  246. */
  247. $newKey = $name.$column[$name];
  248. /*
  249. * set new key
  250. */
  251. Arr::set($input, "{$this->column}.$index.$newKey", $value);
  252. /*
  253. * forget the old key and value
  254. */
  255. Arr::forget($input, "{$this->column}.$index.$name");
  256. }
  257. }
  258. }
  259. /**
  260. * Prepare input data for insert or update.
  261. *
  262. * @param array $input
  263. *
  264. * @return array
  265. */
  266. public function prepare($input)
  267. {
  268. $form = $this->buildNestedForm($this->column, $this->builder);
  269. return array_values(
  270. collect($form->setOriginal($this->original, $this->getKeyName())->prepare($input))
  271. ->reject(function ($item) {
  272. return $item[NestedForm::REMOVE_FLAG_NAME] == 1;
  273. })
  274. ->map(function ($item) {
  275. unset($item[NestedForm::REMOVE_FLAG_NAME]);
  276. return $item;
  277. })
  278. ->toArray()
  279. );
  280. }
  281. /**
  282. * Build a Nested form.
  283. *
  284. * @param string $column
  285. * @param \Closure $builder
  286. * @param null $key
  287. *
  288. * @return NestedForm
  289. */
  290. protected function buildNestedForm($column, \Closure $builder, $key = null)
  291. {
  292. $form = new Form\NestedForm($column, $key);
  293. $form->setForm($this->form);
  294. call_user_func($builder, $form);
  295. $form->hidden($this->getKeyName());
  296. $form->hidden(NestedForm::REMOVE_FLAG_NAME)->default(0)->addElementClass(NestedForm::REMOVE_FLAG_CLASS);
  297. return $form;
  298. }
  299. /**
  300. * Get the HasMany relation key name.
  301. *
  302. * @return string
  303. */
  304. protected function getKeyName()
  305. {
  306. if (is_null($this->form)) {
  307. return;
  308. }
  309. return $this->relationKeyName;
  310. }
  311. /**
  312. * @param string $relationKeyName
  313. */
  314. public function setRelationKeyName(?string $relationKeyName)
  315. {
  316. $this->relationKeyName = $relationKeyName;
  317. return $this;
  318. }
  319. /**
  320. * Set view mode.
  321. *
  322. * @param string $mode currently support `tab` mode.
  323. *
  324. * @return $this
  325. *
  326. * @author Edwin Hui
  327. */
  328. public function mode($mode)
  329. {
  330. $this->viewMode = $mode;
  331. return $this;
  332. }
  333. /**
  334. * Use tab mode to showing hasmany field.
  335. *
  336. * @return HasMany
  337. */
  338. public function useTab()
  339. {
  340. return $this->mode('tab');
  341. }
  342. /**
  343. * Use table mode to showing hasmany field.
  344. *
  345. * @return HasMany
  346. */
  347. public function useTable()
  348. {
  349. return $this->mode('table');
  350. }
  351. /**
  352. * Build Nested form for related data.
  353. *
  354. * @throws \Exception
  355. *
  356. * @return array
  357. */
  358. protected function buildRelatedForms()
  359. {
  360. if (is_null($this->form)) {
  361. return [];
  362. }
  363. $forms = [];
  364. /*
  365. * If redirect from `exception` or `validation error` page.
  366. *
  367. * Then get form data from session flash.
  368. *
  369. * Else get data from database.
  370. */
  371. if ($values = old($this->column)) {
  372. foreach ($values as $key => $data) {
  373. if ($data[NestedForm::REMOVE_FLAG_NAME] == 1) {
  374. continue;
  375. }
  376. $forms[$key] = $this->buildNestedForm($this->column, $this->builder, $key)
  377. ->fill($data);
  378. }
  379. } else {
  380. if (is_array($this->value)) {
  381. foreach ($this->value as $data) {
  382. $key = Arr::get($data, $this->getKeyName());
  383. $forms[$key] = $this->buildNestedForm($this->column, $this->builder, $key)
  384. ->fill($data);
  385. }
  386. }
  387. }
  388. return $forms;
  389. }
  390. /**
  391. * Setup script for this field in different view mode.
  392. *
  393. * @param string $script
  394. *
  395. * @return void
  396. */
  397. protected function setupScript($script)
  398. {
  399. $method = 'setupScriptFor'.ucfirst($this->viewMode).'View';
  400. call_user_func([$this, $method], $script);
  401. }
  402. /**
  403. * Setup default template script.
  404. *
  405. * @param string $templateScript
  406. *
  407. * @return void
  408. */
  409. protected function setupScriptForDefaultView($templateScript)
  410. {
  411. $removeClass = NestedForm::REMOVE_FLAG_CLASS;
  412. $defaultKey = NestedForm::DEFAULT_KEY_NAME;
  413. /**
  414. * When add a new sub form, replace all element key in new sub form.
  415. *
  416. * @example comments[new___key__][title] => comments[new_{index}][title]
  417. *
  418. * {count} is increment number of current sub form count.
  419. */
  420. $script = <<<JS
  421. (function () {
  422. var index = 0;
  423. $('#has-many-{$this->column}').on('click', '.add', function () {
  424. var tpl = $('template.{$this->column}-tpl');
  425. index++;
  426. var template = tpl.html().replace(/{$defaultKey}/g, index);
  427. $('.has-many-{$this->column}-forms').append(template);
  428. {$templateScript}
  429. });
  430. $('#has-many-{$this->column}').on('click', '.remove', function () {
  431. $(this).closest('.has-many-{$this->column}-form').hide();
  432. $(this).closest('.has-many-{$this->column}-form').find('.$removeClass').val(1);
  433. });
  434. })();
  435. JS;
  436. Admin::script($script);
  437. }
  438. /**
  439. * Setup tab template script.
  440. *
  441. * @param string $templateScript
  442. *
  443. * @return void
  444. */
  445. protected function setupScriptForTabView($templateScript)
  446. {
  447. $removeClass = NestedForm::REMOVE_FLAG_CLASS;
  448. $defaultKey = NestedForm::DEFAULT_KEY_NAME;
  449. $script = <<<JS
  450. (function () {
  451. $('#has-many-{$this->column} > .nav').off('click', 'i.close-tab').on('click', 'i.close-tab', function(){
  452. var \$navTab = $(this).siblings('a');
  453. var \$pane = $(\$navTab.attr('href'));
  454. if( \$pane.hasClass('new') ){
  455. \$pane.remove();
  456. }else{
  457. \$pane.removeClass('active').find('.$removeClass').val(1);
  458. }
  459. if(\$navTab.closest('li').hasClass('active')){
  460. \$navTab.closest('li').remove();
  461. $('#has-many-{$this->column} > .nav > li:nth-child(1) > a').tab('show');
  462. }else{
  463. \$navTab.closest('li').remove();
  464. }
  465. });
  466. var index = 0;
  467. $('#has-many-{$this->column} > .header').off('click', '.add').on('click', '.add', function(){
  468. index++;
  469. var navTabHtml = $('#has-many-{$this->column} > template.nav-tab-tpl').html().replace(/{$defaultKey}/g, index);
  470. var paneHtml = $('#has-many-{$this->column} > template.pane-tpl').html().replace(/{$defaultKey}/g, index);
  471. $('#has-many-{$this->column} > .nav').append(navTabHtml);
  472. $('#has-many-{$this->column} > .tab-content').append(paneHtml);
  473. $('#has-many-{$this->column} > .nav > li:last-child a').tab('show');
  474. {$templateScript}
  475. });
  476. if ($('.has-error').length) {
  477. $('.has-error').parent('.tab-pane').each(function () {
  478. var tabId = '#'+$(this).attr('id');
  479. $('li a[href="'+tabId+'"] i').removeClass('hide');
  480. });
  481. var first = $('.has-error:first').parent().attr('id');
  482. $('li a[href="#'+first+'"]').tab('show');
  483. }
  484. })();
  485. JS;
  486. Admin::script($script);
  487. }
  488. /**
  489. * Setup default template script.
  490. *
  491. * @param string $templateScript
  492. *
  493. * @return void
  494. */
  495. protected function setupScriptForTableView($templateScript)
  496. {
  497. $removeClass = NestedForm::REMOVE_FLAG_CLASS;
  498. $defaultKey = NestedForm::DEFAULT_KEY_NAME;
  499. /**
  500. * When add a new sub form, replace all element key in new sub form.
  501. *
  502. * @example comments[new___key__][title] => comments[new_{index}][title]
  503. *
  504. * {count} is increment number of current sub form count.
  505. */
  506. $script = <<<JS
  507. (function () {
  508. var index = 0;
  509. $('#has-many-{$this->column}').on('click', '.add', function () {
  510. var tpl = $('template.{$this->column}-tpl');
  511. index++;
  512. var template = tpl.html().replace(/{$defaultKey}/g, index);
  513. $('.has-many-{$this->column}-forms').append(template);
  514. {$templateScript}
  515. });
  516. $('#has-many-{$this->column}').on('click', '.remove', function () {
  517. $(this).closest('.has-many-{$this->column}-form').hide();
  518. $(this).closest('.has-many-{$this->column}-form').find('.$removeClass').val(1);
  519. });
  520. })();
  521. JS;
  522. Admin::script($script);
  523. }
  524. /**
  525. * Disable create button.
  526. *
  527. * @return $this
  528. */
  529. public function disableCreate()
  530. {
  531. $this->options['allowCreate'] = false;
  532. return $this;
  533. }
  534. /**
  535. * Disable delete button.
  536. *
  537. * @return $this
  538. */
  539. public function disableDelete()
  540. {
  541. $this->options['allowDelete'] = false;
  542. return $this;
  543. }
  544. /**
  545. * Render the `HasMany` field.
  546. *
  547. * @throws \Exception
  548. *
  549. * @return \Illuminate\View\View
  550. */
  551. public function render()
  552. {
  553. if ($this->viewMode == 'table') {
  554. return $this->renderTable();
  555. }
  556. // specify a view to render.
  557. $this->view = $this->views[$this->viewMode];
  558. list($template, $script) = $this->buildNestedForm($this->column, $this->builder)
  559. ->getTemplateHtmlAndScript();
  560. $this->setupScript($script);
  561. return parent::render()->with([
  562. 'forms' => $this->buildRelatedForms(),
  563. 'template' => $template,
  564. 'relationName' => $this->relationName,
  565. 'options' => $this->options,
  566. ]);
  567. }
  568. /**
  569. * Render the `HasMany` field for table style.
  570. *
  571. * @throws \Exception
  572. *
  573. * @return mixed
  574. */
  575. protected function renderTable()
  576. {
  577. $headers = [];
  578. $fields = [];
  579. $hidden = [];
  580. $scripts = [];
  581. /* @var Field $field */
  582. foreach ($this->buildNestedForm($this->column, $this->builder)->fields() as $field) {
  583. if (is_a($field, Hidden::class)) {
  584. $hidden[] = $field->render();
  585. } else {
  586. /* Hide label and set field width 100% */
  587. $field->setLabelClass(['hidden']);
  588. $field->setWidth(12, 0);
  589. $fields[] = $field->render();
  590. $headers[] = $field->label();
  591. }
  592. /*
  593. * Get and remove the last script of Admin::$script stack.
  594. */
  595. if ($field->getScript()) {
  596. $scripts[] = array_pop(Admin::$script);
  597. }
  598. }
  599. /* Build row elements */
  600. $template = array_reduce($fields, function ($all, $field) {
  601. $all .= "<td>{$field}</td>";
  602. return $all;
  603. }, '');
  604. /* Build cell with hidden elements */
  605. $template .= '<td class="hidden">'.implode('', $hidden).'</td>';
  606. $this->setupScript(implode("\r\n", $scripts));
  607. // specify a view to render.
  608. $this->view = $this->views[$this->viewMode];
  609. return parent::render()->with([
  610. 'headers' => $headers,
  611. 'forms' => $this->buildRelatedForms(),
  612. 'template' => $template,
  613. 'relationName' => $this->relationName,
  614. 'options' => $this->options,
  615. ]);
  616. }
  617. }