SproutCMS

This is the code documentation for the SproutCMS project

source of /sprout/Helpers/Form.php

Copyright (C) 2017 Karmabunny Pty Ltd.

This file is a part of SproutCMS.

SproutCMS is free software: you can redistribute it and/or modify it under the terms
of the GNU General Public License as published by the Free Software Foundation, either
version 2 of the License, or (at your option) any later version.

For more information, visit <http://getsproutcms.com>.

This class was originally from Kohana 2.3.4
Copyright 2007-2008 Kohana Team
  1. <?php
  2. /**
  3.  * Copyright (C) 2017 Karmabunny Pty Ltd.
  4.  *
  5.  * This file is a part of SproutCMS.
  6.  *
  7.  * SproutCMS is free software: you can redistribute it and/or modify it under the terms
  8.  * of the GNU General Public License as published by the Free Software Foundation, either
  9.  * version 2 of the License, or (at your option) any later version.
  10.  *
  11.  * For more information, visit <http://getsproutcms.com>.
  12.  *
  13.  * This class was originally from Kohana 2.3.4
  14.  * Copyright 2007-2008 Kohana Team
  15.  */
  16. namespace Sprout\Helpers;
  17.  
  18. use ReflectionMethod;
  19.  
  20.  
  21. /**
  22.  * Helper functions for outputting form elements.
  23.  *
  24.  * Wraps form fields (e.g. from {@see Fb}) with additional HTML.
  25.  *
  26.  * Most wrapping will done using the __callStatic method which actually just calls {@see Form::fieldAuto}.
  27.  * That method uses reflection to look for a custom docblock tag, @wrap-in-fieldset.
  28.  * If that docblock tag is found, the field is wrapped in {@see Form::fieldFieldset}
  29.  * If that docblock tag is not found (the most common case), the field is wrapped in {@see Form::fieldPlain}
  30.  *
  31.  * If the field being wrapped isn't in the Fb helper, the methods fieldAuto, fieldPlain, and fieldFieldset
  32.  * can be invoked directly.
  33.  *
  34.  * The outermost wrapper DIV around the field has a class of "field-element".
  35.  * Additional classes are also added:
  36.  * - The method and class name, in the format 'field-element--id-<name>'
  37.  * - If an "id" attribute is set, in the format 'field-element--id-<id>'
  38.  * - If the field is required, 'field-element--required'
  39.  * - If the field is disabled, 'field-element--disabled'
  40.  * - If the field has an error, 'field-element--error'
  41.  * - One or more custom classes can be specified using the attribute "-wrapper-class".
  42.  * Each (array or space separated) class is prefixed with 'field-element--'
  43.  *
  44.  * @example
  45.  * Form::setData($data);
  46.  * Form::setErrors($errors);
  47.  *
  48.  * Form::nextFieldDetails('First name', true);
  49.  * echo Form::text('first_name');
  50.  *
  51.  * Form::nextFieldDetails('Email', true, 'Please enter your email address');
  52.  * echo Form::email('email', [], ['-wrapper-class' => 'small']);
  53.  *
  54.  * Form::nextFieldDetails('Phone', false, 'Enter a phone number using the unique UI');
  55.  * echo Form::fieldPlain('SproutModules\Someone\CustomModule\Helpers\FbHack::phone', 'phone', [], []);
  56.  */
  57. class Form
  58. {
  59. static $errors;
  60. static $next_label = null;
  61. static $next_required = null;
  62. static $next_helptext = null;
  63. static $name_format = "%s";
  64. static $id_prefix = '';
  65.  
  66.  
  67. /**
  68.   * Gets the form per-field value for a single field
  69.   *
  70.   * As form field datas are stored using the {@see Fb} class, this method just gets the data from there
  71.   *
  72.   * @param string $field The field name
  73.   */
  74. public static function getData($field)
  75. {
  76. return Fb::getData($field);
  77. }
  78.  
  79.  
  80. /**
  81.   * Set form per-field values for the fields
  82.   *
  83.   * As form fields are rendered using the {@see Fb} class, this method just sets the data there
  84.   *
  85.   * @param array $data In the format
  86.   * Key: (string)<field name>
  87.   * Value: (string)<field value>
  88.   */
  89. public static function setData(array $data)
  90. {
  91. Fb::setData($data);
  92. }
  93.  
  94.  
  95. /**
  96.   * Sets the value for a single field
  97.   *
  98.   * As form fields are rendered using the {@see Fb} class, this method just sets the data there
  99.   *
  100.   * @param array $field Field name, e.g. 'first_name'
  101.   * @param array $value Field value, e.g. 'John'
  102.   * @return void
  103.   */
  104. public static function setFieldValue($field, $value)
  105. {
  106. Fb::setFieldValue($field, $value);
  107. }
  108.  
  109.  
  110. /**
  111.   * Set per-field error messages to display
  112.   *
  113.   * A given field can have either a single error message or an array of errors
  114.   * The output from the {@see Validator::getFieldErrors} method can be used directly as input to this method
  115.   *
  116.   * @param array $errors In the format
  117.   * Key: (string)<field name>
  118.   * Value: (string | array of string)<errors>
  119.   */
  120. public static function setErrors(array $errors)
  121. {
  122. self::$errors = $errors;
  123. }
  124.  
  125.  
  126. /**
  127.   * Load data and errors from the session, with optional record id validation
  128.   *
  129.   * Expected session keys:
  130.   * record_id Checked against $verify_record_id, session data is thrown away in case of mismatch
  131.   * field_values Field data, loaded using {@see Form::setData}
  132.   * field_errors Field errors, loaded using {@see Form::setErrors}
  133.   *
  134.   * @example
  135.   * $data = Form::loadFromSession('register');
  136.   * if (empty($data)) {
  137.   * $data = $this->add_defaults;
  138.   * Form::setData($data);
  139.   * }
  140.   *
  141.   * @param string $key Session key to get values from
  142.   * @param mixed $verify_record_id For edit record verification
  143.   * @return array Loaded session data
  144.   * @return null No session data found
  145.   */
  146. public static function loadFromSession($key, $verify_record_id = null)
  147. {
  148. Session::instance();
  149.  
  150. if (!empty($verify_record_id) and !empty($_SESSION[$key]['record_id'])) {
  151. if ($_SESSION[$key]['record_id'] != $verify_record_id) {
  152. unset($_SESSION[$key]);
  153. return null;
  154. }
  155. }
  156.  
  157. if (!empty($_SESSION[$key]['field_errors'])) {
  158. self::setErrors($_SESSION[$key]['field_errors']);
  159. }
  160.  
  161. if (!empty($_SESSION[$key]['field_values'])) {
  162. self::setData($_SESSION[$key]['field_values']);
  163. return $_SESSION[$key]['field_values'];
  164. } else {
  165. return null;
  166. }
  167. }
  168.  
  169.  
  170. /**
  171.   * Set a format string which will alter the field name prior to being passed to the underlying render method
  172.   *
  173.   * Formatting is done using {@see sprintf}
  174.   * A single parameter is provided to the sprintf() call, the field name
  175.   * The default format does no transformation, i.e. the string '%s'
  176.   * This parameter persists across multiple form fields
  177.   *
  178.   * @example
  179.   * Form::setFieldNameFormat('pages[%s]')
  180.   * Form::text('name') // field name will be 'pages[text]'
  181.   *
  182.   * @param string $format Format string
  183.   */
  184. public static function setFieldNameFormat($format)
  185. {
  186. self::$name_format = $format;
  187. }
  188.  
  189.  
  190. /**
  191.   * Sets the prefix for generated IDs
  192.   *
  193.   * @param string $prefix The prefix
  194.   */
  195. public static function setFieldIdPrefix($prefix)
  196. {
  197. static::$id_prefix = $prefix;
  198. Fb::$id_prefix = $prefix;
  199. }
  200.  
  201.  
  202. /**
  203.   * Generate a unique id which should be stable across calls to this URL
  204.   * as long as the number and order of fields on the page remains the same
  205.   *
  206.   * @return string 'field?', where ? is an incrementing number starting at zero
  207.   */
  208. protected static function genId()
  209. {
  210. static $inc = 0;
  211. return static::$id_prefix . 'field' . $inc++;
  212. }
  213.  
  214.  
  215. /**
  216.   * Reset the state machine for field values
  217.   */
  218. public static function resetField()
  219. {
  220. self::$next_label = null;
  221. self::$next_required = null;
  222. self::$next_helptext = null;
  223. }
  224.  
  225.  
  226. /**
  227.   * Set the details for the next field which will be outputted.
  228.   *
  229.   * After returning a field, these values will be cleared from the state machine
  230.   *
  231.   * Both the label and helptext support a subset of HTML, {@see Text::limitedSubsetHtml} for more details
  232.   *
  233.   * @param string $label Human label for the field (e.g. 'Email address'). Some HTML allowed
  234.   * @param bool $required True if this field is required, false if it's optional
  235.   * @param string $helptext Optional HTML helptext
  236.   */
  237. public static function nextFieldDetails($label, $required, $helptext = null)
  238. {
  239. self::$next_label = Text::limitedSubsetHtml($label);
  240. self::$next_required = $required;
  241. self::$next_helptext = Text::limitedSubsetHtml($helptext);
  242. }
  243.  
  244.  
  245. /**
  246.   * Convert a full method name (e.g. Sprout\Helpers\Fb::text) into a friendly class name
  247.   *
  248.   * The classes {@see Fb} and {@see Form} aren't emitted, but all other class names are
  249.   *
  250.   * @param string $method Full original method name, in namespace\class::method format
  251.   * @return string HTML-safe name for use in a CSS class
  252.   */
  253. protected static function fieldMethodClass($method)
  254. {
  255. $method = str_replace('Sprout\Helpers\Fb::', '', $method);
  256. $method = str_replace('Sprout\Helpers\Form::', '', $method);
  257. $method = str_replace('\\', '-', $method);
  258. $method = str_replace('::', '--', $method);
  259. return Enc::id(strtolower($method));
  260. }
  261.  
  262.  
  263. /**
  264.   * Format a field name as per the specification defined by {@see Form::setFieldNameFormat}
  265.   *
  266.   * @param string $name Unformatted field name
  267.   * @return string Formatted field name
  268.   */
  269. protected static function convertFieldName($name)
  270. {
  271. if (strpos($name, ',') === false) {
  272. return sprintf(self::$name_format, $name);
  273. }
  274.  
  275. // Handle compound fields (e.g. Fb::googleMap)
  276. $fields = explode(',', $name);
  277. foreach ($fields as &$f) {
  278. $f = sprintf(self::$name_format, $f);
  279. }
  280. return implode(',', $fields);
  281. }
  282.  
  283.  
  284. /**
  285.   * Return the errors for a given field
  286.   *
  287.   * Supports nested error arrays; If $field_name is something like member[5][test] then the error
  288.   * will be read from self::$errors['member']['5']['test']
  289.   *
  290.   * @param string $field_name Field to return errors for
  291.   * @return array Error messages, as strings
  292.   * @return NULL if there aren't any error messages
  293.   */
  294. public static function getFieldErrors($field_name)
  295. {
  296. if (strpos($field_name, '[') === false) {
  297. $val = @self::$errors[$field_name];
  298. } else {
  299. // Get a list of keys
  300. $keys = explode('[', $field_name);
  301. foreach ($keys as &$k) {
  302. $k = rtrim($k, ']');
  303. if ($k == '') return null; // Anon keys not supported
  304. }
  305. unset($k);
  306.  
  307. // Loop through the keys till we get the value we want
  308. $val = self::$errors;
  309. foreach ($keys as $k) {
  310. $val = @$val[$k];
  311. }
  312. }
  313.  
  314. if (empty($val)) {
  315. return null;
  316. } else if (is_array($val)) {
  317. return $val;
  318. } else {
  319. return [$val];
  320. }
  321. }
  322.  
  323.  
  324. /**
  325.   * Return HTML for a 'plain' field, i.e. one which doesn't require a FIELDSET wrapped around it.
  326.   *
  327.   * The main wrapping DIV will contain additional classes if the field is required, disabled or has an error.
  328.   * A class is also output for the field method name (if the name contains "Sprout\Helpers\Fb::" this is removed)
  329.   * If the field has an explicit ID set, that will be added as a class on the wrapper too.
  330.   *
  331.   * The special attribute "-wrapper-class" can be used to add classes to the wrapper DIV.
  332.   * Multiple classes can be specified, space separated.
  333.   * These classes will be prefixed with "field-element--"
  334.   *
  335.   * @example
  336.   * echo Form::fieldPlain('Sprout\Helpers\Fb::text', 'first_name', [], []);
  337.   *
  338.   * @example
  339.   * // Adds the class "field-element--id-first-name" to the wrapper
  340.   * echo Form::fieldPlain('Sprout\Helpers\Fb::text', 'first_name', ['id' => 'first-name'], []);
  341.   *
  342.   * @example
  343.   * // Adds the class "field-element--small" to the wrapper
  344.   * echo Form::fieldPlain('Sprout\Helpers\Fb::text', 'first_name', ['-wrapper-class' => 'small'], []);
  345.   *
  346.   * @param callable $method The actual field rendering method
  347.   * @param string $name The field name - this is passed to the rendering method
  348.   * @param array $attrs The field attrs - this is passed to the rendering method
  349.   * @param array $options The field options - this is passed to the rendering method
  350.   * @return string HTML
  351.   */
  352. public static function fieldPlain(callable $method, $name, array $attrs = [], array $options = [])
  353. {
  354. $name = self::convertFieldName($name);
  355. $errs = self::getFieldErrors($name);
  356.  
  357. $classes = array('field-element');
  358. $classes[] = 'field-element--' . self::fieldMethodClass($method);
  359. if (isset($attrs['id'])) {
  360. $classes[] = 'field-element--id-' . Enc::id($attrs['id']);
  361. }
  362. if (self::$next_required) {
  363. $classes[] = 'field-element--required';
  364. }
  365. if (isset($attrs['disabled']) or in_array('disabled', $attrs, true)) {
  366. $classes[] = 'field-element--disabled';
  367. }
  368. if (!empty($errs)) {
  369. $classes[] = 'field-element--error';
  370. }
  371. if (isset($attrs['-wrapper-class'])) {
  372. if (is_string($attrs['-wrapper-class'])) {
  373. $attrs['-wrapper-class'] = preg_split('/\s+/', $attrs['-wrapper-class']);
  374. }
  375. foreach ($attrs['-wrapper-class'] as $class) {
  376. $classes[] = 'field-element--' . $class;
  377. }
  378. unset($attrs['-wrapper-class']);
  379. }
  380. $classes = implode(' ', $classes);
  381. $out = '<div class="' . Enc::html($classes) . '">';
  382.  
  383. if (!isset($attrs['id'])) {
  384. $attrs['id'] = self::genId();
  385. }
  386.  
  387. $field_html = call_user_func($method, $name, $attrs, $options);
  388.  
  389. // It is invalid to output a LABEL without a corresponding element
  390. // check if the ID exists in the field
  391. $has_id_attr = (strpos($field_html, 'id="' . $attrs['id'] . '"') !== false);
  392.  
  393. // Label section
  394. if (self::$next_label) {
  395. $out .= '<div class="field-label">';
  396. if ($has_id_attr) {
  397. $out .= '<label for="' . Enc::html($attrs['id']) . '">';
  398. }
  399. $out .= self::$next_label;
  400. if (self::$next_required) {
  401. $out .= ' <span class="field-label__required">required</span>';
  402. }
  403. if ($has_id_attr) {
  404. $out .= '</label>';
  405. }
  406. if (self::$next_helptext) {
  407. $out .= '<div class="field-helper">' . self::$next_helptext . '</div>';
  408. }
  409. $out .= '</div>';
  410. }
  411.  
  412. // Field itself
  413. $out .= '<div class="field-input">';
  414. $out .= $field_html;
  415. $out .= '</div>';
  416.  
  417. // Field errors
  418. if (!empty($errs)) {
  419. $out .= '<div class="field-error">';
  420. $out .= '<ul class="field-error__list">';
  421. foreach ($errs as $err) {
  422. $out .= '<li class="field-error__list__item">' . Enc::html($err) . '</li>';
  423. }
  424. $out .= '</ul>';
  425. $out .= '</div>';
  426. }
  427.  
  428. $out .= '</div>';
  429. $out .= PHP_EOL . PHP_EOL;
  430.  
  431. self::resetField();
  432.  
  433. return $out;
  434. }
  435.  
  436.  
  437. /**
  438.   * Return HTML for a field wrapped in a FIELDSET
  439.   *
  440.   * The main wrapping DIV will contain additional classes if the field is required, disabled or has an error.
  441.   * A class is also output for hte field method name (if the name contains "Sprout\Helpers\Fb::" this is removed)
  442.   * If the field has an explicit ID set, that will be added as a class on the wrapper too.
  443.   *
  444.   * The special attribute "-wrapper-class" can be used to add classes to the wrapper DIV.
  445.   * Multiple classes can be specified, space separated.
  446.   * These classes will be prefixed with "field-element--"
  447.   *
  448.   * @param callable $method The actual field rendering method
  449.   * @param string $name The field name - this is passed to the rendering method
  450.   * @param array $attrs The field attrs - this is passed to the rendering method
  451.   * @param array $options The field options - this is passed to the rendering method
  452.   * @return string HTML
  453.   */
  454. public static function fieldFieldset(callable $method, $name, array $attrs = [], array $options = [])
  455. {
  456. $name = self::convertFieldName($name);
  457. $errs = self::getFieldErrors($name);
  458.  
  459. $classes = array('field-element');
  460. $classes[] = 'field-element--' . self::fieldMethodClass($method);
  461. if (isset($attrs['id'])) {
  462. $classes[] = 'field-element--id-' . Enc::id($attrs['id']);
  463. }
  464. if (self::$next_required) {
  465. $classes[] = 'field-element--required';
  466. }
  467. if (isset($attrs['disabled']) or in_array('disabled', $attrs, true)) {
  468. $classes[] = 'field-element--disabled';
  469. }
  470. if (!empty($errs)) {
  471. $classes[] = 'field-element--error';
  472. }
  473. if (isset($attrs['-wrapper-class'])) {
  474. if (is_string($attrs['-wrapper-class'])) {
  475. $attrs['-wrapper-class'] = preg_split('/\s+/', $attrs['-wrapper-class']);
  476. }
  477. foreach ($attrs['-wrapper-class'] as $class) {
  478. $classes[] = 'field-element--' . $class;
  479. }
  480. unset($attrs['-wrapper-class']);
  481. }
  482. $classes = implode(' ', $classes);
  483.  
  484.  
  485. if (!isset($attrs['id'])) {
  486. $attrs['id'] = self::genId();
  487. }
  488.  
  489. $out = '<div class="' . Enc::html($classes) . '">';
  490. $out .= '<fieldset class="fieldset--' . self::fieldMethodClass($method) . '">';
  491.  
  492. // Label section
  493. if (self::$next_label) {
  494. $out .= '<legend class="fieldset__legend">';
  495. $out .= self::$next_label;
  496. if (self::$next_required) {
  497. $out .= ' <span class="field-label__required">required</span>';
  498. }
  499. $out .= '</legend>';
  500.  
  501. if (self::$next_helptext) {
  502. $out .= '<div class="field-helper">' . self::$next_helptext . '</div>';
  503. }
  504. }
  505.  
  506. // Field itself
  507. $out .= '<div class="field-element__input-set">';
  508. $out .= call_user_func($method, $name, $attrs, $options);
  509. $out .= '</div>';
  510.  
  511. $out .= '</fieldset>';
  512.  
  513. // Field errors
  514. if (!empty($errs)) {
  515. $out .= '<div class="field-error">';
  516. $out .= '<ul class="field-error__list">';
  517. foreach ($errs as $err) {
  518. $out .= '<li class="field-error__list__item">' . Enc::html($err) . '</li>';
  519. }
  520. $out .= '</ul>';
  521. $out .= '</div>';
  522. }
  523.  
  524. $out .= '</div>';
  525. $out .= PHP_EOL . PHP_EOL;
  526.  
  527. self::resetField();
  528.  
  529. return $out;
  530. }
  531.  
  532.  
  533. /**
  534.   * Return HTML for a field, with the wrapping HTML detected automatically.
  535.   *
  536.   * To enable fieldset wrapping, add the docblock tag @wrap-in-fieldset to the field generation method
  537.   *
  538.   * @param callable $method The actual field rendering method
  539.   * @param string $name The field name - this is passed to the rendering method
  540.   * @param array $attrs The field attrs - this is passed to the rendering method
  541.   * @param array $options The field options - this is passed to the rendering method
  542.   * @return string HTML
  543.   */
  544. public static function fieldAuto(callable $method, $name, array $attrs = [], array $options = [])
  545. {
  546. $use_fieldset = false;
  547.  
  548. $func = new ReflectionMethod($method);
  549. $comment = $func->getDocComment();
  550. if ($comment and strpos($comment, '@wrap-in-fieldset') !== false) {
  551. $use_fieldset = true;
  552. }
  553.  
  554. if ($use_fieldset) {
  555. return static::fieldFieldset($method, $name, $attrs, $options);
  556. } else {
  557. return static::fieldPlain($method, $name, $attrs, $options);
  558. }
  559. }
  560.  
  561.  
  562. /**
  563.   * Auto-wrapper around Fb methods
  564.   *
  565.   * Will wrap the Fb method with the same name as the called method, e.g. Form::datepicker wraps Fb::datepicker
  566.   * Wrapping is done using {@see Form::fieldAuto}
  567.   *
  568.   * @param string $func Method name
  569.   * @param array $args Method arguments
  570.   * @return string HTML
  571.   */
  572. public static function __callStatic($func, $args)
  573. {
  574. if (!isset($args[1])) $args[1] = [];
  575. if (!isset($args[2])) $args[2] = [];
  576. return self::fieldAuto('Sprout\Helpers\Fb::' . $func, $args[0], $args[1], $args[2]);
  577. }
  578.  
  579.  
  580. /**
  581.   * Returns the first argument
  582.   *
  583.   * This hacky little method works around the fact that fieldPlain only accepts a method name
  584.   *
  585.   * @param string $str
  586.   * @return string
  587.   */
  588. protected static function passString($str) {
  589. return $str;
  590. }
  591.  
  592.  
  593. /**
  594.   * Return HTML which has been wrapped in the form field DIVs
  595.   *
  596.   * @param string $html Content to wrap in the field
  597.   * @return string HTML
  598.   */
  599. public static function html($html)
  600. {
  601. return static::fieldPlain('Sprout\Helpers\Form::passString', $html);
  602. }
  603.  
  604.  
  605. /**
  606.   * Return content which has been HTML-encoded and wrapped in the form field DIVs
  607.   *
  608.   * @param string $plain Plain text to encode and wrap in the field
  609.   * @return string HTML
  610.   **/
  611. public static function out($plain)
  612. {
  613. return static::fieldPlain('Sprout\Helpers\Form::passString', Enc::html($plain));
  614. }
  615.  
  616.  
  617. /**
  618.   * Returns HTML for a text field, using {@see Fb::text} to generate the field itself
  619.   *
  620.   * @param string $name The name of the input field
  621.   * @param array $attrs Extra attributes for the input field
  622.   * @return string HTML
  623.   */
  624. public static function text($name, array $attrs = [])
  625. {
  626. return static::fieldPlain('Sprout\Helpers\Fb::text', $name, $attrs);
  627. }
  628.  
  629.  
  630. /**
  631.   * Returns HTML for a number field, using {@see Fb::number} to generate the field itself
  632.   *
  633.   * @param string $name The name of the input field
  634.   * @param array $attrs Extra attributes for the input field
  635.   * @return string HTML
  636.   */
  637. public static function number($name, array $attrs = [])
  638. {
  639. return static::fieldPlain('Sprout\Helpers\Fb::number', $name, $attrs);
  640. }
  641.  
  642.  
  643. /**
  644.   * Returns HTML for a money field, using {@see Fb::money} to generate the field itself
  645.   *
  646.   * @param string $name The name of the input field
  647.   * @param array $attrs Extra attributes for the input field
  648.   * @return string HTML
  649.   */
  650. public static function money($name, array $attrs = [], array $options = [])
  651. {
  652. return static::fieldPlain('Sprout\Helpers\Fb::money', $name, $attrs, $options);
  653. }
  654.  
  655.  
  656.  
  657. /**
  658.   * Returns HTML for a password field, using {@see Fb::password} to generate the field itself
  659.   *
  660.   * @param string $name The name of the input field
  661.   * @param array $attrs Extra attributes for the input field
  662.   * @return string HTML
  663.   */
  664. public static function password($name, array $attrs = [])
  665. {
  666. return static::fieldPlain('Sprout\Helpers\Fb::password', $name, $attrs);
  667. }
  668.  
  669.  
  670. /**
  671.   * Returns HTML for a bunch of radiobuttons, using {@see Fb::multiradio} to generate the fields
  672.   *
  673.   * @param string $name The name of the input field
  674.   * @param array $attrs Extra attributes for the input field
  675.   * @return string HTML
  676.   */
  677. public static function multiradio($name, array $attrs = [], array $options = [])
  678. {
  679. return static::fieldFieldset('Sprout\Helpers\Fb::multiradio', $name, $attrs, $options);
  680. }
  681.  
  682.  
  683. /**
  684.   * Returns HTML for a list of checkboxes, applying name conversions along the way
  685.   *
  686.   * Uses {@see Fb::checkboxBoolList} to generate the underlying checkbox list
  687.   *
  688.   * @param array $checkboxes An array of name => label mappings
  689.   * @param array $attrs Extra attributes applied to each checkbox field
  690.   * @return string HTML
  691.   */
  692. public static function checkboxList(array $checkboxes, array $attrs = [])
  693. {
  694. $prefixed_names = [];
  695.  
  696. foreach ($checkboxes as $name => $label) {
  697. $name = static::convertFieldName($name);
  698. $prefixed_names[$name] = $label;
  699. }
  700.  
  701. return static::fieldFieldset('Sprout\Helpers\Fb::checkboxBoolList', '', $attrs, $prefixed_names);
  702. }
  703.  
  704.  
  705. /**
  706.   * Returns HTML for an auto-complete list of records
  707.   *
  708.   * The form data for this field should be an array of arrays with at least the following keys:
  709.   * [
  710.   * 'id' => record ID,
  711.   * 'value' => title text visible in the list item,
  712.   * 'orderkey' => ordinal value for record ordering
  713.   * ]
  714.   *
  715.   * @param string $name Field name
  716.   * @param string $attrs Unused
  717.   * @param array $options Options; these are passed to the JS
  718.   * lookup_url string AJAX lookup URL, {@see Fb::autocomplete}; Required
  719.   * min_term_length int Min term length for autocomplete; default = 3
  720.   * reorder bool Default = false
  721.   * @return string HTML
  722.   */
  723. public static function autofillList($name, array $attrs = [], array $options = [])
  724. {
  725. Needs::fileGroup('sprout/autofill_list');
  726.  
  727. if (!isset($options['min_term_length'])) $options['min_term_length'] = 3;
  728. if (!isset($options['reorder'])) $options['reorder'] = false;
  729. if (!isset($options['single'])) $options['single'] = 'an item';
  730.  
  731. $search_label = "Search for {$options['single']} to add it to the list:";
  732. $search_field_id = Enc::id("autofill-{$name}-search");
  733.  
  734. $opts = [
  735. 'name' => $name,
  736. 'lookup_url' => $options['lookup_url'],
  737. 'min_term_length' => $options['min_term_length'],
  738. 'reorder' => $options['reorder'],
  739. 'single' => $options['single'],
  740. ];
  741.  
  742. $data = Fb::getData($name);
  743. if (empty($data)) $data = [];
  744. foreach ($data as &$el) {
  745. if (!is_array($el)) {
  746. $el = Enc::html($el);
  747. continue;
  748. }
  749. foreach ($el as &$val) {
  750. $val = Enc::html($val);
  751. }
  752. }
  753.  
  754. $out = '<div class="autofill-wrap">';
  755. $out .= '<div class="autofill-search">';
  756. $out .= '<div class="autofill-heading"><label for="' . $search_field_id . '">' . Enc::html($search_label) . '</label></div>';
  757. $out .= self::fieldPlain(
  758. 'Sprout\Helpers\Fb::text',
  759. $name . '_search',
  760. ['-wrapper-class' => 'white', 'id' => $search_field_id]
  761. );
  762. $out .= '</div>';
  763. $out .= '<script type="application/json" class="autofill-list-opts">' . json_encode($opts) . '</script>';
  764. $out .= '<script type="application/json" class="autofill-list-data">' . json_encode($data) . '</script>';
  765. $out .= '<div class="autofill-list"></div>';
  766.  
  767. // Field errors
  768. $errs = self::getFieldErrors($name);
  769. if (!empty($errs)) {
  770. $out .= '<div class="field-error">';
  771. $out .= '<ul class="field-error__list">';
  772. foreach ($errs as $err) {
  773. $out .= '<li class="field-error__list__item">' . Enc::html($err) . '</li>';
  774. }
  775. $out .= '</ul>';
  776. $out .= '</div>';
  777. }
  778.  
  779. $out .= '</div>';
  780. $out .= PHP_EOL . PHP_EOL;
  781.  
  782. return $out;
  783. }
  784. }
  785.  
  786.