Select.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. <?php
  2. namespace Dcat\Admin\Form\Field;
  3. use Dcat\Admin\Admin;
  4. use Dcat\Admin\Form\Field;
  5. use Dcat\Admin\Support\Helper;
  6. use Illuminate\Database\Eloquent\Model;
  7. use Illuminate\Support\Arr;
  8. use Illuminate\Support\Str;
  9. class Select extends Field
  10. {
  11. use CanCascadeFields;
  12. public static $js = '@select2';
  13. public static $css = '@select2';
  14. protected $cascadeEvent = 'change';
  15. /**
  16. * @var array
  17. */
  18. protected $groups = [];
  19. /**
  20. * @var array
  21. */
  22. protected $config = [];
  23. /**
  24. * Set options.
  25. *
  26. * @param array|\Closure|string $options
  27. *
  28. * @return $this|mixed
  29. */
  30. public function options($options = [])
  31. {
  32. if ($options instanceof \Closure) {
  33. $this->options = $options;
  34. return $this;
  35. }
  36. // remote options
  37. if (is_string($options)) {
  38. // reload selected
  39. if (class_exists($options) && in_array(Model::class, class_parents($options))) {
  40. return $this->model(...func_get_args());
  41. }
  42. return $this->loadRemoteOptions(...func_get_args());
  43. }
  44. $this->options = Helper::array($options);
  45. return $this;
  46. }
  47. /**
  48. * @param array $groups
  49. */
  50. /**
  51. * Set option groups.
  52. *
  53. * eg: $group = [
  54. * [
  55. * 'label' => 'xxxx',
  56. * 'options' => [
  57. * 1 => 'foo',
  58. * 2 => 'bar',
  59. * ...
  60. * ],
  61. * ...
  62. * ]
  63. *
  64. * @param array $groups
  65. *
  66. * @return $this
  67. */
  68. public function groups(array $groups)
  69. {
  70. $this->groups = $groups;
  71. return $this;
  72. }
  73. /**
  74. * Load options for other select on change.
  75. *
  76. * @param string $field
  77. * @param string $sourceUrl
  78. * @param string $idField
  79. * @param string $textField
  80. *
  81. * @return $this
  82. */
  83. public function load($field, $sourceUrl, string $idField = 'id', string $textField = 'text')
  84. {
  85. if (Str::contains($field, '.')) {
  86. $field = $this->formatName($field);
  87. $class = static::FIELD_CLASS_PREFIX.str_replace(['[', ']'], '_', $field);
  88. } else {
  89. $class = static::FIELD_CLASS_PREFIX.$field;
  90. }
  91. $sourceUrl = admin_url($sourceUrl);
  92. $script = <<<JS
  93. $(document).off('change', "{$this->getElementClassSelector()}");
  94. $(document).on('change', "{$this->getElementClassSelector()}", function () {
  95. var target = $(this).closest('.fields-group').find(".$class");
  96. if (String(this.value) !== '0' && ! this.value) {
  97. return;
  98. }
  99. $.ajax("$sourceUrl?q="+this.value).then(function (data) {
  100. target.find("option").remove();
  101. $(target).select2({
  102. data: $.map(data, function (d) {
  103. d.id = d.$idField;
  104. d.text = d.$textField;
  105. return d;
  106. })
  107. }).val(target.attr('data-value').split(',')).trigger('change');
  108. });
  109. });
  110. $("{$this->getElementClassSelector()}").trigger('change');
  111. JS;
  112. Admin::script($script);
  113. return $this;
  114. }
  115. /**
  116. * Load options for other selects on change.
  117. *
  118. * @param string $fields
  119. * @param string $sourceUrls
  120. * @param string $idField
  121. * @param string $textField
  122. *
  123. * @return $this
  124. */
  125. public function loads($fields = [], $sourceUrls = [], string $idField = 'id', string $textField = 'text')
  126. {
  127. $fieldsStr = implode('^', array_map(function ($field) {
  128. if (Str::contains($field, '.')) {
  129. return static::FIELD_CLASS_PREFIX.str_replace('.', '_', $field).'_';
  130. }
  131. return static::FIELD_CLASS_PREFIX.$field;
  132. }, (array) $fields));
  133. $urlsStr = implode('^', array_map(function ($url) {
  134. return admin_url($url);
  135. }, (array) $sourceUrls));
  136. $script = <<<JS
  137. (function () {
  138. var fields = '$fieldsStr'.split('^');
  139. var urls = '$urlsStr'.split('^');
  140. var refreshOptions = function(url, target) {
  141. $.ajax(url).then(function(data) {
  142. target.find("option").remove();
  143. $(target).select2({
  144. data: $.map(data, function (d) {
  145. d.id = d.$idField;
  146. d.text = d.$textField;
  147. return d;
  148. })
  149. }).val(target.data('value').split(',')).trigger('change');
  150. });
  151. };
  152. $(document).off('change', "{$this->getElementClassSelector()}");
  153. $(document).on('change', "{$this->getElementClassSelector()}", function () {
  154. var _this = this;
  155. var promises = [];
  156. fields.forEach(function(field, index){
  157. var target = $(_this).closest('.fields-group').find('.' + fields[index]);
  158. if (_this.value !== '0' && ! _this.value) {
  159. return;
  160. }
  161. promises.push(refreshOptions(urls[index] + "?q="+ _this.value, target));
  162. });
  163. $.when(promises).then(function() {});
  164. });
  165. $("{$this->getElementClassSelector()}").trigger('change');
  166. })()
  167. JS;
  168. Admin::script($script);
  169. return $this;
  170. }
  171. /**
  172. * Load options from current selected resource(s).
  173. *
  174. * @param string $model
  175. * @param string $idField
  176. * @param string $textField
  177. *
  178. * @return $this
  179. */
  180. public function model($model, string $idField = 'id', string $textField = 'name')
  181. {
  182. if (! class_exists($model)
  183. || ! in_array(Model::class, class_parents($model))
  184. ) {
  185. throw new \InvalidArgumentException("[$model] must be a valid model class");
  186. }
  187. $this->options = function ($value) use ($model, $idField, $textField) {
  188. if (empty($value)) {
  189. return [];
  190. }
  191. $resources = [];
  192. if (is_array($value)) {
  193. if (Arr::isAssoc($value)) {
  194. $resources[] = Arr::get($value, $idField);
  195. } else {
  196. $resources = array_column($value, $idField);
  197. }
  198. } else {
  199. $resources[] = $value;
  200. }
  201. return $model::find($resources)->pluck($textField, $idField)->toArray();
  202. };
  203. return $this;
  204. }
  205. /**
  206. * Load options from remote.
  207. *
  208. * @param string $url
  209. * @param array $parameters
  210. * @param array $options
  211. *
  212. * @return $this
  213. */
  214. protected function loadRemoteOptions(string $url, array $parameters = [], array $options = [])
  215. {
  216. $ajaxOptions = [
  217. 'url' => admin_url($url.'?'.http_build_query($parameters)),
  218. ];
  219. $configs = array_merge([
  220. 'allowClear' => true,
  221. 'placeholder' => [
  222. 'id' => '',
  223. 'text' => $this->placeholder(),
  224. ],
  225. ], $this->config);
  226. $configs = json_encode($configs);
  227. $configs = substr($configs, 1, strlen($configs) - 2);
  228. $ajaxOptions = json_encode(array_merge($ajaxOptions, $options));
  229. $this->script = <<<JS
  230. $.ajax({$ajaxOptions}).done(function(data) {
  231. $("{$this->getElementClassSelector()}").each(function (_, select) {
  232. select = $(select);
  233. select.select2({
  234. data: data,
  235. $configs
  236. });
  237. var value = select.data('value') + '';
  238. if (value) {
  239. select.val(value.split(',')).trigger("change")
  240. }
  241. });
  242. });
  243. JS;
  244. return $this;
  245. }
  246. /**
  247. * Load options from ajax results.
  248. *
  249. * @param string $url
  250. * @param $idField
  251. * @param $textField
  252. *
  253. * @return $this
  254. */
  255. public function ajax(string $url, string $idField = 'id', string $textField = 'text')
  256. {
  257. $configs = array_merge([
  258. 'allowClear' => true,
  259. 'placeholder' => $this->placeholder(),
  260. 'minimumInputLength' => 1,
  261. ], $this->config);
  262. $configs = json_encode($configs);
  263. $configs = substr($configs, 1, strlen($configs) - 2);
  264. $url = admin_url($url);
  265. $this->script = <<<JS
  266. $("{$this->getElementClassSelector()}").select2({
  267. ajax: {
  268. url: "$url",
  269. dataType: 'json',
  270. delay: 250,
  271. data: function (params) {
  272. return {
  273. q: params.term,
  274. page: params.page
  275. };
  276. },
  277. processResults: function (data, params) {
  278. params.page = params.page || 1;
  279. return {
  280. results: $.map(data.data, function (d) {
  281. d.id = d.$idField;
  282. d.text = d.$textField;
  283. return d;
  284. }),
  285. pagination: {
  286. more: data.next_page_url
  287. }
  288. };
  289. },
  290. cache: true
  291. },
  292. $configs,
  293. escapeMarkup: function (markup) {
  294. return markup;
  295. }
  296. });
  297. JS;
  298. return $this;
  299. }
  300. /**
  301. * Set config for select2.
  302. *
  303. * all configurations see https://select2.org/configuration/options-api
  304. *
  305. * @param string $key
  306. * @param mixed $val
  307. *
  308. * @return $this
  309. */
  310. public function config(string $key, $val)
  311. {
  312. $this->config[$key] = $val;
  313. return $this;
  314. }
  315. /**
  316. * Disable clear button.
  317. *
  318. * @return $this
  319. */
  320. public function disableClearButton()
  321. {
  322. return $this->config('allowClear', false);
  323. }
  324. /**
  325. * {@inheritdoc}
  326. */
  327. public function render()
  328. {
  329. static::defineLang();
  330. $configs = array_merge([
  331. 'allowClear' => true,
  332. 'placeholder' => [
  333. 'id' => '',
  334. 'text' => $this->placeholder(),
  335. ],
  336. ], $this->config);
  337. $configs = json_encode($configs);
  338. if (empty($this->script)) {
  339. $this->script = "$(\"{$this->getElementClassSelector()}\").select2($configs);";
  340. }
  341. $this->addCascadeScript();
  342. if ($this->options instanceof \Closure) {
  343. $this->options = $this->options->bindTo($this->values());
  344. $this->options(call_user_func($this->options, $this->value(), $this));
  345. }
  346. $this->options = array_filter($this->options, 'strlen');
  347. $this->addVariables([
  348. 'options' => $this->options,
  349. 'groups' => $this->groups,
  350. ]);
  351. $this->attribute('data-value', implode(',', Helper::array($this->value())));
  352. return parent::render();
  353. }
  354. /**
  355. * {@inheritdoc}
  356. */
  357. public function placeholder($placeholder = null)
  358. {
  359. if ($placeholder === null) {
  360. return $this->placeholder ?: $this->label;
  361. }
  362. $this->placeholder = $placeholder;
  363. return $this;
  364. }
  365. /**
  366. * @return void
  367. */
  368. public static function defineLang()
  369. {
  370. $lang = trans('select2');
  371. if (! is_array($lang) || empty($lang)) {
  372. return;
  373. }
  374. $locale = config('app.locale');
  375. Admin::script(
  376. <<<JS
  377. (function () {
  378. if (! $.fn.select2) {
  379. return;
  380. }
  381. var e = $.fn.select2.amd;
  382. return e.define("select2/i18n/{$locale}", [], function () {
  383. return {
  384. errorLoading: function () {
  385. return "{$lang['error_loading']}"
  386. }, inputTooLong: function (e) {
  387. return "{$lang['input_too_long']}".replace(':num', e.input.length - e.maximum)
  388. }, inputTooShort: function (e) {
  389. return "{$lang['input_too_short']}".replace(':num', e.minimum - e.input.length)
  390. }, loadingMore: function () {
  391. return "{$lang['loading_more']}"
  392. }, maximumSelected: function (e) {
  393. return "{$lang['maximum_selected']}".replace(':num', e.maximum)
  394. }, noResults: function () {
  395. return "{$lang['no_results']}"
  396. }, searching: function () {
  397. return "{$lang['searching']}"
  398. }
  399. }
  400. }), {define: e.define, require: e.require}
  401. })()
  402. JS
  403. );
  404. }
  405. }