CanCascadeFields.php 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. <?php
  2. namespace Dcat\Admin\Form\Field;
  3. use Dcat\Admin\Admin;
  4. use Dcat\Admin\Exception\RuntimeException;
  5. use Dcat\Admin\Form;
  6. use Illuminate\Support\Arr;
  7. /**
  8. * @property Form $form
  9. */
  10. trait CanCascadeFields
  11. {
  12. /**
  13. * @var array
  14. */
  15. protected $conditions = [];
  16. /**
  17. * @var array
  18. */
  19. protected $cascadeGroups = [];
  20. /**
  21. * @param $operator
  22. * @param $value
  23. * @param $closure
  24. * @return $this
  25. */
  26. public function when($operator, $value, $closure = null)
  27. {
  28. if (func_num_args() == 2) {
  29. $closure = $value;
  30. $value = $operator;
  31. $operator = $this->getDefaultOperator();
  32. }
  33. $this->formatValues($operator, $value);
  34. $this->addDependents($operator, $value, $closure);
  35. return $this;
  36. }
  37. protected function getDefaultOperator()
  38. {
  39. if ($this instanceof MultipleSelect || $this instanceof Checkbox) {
  40. return 'in';
  41. }
  42. return '=';
  43. }
  44. /**
  45. * @param string $operator
  46. * @param mixed $value
  47. */
  48. protected function formatValues(string $operator, &$value)
  49. {
  50. if (in_array($operator, ['in', 'notIn'])) {
  51. $value = Arr::wrap($value);
  52. }
  53. if (is_array($value)) {
  54. $value = array_map('strval', $value);
  55. } else {
  56. $value = strval($value);
  57. }
  58. }
  59. /**
  60. * @param string $operator
  61. * @param mixed $value
  62. * @param \Closure $closure
  63. */
  64. protected function addDependents(string $operator, $value, \Closure $closure)
  65. {
  66. $this->conditions[] = compact('operator', 'value', 'closure');
  67. ($this->parent ?: $this->form)->cascadeGroup($closure, [
  68. 'column' => $this->column(),
  69. 'index' => count($this->conditions) - 1,
  70. 'class' => $this->getCascadeClass($value, $operator),
  71. ]);
  72. }
  73. /**
  74. * @param mixed $value
  75. * @return string
  76. */
  77. protected function getCascadeClass($value, string $operator)
  78. {
  79. if (is_array($value)) {
  80. $value = implode('-', $value);
  81. }
  82. $map = [
  83. '=' => '0',
  84. '>' => '1',
  85. '<' => '2',
  86. '!=' => '3',
  87. 'in' => '4',
  88. 'notIn' => '5',
  89. '>=' => '6',
  90. '<=' => '7',
  91. 'has' => '8',
  92. ];
  93. return str_replace(
  94. '.',
  95. '',
  96. sprintf('cascade-%s-%s-%s', str_replace(' ', '-', $this->getElementClassString()), $value, $map[$operator])
  97. );
  98. }
  99. protected function addCascadeScript()
  100. {
  101. if (! $script = $this->getCascadeScript()) {
  102. return;
  103. }
  104. Admin::script(
  105. <<<JS
  106. Dcat.init('{$this->getElementClassSelector()}', function (\$this) {
  107. {$script}
  108. });
  109. JS
  110. );
  111. }
  112. /**
  113. * Add cascade scripts to contents.
  114. *
  115. * @return string
  116. */
  117. protected function getCascadeScript()
  118. {
  119. if (empty($this->conditions)) {
  120. return;
  121. }
  122. $cascadeGroups = collect($this->conditions)->map(function ($condition) {
  123. return [
  124. 'class' => $this->getCascadeClass($condition['value'], $condition['operator']),
  125. 'operator' => $condition['operator'],
  126. 'value' => $condition['value'],
  127. ];
  128. })->toJson();
  129. return <<<JS
  130. (function () {
  131. var compare = function (a, b, o) {
  132. if (! $.isArray(b)) {
  133. return operator_table[o](a, b)
  134. }
  135. if (o === '!=') {
  136. var result = true;
  137. for (var i in b) {
  138. if (! operator_table[o](a, b[i])) {
  139. result = false;
  140. break;
  141. }
  142. }
  143. return result;
  144. }
  145. for (var i in b) {
  146. if (operator_table[o](a, b[i])) {
  147. return true;
  148. }
  149. }
  150. };
  151. var operator_table = {
  152. '=': function(a, b) {
  153. if ($.isArray(a) && $.isArray(b)) {
  154. return $(a).not(b).length === 0 && $(b).not(a).length === 0;
  155. }
  156. return String(a) === String(b);
  157. },
  158. '>': function(a, b) {
  159. return a > b;
  160. },
  161. '<': function(a, b) {
  162. return a < b;
  163. },
  164. '>=': function(a, b) { return a >= b; },
  165. '<=': function(a, b) { return a <= b; },
  166. '!=': function(a, b) {
  167. return ! operator_table['='](a, b);
  168. },
  169. 'in': function(a, b) { return Dcat.helpers.inObject(a, String(b), true); },
  170. 'notIn': function(a, b) { return ! Dcat.helpers.inObject(a, String(b), true); },
  171. 'has': function(a, b) { return Dcat.helpers.inObject(b, String(b), true); },
  172. };
  173. var cascade_groups = {$cascadeGroups}, event = '{$this->cascadeEvent}';
  174. \$this.on(event, function (e) {
  175. {$this->getFormFrontValue()}
  176. let parent = \$this.closest('.fields-group');
  177. if (parent.length === 0){
  178. parent = \$this.closest('form');
  179. }
  180. cascade_groups.forEach(function (event) {
  181. var group = parent.find('div.cascade-group.'+event.class);
  182. if (compare(checked, event.value, event.operator)) {
  183. group.removeClass('d-none');
  184. } else {
  185. group.addClass('d-none');
  186. }
  187. });
  188. }).trigger(event);
  189. })();
  190. JS;
  191. }
  192. /**
  193. * @return string
  194. */
  195. protected function getFormFrontValue()
  196. {
  197. switch (get_class($this)) {
  198. case Select::class:
  199. case MultipleSelect::class:
  200. return 'var checked = $(this).val();';
  201. case Radio::class:
  202. return <<<JS
  203. var checked = \$(this).closest('.form-group').find(':checked').val();
  204. JS;
  205. case Checkbox::class:
  206. return <<<JS
  207. var checked = \$this.closest('.form-group').find(':checked').map(function(){
  208. return $(this).val();
  209. }).get();
  210. JS;
  211. default:
  212. throw new RuntimeException('Invalid form field type');
  213. }
  214. }
  215. }