No Description

class-text.php 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505
  1. <?php
  2. /**
  3. * Single line text field.
  4. *
  5. * @since 1.0.0
  6. */
  7. class WPForms_Field_Text extends WPForms_Field {
  8. /**
  9. * Primary class constructor.
  10. *
  11. * @since 1.0.0
  12. */
  13. public function init() {
  14. // Define field type information.
  15. $this->name = esc_html__( 'Single Line Text', 'wpforms-lite' );
  16. $this->type = 'text';
  17. $this->icon = 'fa-text-width';
  18. $this->order = 30;
  19. // Define additional field properties.
  20. add_filter( 'wpforms_field_properties_text', array( $this, 'field_properties' ), 5, 3 );
  21. add_action( 'wpforms_frontend_js', array( $this, 'frontend_js' ) );
  22. }
  23. /**
  24. * Convert mask formatted for jquery.inputmask into the format used by amp-inputmask.
  25. *
  26. * Note that amp-inputmask does not yet support all of the options that jquery.inputmask provides.
  27. * In particular, amp-inputmask doesn't provides:
  28. * - Upper-alphabetical mask.
  29. * - Upper-alphanumeric mask.
  30. * - Advanced Input Masks with arbitrary repeating groups.
  31. *
  32. * @link https://amp.dev/documentation/components/amp-inputmask
  33. * @link https://wpforms.com/docs/how-to-use-custom-input-masks/
  34. *
  35. * @param string $mask Mask formatted for jquery.inputmask.
  36. * @return array {
  37. * Mask and placeholder.
  38. *
  39. * @type string $mask Mask for amp-inputmask.
  40. * @type string $placeholder Placeholder derived from mask if one is not supplied.
  41. * }
  42. */
  43. protected function convert_mask_to_amp_inputmask( $mask ) {
  44. $placeholder = '';
  45. // Convert jquery.inputmask format into amp-inputmask format.
  46. $amp_mask = '';
  47. $req_mask_mapping = array(
  48. '9' => '0', // Numeric.
  49. 'a' => 'L', // Alphabetical (a-z or A-Z).
  50. 'A' => 'L', // Upper-alphabetical (A-Z). Note: AMP does not have an uppercase-alphabetical mask type, so same as previous.
  51. '*' => 'A', // Alphanumeric (0-9, a-z, A-Z).
  52. '&' => 'A', // Upper-alphanumeric (A-Z, 0-9). Note: AMP does not have an uppercase-alphanumeric mask type, so same as previous.
  53. ' ' => '_', // Automatically insert spaces.
  54. );
  55. $opt_mask_mapping = array(
  56. '9' => '9', // The user may optionally add a numeric character.
  57. 'a' => 'l', // The user may optionally add an alphabetical character.
  58. 'A' => 'l', // The user may optionally add an alphabetical character.
  59. '*' => 'a', // The user may optionally add an alphanumeric character.
  60. '&' => 'a', // The user may optionally add an alphanumeric character.
  61. );
  62. $placeholder_mapping = array(
  63. '9' => '0',
  64. 'a' => 'a',
  65. 'A' => 'a',
  66. '*' => '_',
  67. '&' => '_',
  68. );
  69. $is_inside_optional = false;
  70. $last_mask_token = null;
  71. for ( $i = 0, $len = strlen( $mask ); $i < $len; $i++ ) {
  72. if ( '[' === $mask[ $i ] ) {
  73. $is_inside_optional = true;
  74. $placeholder .= $mask[ $i ];
  75. continue;
  76. } elseif ( ']' === $mask[ $i ] ) {
  77. $is_inside_optional = false;
  78. $placeholder .= $mask[ $i ];
  79. continue;
  80. } elseif ( isset( $last_mask_token ) && preg_match( '/^\{(?P<n>\d+)(?:,(?P<m>\d+))?\}/', substr( $mask, $i ), $matches ) ) {
  81. $amp_mask .= str_repeat( $req_mask_mapping[ $last_mask_token ], $matches['n'] );
  82. $placeholder .= str_repeat( $placeholder_mapping[ $last_mask_token ], $matches['n'] );
  83. if ( isset( $matches['m'] ) ) {
  84. $amp_mask .= str_repeat( $opt_mask_mapping[ $last_mask_token ], $matches['m'] );
  85. $placeholder .= str_repeat( $placeholder_mapping[ $last_mask_token ], $matches['m'] );
  86. }
  87. $i += strlen( $matches[0] ) - 1;
  88. $last_mask_token = null; // Reset.
  89. continue;
  90. }
  91. if ( '\\' === $mask[ $i ] ) {
  92. $amp_mask .= '\\';
  93. $i++;
  94. if ( ! isset( $mask[ $i ] ) ) {
  95. continue;
  96. }
  97. $amp_mask .= $mask[ $i ];
  98. } else {
  99. // Remember this token in case it is a mask.
  100. if ( isset( $opt_mask_mapping[ $mask[ $i ] ] ) ) {
  101. $last_mask_token = $mask[ $i ];
  102. }
  103. if ( $is_inside_optional && isset( $opt_mask_mapping[ $mask[ $i ] ] ) ) {
  104. $amp_mask .= $opt_mask_mapping[ $mask[ $i ] ];
  105. } elseif ( isset( $req_mask_mapping[ $mask[ $i ] ] ) ) {
  106. $amp_mask .= $req_mask_mapping[ $mask[ $i ] ];
  107. } else {
  108. $amp_mask .= '\\' . $mask[ $i ];
  109. }
  110. }
  111. if ( isset( $placeholder_mapping[ $mask[ $i ] ] ) ) {
  112. $placeholder .= $placeholder_mapping[ $mask[ $i ] ];
  113. } else {
  114. $placeholder .= $mask[ $i ];
  115. }
  116. }
  117. return array( $amp_mask, $placeholder );
  118. }
  119. /**
  120. * Define additional field properties.
  121. *
  122. * @since 1.4.5
  123. *
  124. * @param array $properties Field properties.
  125. * @param array $field Field settings.
  126. * @param array $form_data Form data and settings.
  127. *
  128. * @return array
  129. */
  130. public function field_properties( $properties, $field, $form_data ) {
  131. // Input primary: Detect custom input mask.
  132. if ( ! empty( $field['input_mask'] ) ) {
  133. // Add class that will trigger custom mask.
  134. $properties['inputs']['primary']['class'][] = 'wpforms-masked-input';
  135. if ( wpforms_is_amp() ) {
  136. list( $amp_mask, $placeholder ) = $this->convert_mask_to_amp_inputmask( $field['input_mask'] );
  137. $properties['inputs']['primary']['attr']['mask'] = $amp_mask;
  138. if ( empty( $properties['inputs']['primary']['attr']['placeholder'] ) ) {
  139. $properties['inputs']['primary']['attr']['placeholder'] = $placeholder;
  140. }
  141. } elseif ( false !== strpos( $field['input_mask'], 'alias:' ) ) {
  142. $mask = str_replace( 'alias:', '', $field['input_mask'] );
  143. $properties['inputs']['primary']['data']['inputmask-alias'] = $mask;
  144. } elseif ( false !== strpos( $field['input_mask'], 'regex:' ) ) {
  145. $mask = str_replace( 'regex:', '', $field['input_mask'] );
  146. $properties['inputs']['primary']['data']['inputmask-regex'] = $mask;
  147. } elseif ( false !== strpos( $field['input_mask'], 'date:' ) ) {
  148. $mask = str_replace( 'date:', '', $field['input_mask'] );
  149. $properties['inputs']['primary']['data']['inputmask-alias'] = 'datetime';
  150. $properties['inputs']['primary']['data']['inputmask-inputformat'] = $mask;
  151. } else {
  152. $properties['inputs']['primary']['data']['inputmask-mask'] = $field['input_mask'];
  153. $properties['inputs']['primary']['data']['rule-empty-blanks'] = true;
  154. }
  155. }
  156. return $properties;
  157. }
  158. /**
  159. * Field options panel inside the builder.
  160. *
  161. * @since 1.0.0
  162. *
  163. * @param array $field Field settings.
  164. */
  165. public function field_options( $field ) {
  166. /*
  167. * Basic field options.
  168. */
  169. // Options open markup.
  170. $this->field_option(
  171. 'basic-options',
  172. $field,
  173. array(
  174. 'markup' => 'open',
  175. )
  176. );
  177. // Label.
  178. $this->field_option( 'label', $field );
  179. // Description.
  180. $this->field_option( 'description', $field );
  181. // Required toggle.
  182. $this->field_option( 'required', $field );
  183. // Options close markup.
  184. $this->field_option(
  185. 'basic-options',
  186. $field,
  187. array(
  188. 'markup' => 'close',
  189. )
  190. );
  191. /*
  192. * Advanced field options.
  193. */
  194. // Options open markup.
  195. $this->field_option(
  196. 'advanced-options',
  197. $field,
  198. array(
  199. 'markup' => 'open',
  200. )
  201. );
  202. // Size.
  203. $this->field_option( 'size', $field );
  204. // Placeholder.
  205. $this->field_option( 'placeholder', $field );
  206. // Limit length.
  207. $args = [
  208. 'slug' => 'limit_enabled',
  209. 'content' => $this->field_element(
  210. 'toggle',
  211. $field,
  212. [
  213. 'slug' => 'limit_enabled',
  214. 'value' => isset( $field['limit_enabled'] ),
  215. 'desc' => esc_html__( 'Limit Length', 'wpforms-lite' ),
  216. 'tooltip' => esc_html__( 'Check this option to limit text length by characters or words count.', 'wpforms-lite' ),
  217. ],
  218. false
  219. ),
  220. ];
  221. $this->field_element( 'row', $field, $args );
  222. $count = $this->field_element(
  223. 'text',
  224. $field,
  225. [
  226. 'type' => 'number',
  227. 'slug' => 'limit_count',
  228. 'attrs' => [
  229. 'min' => 1,
  230. 'step' => 1,
  231. 'pattern' => '[0-9]',
  232. ],
  233. 'value' => ! empty( $field['limit_count'] ) ? absint( $field['limit_count'] ) : 1,
  234. ],
  235. false
  236. );
  237. $mode = $this->field_element(
  238. 'select',
  239. $field,
  240. [
  241. 'slug' => 'limit_mode',
  242. 'value' => ! empty( $field['limit_mode'] ) ? esc_attr( $field['limit_mode'] ) : 'characters',
  243. 'options' => [
  244. 'characters' => esc_html__( 'Characters', 'wpforms-lite' ),
  245. 'words' => esc_html__( 'Words', 'wpforms-lite' ),
  246. ],
  247. ],
  248. false
  249. );
  250. $args = [
  251. 'slug' => 'limit_controls',
  252. 'class' => ! isset( $field['limit_enabled'] ) ? 'wpforms-hide' : '',
  253. 'content' => $count . $mode,
  254. ];
  255. $this->field_element( 'row', $field, $args );
  256. // Default value.
  257. $this->field_option( 'default_value', $field );
  258. // Input Mask.
  259. $lbl = $this->field_element(
  260. 'label',
  261. $field,
  262. [
  263. 'slug' => 'input_mask',
  264. 'value' => esc_html__( 'Input Mask', 'wpforms-lite' ),
  265. 'tooltip' => esc_html__( 'Enter your custom input mask.', 'wpforms-lite' ),
  266. 'after_tooltip' => '<a href="https://wpforms.com/how-to-use-custom-input-masks/" class="after-label-description" target="_blank" rel="noopener noreferrer">' . esc_html__( 'See Examples & Docs', 'wpforms-lite' ) . '</a>',
  267. ],
  268. false
  269. );
  270. $fld = $this->field_element(
  271. 'text',
  272. $field,
  273. [
  274. 'slug' => 'input_mask',
  275. 'value' => ! empty( $field['input_mask'] ) ? esc_attr( $field['input_mask'] ) : '',
  276. ],
  277. false
  278. );
  279. $this->field_element(
  280. 'row',
  281. $field,
  282. [
  283. 'slug' => 'input_mask',
  284. 'content' => $lbl . $fld,
  285. ]
  286. );
  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. *
  305. * @param array $field Field settings.
  306. */
  307. public function field_preview( $field ) {
  308. // Define data.
  309. $placeholder = ! empty( $field['placeholder'] ) ? esc_attr( $field['placeholder'] ) : '';
  310. // Label.
  311. $this->field_preview_option( 'label', $field );
  312. // Primary input.
  313. echo '<input type="text" placeholder="' . esc_attr( $placeholder ) . '" class="primary-input" readonly>';
  314. // Description.
  315. $this->field_preview_option( 'description', $field );
  316. }
  317. /**
  318. * Field display on the form front-end.
  319. *
  320. * @since 1.0.0
  321. *
  322. * @param array $field Field settings.
  323. * @param array $deprecated Deprecated.
  324. * @param array $form_data Form data and settings.
  325. */
  326. public function field_display( $field, $deprecated, $form_data ) {
  327. // Define data.
  328. $primary = $field['properties']['inputs']['primary'];
  329. if ( isset( $field['limit_enabled'] ) ) {
  330. $limit_count = isset( $field['limit_count'] ) ? absint( $field['limit_count'] ) : 0;
  331. $limit_mode = isset( $field['limit_mode'] ) ? sanitize_key( $field['limit_mode'] ) : 'characters';
  332. $primary['data']['form-id'] = $form_data['id'];
  333. $primary['data']['field-id'] = $field['id'];
  334. if ( 'characters' === $limit_mode ) {
  335. $primary['class'][] = 'wpforms-limit-characters-enabled';
  336. $primary['attr']['maxlength'] = $limit_count;
  337. $primary['data']['text-limit'] = $limit_count;
  338. } else {
  339. $primary['class'][] = 'wpforms-limit-words-enabled';
  340. $primary['data']['text-limit'] = $limit_count;
  341. }
  342. }
  343. // Primary field.
  344. printf(
  345. '<input type="text" %s %s>',
  346. wpforms_html_attributes( $primary['id'], $primary['class'], $primary['data'], $primary['attr'] ),
  347. $primary['required'] // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
  348. );
  349. }
  350. /**
  351. * Enqueue frontend limit option js.
  352. *
  353. * @since 1.5.6
  354. *
  355. * @param array $forms Forms on the current page.
  356. */
  357. public function frontend_js( $forms ) {
  358. // Get fields.
  359. $fields = array_map(
  360. function( $form ) {
  361. return empty( $form['fields'] ) ? array() : $form['fields'];
  362. },
  363. (array) $forms
  364. );
  365. // Make fields flat.
  366. $fields = array_reduce(
  367. $fields,
  368. function( $accumulator, $current ) {
  369. return array_merge( $accumulator, $current );
  370. },
  371. array()
  372. );
  373. // Leave only fields with limit.
  374. $fields = array_filter(
  375. $fields,
  376. function( $field ) {
  377. return $field['type'] === $this->type && isset( $field['limit_enabled'] ) && ! empty( $field['limit_count'] );
  378. }
  379. );
  380. if ( count( $fields ) ) {
  381. $min = \wpforms_get_min_suffix();
  382. wp_enqueue_script( 'wpforms-text-limit', WPFORMS_PLUGIN_URL . "assets/js/text-limit{$min}.js", array(), WPFORMS_VERSION, true );
  383. }
  384. }
  385. /**
  386. * Format and sanitize field.
  387. *
  388. * @since 1.5.6
  389. *
  390. * @param int $field_id Field ID.
  391. * @param mixed $field_submit Field value that was submitted.
  392. * @param array $form_data Form data and settings.
  393. */
  394. public function format( $field_id, $field_submit, $form_data ) {
  395. $field = $form_data['fields'][ $field_id ];
  396. $name = ! empty( $field['label'] ) ? sanitize_text_field( $field['label'] ) : '';
  397. // Sanitize.
  398. $value = sanitize_text_field( $field_submit );
  399. wpforms()->process->fields[ $field_id ] = array(
  400. 'name' => $name,
  401. 'value' => $value,
  402. 'id' => absint( $field_id ),
  403. 'type' => $this->type,
  404. );
  405. }
  406. /**
  407. * Validate field on form submit.
  408. *
  409. * @since 1.6.2
  410. *
  411. * @param int $field_id Field ID.
  412. * @param mixed $field_submit Field value that was submitted.
  413. * @param array $form_data Form data and settings.
  414. */
  415. public function validate( $field_id, $field_submit, $form_data ) {
  416. parent::validate( $field_id, $field_submit, $form_data );
  417. if ( empty( $form_data['fields'][ $field_id ] ) || empty( $form_data['fields'][ $field_id ]['limit_enabled'] ) ) {
  418. return;
  419. }
  420. $field = $form_data['fields'][ $field_id ];
  421. $limit = absint( $field['limit_count'] );
  422. $mode = ! empty( $field['limit_mode'] ) ? sanitize_key( $field['limit_mode'] ) : 'characters';
  423. $value = sanitize_text_field( $field_submit );
  424. if ( 'characters' === $mode ) {
  425. if ( mb_strlen( str_replace( "\r\n", "\n", $value ) ) > $limit ) {
  426. /* translators: %s - limit characters number. */
  427. wpforms()->process->errors[ $form_data['id'] ][ $field_id ] = sprintf( _n( 'Text can\'t exceed %d character.', 'Text can\'t exceed %d characters.', $limit, 'wpforms-lite' ), $limit );
  428. return;
  429. }
  430. } else {
  431. if ( wpforms_count_words( $value ) > $limit ) {
  432. /* translators: %s - limit words number. */
  433. wpforms()->process->errors[ $form_data['id'] ][ $field_id ] = sprintf( _n( 'Text can\'t exceed %d word.', 'Text can\'t exceed %d words.', $limit, 'wpforms-lite' ), $limit );
  434. return;
  435. }
  436. }
  437. }
  438. }
  439. new WPForms_Field_Text();