CanCascadeFields.php 5.9 KB

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