Ei kuvausta

class-select.php 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692
  1. <?php
  2. /**
  3. * Dropdown field.
  4. *
  5. * @since 1.0.0
  6. */
  7. class WPForms_Field_Select extends WPForms_Field {
  8. /**
  9. * Choices JS version.
  10. *
  11. * @since 1.6.3
  12. */
  13. const CHOICES_VERSION = '9.0.1';
  14. /**
  15. * Classic (old) style.
  16. *
  17. * @since 1.6.1
  18. *
  19. * @var string
  20. */
  21. const STYLE_CLASSIC = 'classic';
  22. /**
  23. * Modern style.
  24. *
  25. * @since 1.6.1
  26. *
  27. * @var string
  28. */
  29. const STYLE_MODERN = 'modern';
  30. /**
  31. * Primary class constructor.
  32. *
  33. * @since 1.0.0
  34. */
  35. public function init() {
  36. // Define field type information.
  37. $this->name = esc_html__( 'Dropdown', 'wpforms-lite' );
  38. $this->type = 'select';
  39. $this->icon = 'fa-caret-square-o-down';
  40. $this->order = 70;
  41. $this->defaults = array(
  42. 1 => array(
  43. 'label' => esc_html__( 'First Choice', 'wpforms-lite' ),
  44. 'value' => '',
  45. 'default' => '',
  46. ),
  47. 2 => array(
  48. 'label' => esc_html__( 'Second Choice', 'wpforms-lite' ),
  49. 'value' => '',
  50. 'default' => '',
  51. ),
  52. 3 => array(
  53. 'label' => esc_html__( 'Third Choice', 'wpforms-lite' ),
  54. 'value' => '',
  55. 'default' => '',
  56. ),
  57. );
  58. // Define additional field properties.
  59. add_filter( 'wpforms_field_properties_' . $this->type, array( $this, 'field_properties' ), 5, 3 );
  60. // Form frontend CSS enqueues.
  61. add_action( 'wpforms_frontend_css', array( $this, 'enqueue_frontend_css' ) );
  62. // Form frontend JS enqueues.
  63. add_action( 'wpforms_frontend_js', array( $this, 'enqueue_frontend_js' ) );
  64. }
  65. /**
  66. * Define additional field properties.
  67. *
  68. * @since 1.5.0
  69. *
  70. * @param array $properties Field properties.
  71. * @param array $field Field settings.
  72. * @param array $form_data Form data and settings.
  73. *
  74. * @return array
  75. */
  76. public function field_properties( $properties, $field, $form_data ) {
  77. // Remove primary input.
  78. unset( $properties['inputs']['primary'] );
  79. // Define data.
  80. $form_id = absint( $form_data['id'] );
  81. $field_id = absint( $field['id'] );
  82. $choices = $field['choices'];
  83. $dynamic = wpforms_get_field_dynamic_choices( $field, $form_id, $form_data );
  84. if ( $dynamic ) {
  85. $choices = $dynamic;
  86. $field['show_values'] = true;
  87. }
  88. // Set options container (<select>) properties.
  89. $properties['input_container'] = array(
  90. 'class' => array(),
  91. 'data' => array(),
  92. 'id' => "wpforms-{$form_id}-field_{$field_id}",
  93. 'attr' => array(
  94. 'name' => "wpforms[fields][{$field_id}]",
  95. ),
  96. );
  97. // Set properties.
  98. foreach ( $choices as $key => $choice ) {
  99. // Used for dynamic choices.
  100. $depth = isset( $choice['depth'] ) ? absint( $choice['depth'] ) : 1;
  101. $properties['inputs'][ $key ] = array(
  102. 'container' => array(
  103. 'attr' => array(),
  104. 'class' => array( "choice-{$key}", "depth-{$depth}" ),
  105. 'data' => array(),
  106. 'id' => '',
  107. ),
  108. 'label' => array(
  109. 'attr' => array(
  110. 'for' => "wpforms-{$form_id}-field_{$field_id}_{$key}",
  111. ),
  112. 'class' => array( 'wpforms-field-label-inline' ),
  113. 'data' => array(),
  114. 'id' => '',
  115. 'text' => $choice['label'],
  116. ),
  117. 'attr' => array(
  118. 'name' => "wpforms[fields][{$field_id}]",
  119. 'value' => isset( $field['show_values'] ) ? $choice['value'] : $choice['label'],
  120. ),
  121. 'class' => array(),
  122. 'data' => array(),
  123. 'id' => "wpforms-{$form_id}-field_{$field_id}_{$key}",
  124. 'required' => ! empty( $field['required'] ) ? 'required' : '',
  125. 'default' => isset( $choice['default'] ),
  126. );
  127. }
  128. // Add class that changes the field size.
  129. if ( ! empty( $field['size'] ) ) {
  130. $properties['input_container']['class'][] = 'wpforms-field-' . esc_attr( $field['size'] );
  131. }
  132. // Required class for pagebreak validation.
  133. if ( ! empty( $field['required'] ) ) {
  134. $properties['input_container']['class'][] = 'wpforms-field-required';
  135. }
  136. // Add additional class for container.
  137. if (
  138. ! empty( $field['style'] ) &&
  139. in_array( $field['style'], array( self::STYLE_CLASSIC, self::STYLE_MODERN ), true )
  140. ) {
  141. $properties['container']['class'][] = "wpforms-field-select-style-{$field['style']}";
  142. }
  143. return $properties;
  144. }
  145. /**
  146. * Field options panel inside the builder.
  147. *
  148. * @since 1.0.0
  149. *
  150. * @param array $field Field settings.
  151. */
  152. public function field_options( $field ) {
  153. /*
  154. * Basic field options.
  155. */
  156. // Options open markup.
  157. $this->field_option(
  158. 'basic-options',
  159. $field,
  160. array(
  161. 'markup' => 'open',
  162. )
  163. );
  164. // Label.
  165. $this->field_option( 'label', $field );
  166. // Choices.
  167. $this->field_option( 'choices', $field );
  168. // Description.
  169. $this->field_option( 'description', $field );
  170. // Required toggle.
  171. $this->field_option( 'required', $field );
  172. // Options close markup.
  173. $this->field_option(
  174. 'basic-options',
  175. $field,
  176. array(
  177. 'markup' => 'close',
  178. )
  179. );
  180. /*
  181. * Advanced field options.
  182. */
  183. // Options open markup.
  184. $this->field_option(
  185. 'advanced-options',
  186. $field,
  187. array(
  188. 'markup' => 'open',
  189. )
  190. );
  191. // Show Values toggle option. This option will only show if already used
  192. // or if manually enabled by a filter.
  193. if ( ! empty( $field['show_values'] ) || wpforms_show_fields_options_setting() ) {
  194. $show_values = $this->field_element(
  195. 'toggle',
  196. $field,
  197. array(
  198. 'slug' => 'show_values',
  199. 'value' => isset( $field['show_values'] ) ? $field['show_values'] : '0',
  200. 'desc' => esc_html__( 'Show Values', 'wpforms-lite' ),
  201. 'tooltip' => esc_html__( 'Check this option to manually set form field values.', 'wpforms-lite' ),
  202. ),
  203. false
  204. );
  205. $this->field_element(
  206. 'row',
  207. $field,
  208. array(
  209. 'slug' => 'show_values',
  210. 'content' => $show_values,
  211. )
  212. );
  213. }
  214. // Multiple options selection.
  215. $fld = $this->field_element(
  216. 'toggle',
  217. $field,
  218. array(
  219. 'slug' => 'multiple',
  220. 'value' => ! empty( $field['multiple'] ),
  221. 'desc' => esc_html__( 'Multiple Options Selection', 'wpforms-lite' ),
  222. 'tooltip' => esc_html__( 'Allow users to select multiple choices in this field.', 'wpforms-lite' ) . '<br>' .
  223. sprintf(
  224. wp_kses( /* translators: %s - URL to WPForms.com doc article. */
  225. esc_html__( 'For details, including how this looks and works for your site\'s visitors, please check out <a href="%s" target="_blank" rel="noopener noreferrer">our doc</a>. ', 'wpforms-lite' ),
  226. [
  227. 'a' => [
  228. 'href' => [],
  229. 'target' => [],
  230. 'rel' => [],
  231. ],
  232. ]
  233. ),
  234. 'https://wpforms.com/docs/how-to-allow-multiple-selections-to-a-dropdown-field-in-wpforms/'
  235. ),
  236. ),
  237. false
  238. );
  239. $this->field_element(
  240. 'row',
  241. $field,
  242. array(
  243. 'slug' => 'multiple',
  244. 'content' => $fld,
  245. )
  246. );
  247. // Style.
  248. $lbl = $this->field_element(
  249. 'label',
  250. $field,
  251. array(
  252. 'slug' => 'style',
  253. 'value' => esc_html__( 'Style', 'wpforms-lite' ),
  254. 'tooltip' => esc_html__( 'Classic style is the default one generated by your browser. Modern has a fresh look and displays all selected options in a single row.', 'wpforms-lite' ),
  255. ),
  256. false
  257. );
  258. $fld = $this->field_element(
  259. 'select',
  260. $field,
  261. array(
  262. 'slug' => 'style',
  263. 'value' => ! empty( $field['style'] ) ? $field['style'] : self::STYLE_CLASSIC,
  264. 'options' => array(
  265. self::STYLE_CLASSIC => esc_html__( 'Classic', 'wpforms-lite' ),
  266. self::STYLE_MODERN => esc_html__( 'Modern', 'wpforms-lite' ),
  267. ),
  268. ),
  269. false
  270. );
  271. $this->field_element(
  272. 'row',
  273. $field,
  274. array(
  275. 'slug' => 'style',
  276. 'content' => $lbl . $fld,
  277. )
  278. );
  279. // Size.
  280. $this->field_option( 'size', $field );
  281. // Placeholder.
  282. $this->field_option( 'placeholder', $field );
  283. // Dynamic choice auto-populating toggle.
  284. $this->field_option( 'dynamic_choices', $field );
  285. // Dynamic choice source.
  286. $this->field_option( 'dynamic_choices_source', $field );
  287. // Custom CSS classes.
  288. $this->field_option( 'css', $field );
  289. // Hide label.
  290. $this->field_option( 'label_hide', $field );
  291. // Options close markup.
  292. $this->field_option(
  293. 'advanced-options',
  294. $field,
  295. [
  296. 'markup' => 'close',
  297. ]
  298. );
  299. }
  300. /**
  301. * Field preview inside the builder.
  302. *
  303. * @since 1.0.0
  304. * @since 1.6.1 Added a `Modern` style select support.
  305. *
  306. * @param array $field Field settings.
  307. */
  308. public function field_preview( $field ) {
  309. $args = array();
  310. // Label.
  311. $this->field_preview_option( 'label', $field );
  312. // Prepare arguments.
  313. $args['modern'] = false;
  314. if (
  315. ! empty( $field['style'] ) &&
  316. self::STYLE_MODERN === $field['style']
  317. ) {
  318. $args['modern'] = true;
  319. $args['class'] = 'choicesjs-select';
  320. }
  321. // Choices.
  322. $this->field_preview_option( 'choices', $field, $args );
  323. // Description.
  324. $this->field_preview_option( 'description', $field );
  325. }
  326. /**
  327. * Field display on the form front-end.
  328. *
  329. * @since 1.0.0
  330. * @since 1.5.0 Converted to a new format, where all the data are taken not from $deprecated, but field properties.
  331. * @since 1.6.1 Added a multiple select support.
  332. *
  333. * @param array $field Field data and settings.
  334. * @param array $deprecated Deprecated array of field attributes.
  335. * @param array $form_data Form data and settings.
  336. */
  337. public function field_display( $field, $deprecated, $form_data ) {
  338. $container = $field['properties']['input_container'];
  339. $field_placeholder = ! empty( $field['placeholder'] ) ? $field['placeholder'] : '';
  340. $is_multiple = ! empty( $field['multiple'] );
  341. $is_modern = ! empty( $field['style'] ) && self::STYLE_MODERN === $field['style'];
  342. $choices = $field['properties']['inputs'];
  343. if ( ! empty( $field['required'] ) ) {
  344. $container['attr']['required'] = 'required';
  345. }
  346. // If it's a multiple select.
  347. if ( $is_multiple ) {
  348. $container['attr']['multiple'] = 'multiple';
  349. // Change a name attribute.
  350. if ( ! empty( $container['attr']['name'] ) ) {
  351. $container['attr']['name'] .= '[]';
  352. }
  353. }
  354. // Add a class for Choices.js initialization.
  355. if ( $is_modern ) {
  356. $container['class'][] = 'choicesjs-select';
  357. // Add a size-class to data attribute - it is used when Choices.js is initialized.
  358. if ( ! empty( $field['size'] ) ) {
  359. $container['data']['size-class'] = 'wpforms-field-row wpforms-field-' . sanitize_html_class( $field['size'] );
  360. }
  361. $container['data']['search-enabled'] = $this->is_choicesjs_search_enabled( count( $choices ) );
  362. }
  363. $has_default = false;
  364. // Check to see if any of the options were selected by default.
  365. foreach ( $choices as $choice ) {
  366. if ( ! empty( $choice['default'] ) ) {
  367. $has_default = true;
  368. break;
  369. }
  370. }
  371. // Fake placeholder for Modern style.
  372. if ( $is_modern && empty( $field_placeholder ) ) {
  373. $first_choices = reset( $choices );
  374. $field_placeholder = $first_choices['label']['text'];
  375. }
  376. // Preselect default if no other choices were marked as default.
  377. printf(
  378. '<select %s>',
  379. wpforms_html_attributes( $container['id'], $container['class'], $container['data'], $container['attr'] )
  380. );
  381. // Optional placeholder.
  382. if ( ! empty( $field_placeholder ) ) {
  383. printf(
  384. '<option value="" class="placeholder" disabled %s>%s</option>',
  385. selected( false, $has_default || $is_multiple, false ),
  386. esc_html( $field_placeholder )
  387. );
  388. }
  389. // Build the select options.
  390. foreach ( $choices as $key => $choice ) {
  391. printf(
  392. '<option value="%s" %s>%s</option>',
  393. esc_attr( $choice['attr']['value'] ),
  394. selected( true, ! empty( $choice['default'] ), false ),
  395. esc_html( $choice['label']['text'] )
  396. );
  397. }
  398. echo '</select>';
  399. }
  400. /**
  401. * Format and sanitize field.
  402. *
  403. * @since 1.0.2
  404. * @since 1.6.1 Added a support for multiple values.
  405. *
  406. * @param int $field_id Field ID.
  407. * @param string|array $field_submit Submitted field value (selected option).
  408. * @param array $form_data Form data and settings.
  409. */
  410. public function format( $field_id, $field_submit, $form_data ) {
  411. $field = $form_data['fields'][ $field_id ];
  412. $dynamic = ! empty( $field['dynamic_choices'] ) ? $field['dynamic_choices'] : false;
  413. $multiple = ! empty( $field['multiple'] );
  414. $name = sanitize_text_field( $field['label'] );
  415. $value = [];
  416. // Convert submitted field value to array.
  417. if ( ! is_array( $field_submit ) ) {
  418. $field_submit = array( $field_submit );
  419. }
  420. $value_raw = wpforms_sanitize_array_combine( $field_submit );
  421. $data = array(
  422. 'name' => $name,
  423. 'value' => '',
  424. 'value_raw' => $value_raw,
  425. 'id' => absint( $field_id ),
  426. 'type' => $this->type,
  427. );
  428. if ( 'post_type' === $dynamic && ! empty( $field['dynamic_post_type'] ) ) {
  429. // Dynamic population is enabled using post type (like for a `Checkboxes` field).
  430. $value_raw = implode( ',', array_map( 'absint', $field_submit ) );
  431. $data['value_raw'] = $value_raw;
  432. $data['dynamic'] = 'post_type';
  433. $data['dynamic_items'] = $value_raw;
  434. $data['dynamic_post_type'] = $field['dynamic_post_type'];
  435. $posts = array();
  436. foreach ( $field_submit as $id ) {
  437. $post = get_post( $id );
  438. if ( ! is_wp_error( $post ) && ! empty( $post ) && $data['dynamic_post_type'] === $post->post_type ) {
  439. $posts[] = esc_html( $post->post_title );
  440. }
  441. }
  442. $data['value'] = ! empty( $posts ) ? wpforms_sanitize_array_combine( $posts ) : '';
  443. } elseif ( 'taxonomy' === $dynamic && ! empty( $field['dynamic_taxonomy'] ) ) {
  444. // Dynamic population is enabled using taxonomy (like for a `Checkboxes` field).
  445. $value_raw = implode( ',', array_map( 'absint', $field_submit ) );
  446. $data['value_raw'] = $value_raw;
  447. $data['dynamic'] = 'taxonomy';
  448. $data['dynamic_items'] = $value_raw;
  449. $data['dynamic_taxonomy'] = $field['dynamic_taxonomy'];
  450. $terms = array();
  451. foreach ( $field_submit as $id ) {
  452. $term = get_term( $id, $field['dynamic_taxonomy'] );
  453. if ( ! is_wp_error( $term ) && ! empty( $term ) ) {
  454. $terms[] = esc_html( $term->name );
  455. }
  456. }
  457. $data['value'] = ! empty( $terms ) ? wpforms_sanitize_array_combine( $terms ) : '';
  458. } else {
  459. // Normal processing, dynamic population is off.
  460. // If show_values is true, that means values posted are the raw values
  461. // and not the labels. So we need to get the label values.
  462. if ( ! empty( $field['show_values'] ) && (int) $field['show_values'] === 1 ) {
  463. foreach ( $field_submit as $item ) {
  464. foreach ( $field['choices'] as $choice ) {
  465. if ( $item === $choice['value'] ) {
  466. $value[] = $choice['label'];
  467. break;
  468. }
  469. }
  470. }
  471. $data['value'] = ! empty( $value ) ? wpforms_sanitize_array_combine( $value ) : '';
  472. } else {
  473. $data['value'] = $value_raw;
  474. }
  475. }
  476. // Backward compatibility: for single dropdown save a string, for multiple - array.
  477. if ( ! $multiple && is_array( $data ) && ( 1 === count( $data ) ) ) {
  478. $data = reset( $data );
  479. }
  480. // Push field details to be saved.
  481. wpforms()->process->fields[ $field_id ] = $data;
  482. }
  483. /**
  484. * Form frontend CSS enqueues.
  485. *
  486. * @since 1.6.1
  487. *
  488. * @param array $forms Forms on the current page.
  489. */
  490. public function enqueue_frontend_css( $forms ) {
  491. $has_modern_select = false;
  492. foreach ( $forms as $form ) {
  493. if ( $this->is_field_style( $form, self::STYLE_MODERN ) ) {
  494. $has_modern_select = true;
  495. break;
  496. }
  497. }
  498. if ( $has_modern_select || wpforms()->frontend->assets_global() ) {
  499. $min = \wpforms_get_min_suffix();
  500. wp_enqueue_style(
  501. 'wpforms-choicesjs',
  502. WPFORMS_PLUGIN_URL . "assets/css/choices{$min}.css",
  503. array(),
  504. self::CHOICES_VERSION
  505. );
  506. }
  507. }
  508. /**
  509. * Form frontend JS enqueues.
  510. *
  511. * @since 1.6.1
  512. *
  513. * @param array $forms Forms on the current page.
  514. */
  515. public function enqueue_frontend_js( $forms ) {
  516. $has_modern_select = false;
  517. foreach ( $forms as $form ) {
  518. if ( $this->is_field_style( $form, self::STYLE_MODERN ) ) {
  519. $has_modern_select = true;
  520. break;
  521. }
  522. }
  523. if ( $has_modern_select || wpforms()->frontend->assets_global() ) {
  524. $this->enqueue_choicesjs_once( $forms );
  525. }
  526. }
  527. /**
  528. * Whether the provided form has a dropdown field with a specified style.
  529. *
  530. * @since 1.6.1
  531. *
  532. * @param array $form Form data.
  533. * @param string $style Desired field style.
  534. *
  535. * @return bool
  536. */
  537. protected function is_field_style( $form, $style ) {
  538. $is_field_style = false;
  539. if ( empty( $form['fields'] ) ) {
  540. return $is_field_style;
  541. }
  542. foreach ( (array) $form['fields'] as $field ) {
  543. if (
  544. ! empty( $field['type'] ) &&
  545. $field['type'] === $this->type &&
  546. ! empty( $field['style'] ) &&
  547. sanitize_key( $style ) === $field['style']
  548. ) {
  549. $is_field_style = true;
  550. break;
  551. }
  552. }
  553. return $is_field_style;
  554. }
  555. /**
  556. * Get field name for ajax error message.
  557. *
  558. * @since 1.6.3
  559. *
  560. * @param string $name Field name for error triggered.
  561. * @param array $field Field settings.
  562. * @param array $props List of properties.
  563. * @param string $error Error message.
  564. *
  565. * @return string
  566. */
  567. public function ajax_error_field_name( $name, $field, $props, $error ) {
  568. if ( ! isset( $field['type'] ) || 'select' !== $field['type'] ) {
  569. return $name;
  570. }
  571. if ( ! empty( $field['multiple'] ) ) {
  572. $input = isset( $props['inputs'] ) ? end( $props['inputs'] ) : [];
  573. return isset( $input['attr']['name'] ) ? $input['attr']['name'] . '[]' : '';
  574. }
  575. return $name;
  576. }
  577. }
  578. new WPForms_Field_Select();