SproutCMS

This is the code documentation for the SproutCMS project

source of /sprout/Helpers/Fb.php

  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.  
  14. namespace Sprout\Helpers;
  15.  
  16. use Exception;
  17. use InvalidArgumentException;
  18.  
  19. use karmabunny\pdb\Exceptions\RowMissingException;
  20. use Sprout\Helpers\Locales\LocaleInfo;
  21.  
  22.  
  23. /**
  24. * Quick and easy form builder
  25. **/
  26. class Fb
  27. {
  28. /** ID for use on the field label and input/select/etc. element */
  29. public static $field_id = '';
  30.  
  31. /** Extra class(es) for use on div.field-wrapper, div.label, and div.field to support additional styling.
  32.   N.B. If this is set, all inputs will be wrapper in a div.field */
  33. public static $include_class = '';
  34.  
  35. public static $data = [];
  36. public static $scope = 'admin';
  37. public static $dropdown_top = 'Select an option';
  38.  
  39. /**
  40.   * @var string A prefix for generated IDs
  41.   */
  42. public static $id_prefix = '';
  43.  
  44.  
  45. /**
  46.   * Sets the data that is used for form-building
  47.   * This is typically from a database row or saved session data
  48.   * To set a single field, instead use {@see Fb::setFieldValue}
  49.   *
  50.   * @param array $data Field name => value pairs
  51.   * @return void
  52.   */
  53. public static function setData($data)
  54. {
  55. self::$data = $data;
  56. }
  57.  
  58. /**
  59.   * Sets the value for a single field
  60.   * This is the non-array version of {@see Fb::setData}
  61.   *
  62.   * @param array $field Field name, e.g. 'first_name'
  63.   * @param array $value Field value, e.g. 'John'
  64.   * @return void
  65.   */
  66. public static function setFieldValue($field, $value)
  67. {
  68. self::$data[$field] = $value;
  69. }
  70.  
  71.  
  72. /**
  73.   * Sets the text for the top item of dropdown lists.
  74.   * Set to an empty string to not show the top item.
  75.   * This will be reset to the default value after every call to {@see Fb::dropdown}
  76.   * @deprecated Set the special attribute "-dropdown-top" when calling the dropdowns
  77.   * @param string $label The data to put in the first OPTION, with its value being an empty string
  78.   * @return void
  79.   */
  80. public static function setDropdownTop($label)
  81. {
  82. self::$dropdown_top = $label;
  83. }
  84.  
  85.  
  86. /**
  87.   * Gets the data for a single field from the data array
  88.   *
  89.   * Properly handles PHP sub-arrays (e.g. 'options[food]'), doesn't handle
  90.   * anon arrays though (e.g. 'options[]').
  91.   * @param string $name The field name
  92.   * @return mixed The value (often a string)
  93.   */
  94. public static function getData($name)
  95. {
  96. if (strpos($name, '[') === false) {
  97. return @self::$data[$name];
  98. }
  99.  
  100. // Get a list of keys
  101. $keys = explode('[', $name);
  102. foreach ($keys as &$key) {
  103. $key = trim($key, ']');
  104. if ($key == '') return ''; // anon arrays aren't supported
  105. }
  106.  
  107. // Loop through the keys till we get the value we want
  108. $v = self::$data;
  109. foreach ($keys as $k) {
  110. $v = @$v[$k];
  111. }
  112.  
  113. return $v;
  114. }
  115.  
  116.  
  117. /**
  118.   * Generates a heading using a H3 tag
  119.   * @param string $heading
  120.   * @return string H3 element
  121.   */
  122. public static function heading($heading)
  123. {
  124. return '<h3>' . Enc::html($heading) . '</h3>';
  125. }
  126.  
  127.  
  128. /**
  129.   * Generate a unique id
  130.   * @return string 'fb?', where ? is an incrementing number starting at zero
  131.   */
  132. private static function genId()
  133. {
  134. static $inc = 0;
  135.  
  136. return static::$id_prefix . 'fb' . $inc++;
  137. }
  138.  
  139.  
  140. /**
  141.   * Injects the current auto-generated id into an array of attributes.
  142.   * Only applies if an auto-generated id exists and an id isn't already set in the attributes.
  143.   * The auto-generated id is then cleared.
  144.   * @param array $attrs The attributes
  145.   * @return void
  146.   */
  147. protected static function injectId(array &$attrs)
  148. {
  149. if (isset($attrs['id'])) return;
  150. if (!self::$field_id) return;
  151. $attrs['id'] = self::$field_id;
  152. self::$field_id = '';
  153. }
  154.  
  155.  
  156. /**
  157.   * Adds an HTML attribute to the list of attributes.
  158.   * If the attribute has already been set, it will be left alone.
  159.   * N.B. the 'class' attribute is always appended
  160.   * @param array $attrs The list of attributes to modify
  161.   * @param string $name The name of the attribute, e.g. 'style'
  162.   * @param string $value The value of the attribute, e.g. 'display: none;'
  163.   * @return void
  164.   */
  165. protected static function addAttr(array &$attrs, $name, $value)
  166. {
  167. if (isset($attrs[$name])) {
  168. if ($name == 'class') {
  169. if ($attrs['class'] and $value != '') $attrs['class'] .= ' ';
  170. $attrs['class'] .= $value;
  171. }
  172. return;
  173. }
  174. $attrs[$name] = $value;
  175. }
  176.  
  177.  
  178. /**
  179.   * Generates an HTML opening tag, and possibly its closing tag, depending on the params specified
  180.   *
  181.   * You can specify either HTML or plain-text content, but not both
  182.   *
  183.   * @param string $name The name of the tag, e.g. 'textarea'
  184.   * @param array $attrs Attributes for the tag, e.g. ['class' => 'super-input', 'style' => 'font-style: italic']
  185.   * @param string $params Additional options, as follows:
  186.   * - 'html' (string): Specifies HTML content between the opening and closing tags, which MUST be
  187.   * properly encoded.
  188.   * - 'plain' (string): Specifies non-encoded content content between the opening and closing tags.
  189.   * @return string The generated tag
  190.   */
  191. public static function tag($name, array $attrs = [], array $params = [])
  192. {
  193. $tag = '<' . Enc::html($name);
  194. foreach ($attrs as $attr => $val) {
  195. // Support boolean attributes
  196. if (is_int($attr)) $attr = $val;
  197.  
  198. $tag .= ' ' . Enc::html($attr) . '="' . Enc::html($val) . '"';
  199. }
  200. $tag .= '>';
  201.  
  202. $close = false;
  203. if (array_key_exists('html', $params)) {
  204. $tag .= $params['html'];
  205. $close = true;
  206. } elseif (array_key_exists('plain', $params)) {
  207. $tag .= Enc::html($params['plain']);
  208. $close = true;
  209. }
  210.  
  211. if ($close) {
  212. $tag .= '</' . Enc::html($name) . '>';
  213. }
  214.  
  215. return $tag;
  216. }
  217.  
  218.  
  219. /**
  220.   * Generates an HTML INPUT tag using {@see Fb::tag}, with auto-determined value
  221.   *
  222.   * @param string $type The type of input, e.g. 'text', 'hidden', ...
  223.   * @param string $name The name of the input
  224.   * @param array $attrs Attributes for the tag
  225.   * @return string INPUT element
  226.   * @throws InvalidArgumentException if $name is empty
  227.   */
  228. public static function input($type, $name, array $attrs = [])
  229. {
  230. if (empty($name)) {
  231. throw new InvalidArgumentException('An INPUT without a name is invalid');
  232. }
  233.  
  234. $attrs['type'] = $type;
  235. $attrs['name'] = $name;
  236. if ($type != 'file') {
  237. $attrs['value'] = self::getData($name);
  238. }
  239. return self::tag('input', $attrs);
  240. }
  241.  
  242.  
  243. /**
  244.   * Outputs the value of the field directly, in a span
  245.   * @param array $attrs Extra attributes for the input field
  246.   * @return string SPAN element
  247.   */
  248. public static function output($name, array $attrs = [])
  249. {
  250. $value = self::getData($name);
  251. self::addAttr($attrs, 'class', 'field-output');
  252. self::addAttr($attrs, 'name', $name);
  253. return self::tag('span', $attrs, ['plain' => $value]);
  254. }
  255.  
  256.  
  257. /**
  258.   * Generates a text field
  259.   * @todo Use a generic method to generate the INPUT tag and its attributes
  260.   * @param string $name The name of the input field
  261.   * @param array $attrs Extra attributes for the input field
  262.   * @return string INPUT element
  263.   */
  264. public static function text($name, array $attrs = [])
  265. {
  266. self::injectId($attrs);
  267. self::addAttr($attrs, 'class', 'textbox');
  268. return self::input('text', $name, $attrs);
  269. }
  270.  
  271.  
  272. /**
  273.   * Shows a HTML5 number field
  274.   *
  275.   * @param string $name The name of the input field
  276.   * @param array $attrs Extra attributes for the input field; 'min' and 'max' being particularly relevant
  277.   * @return string INPUT element
  278.   */
  279. public static function number($name, array $attrs = [])
  280. {
  281. self::injectId($attrs);
  282. self::addAttr($attrs, 'class', 'textbox');
  283. return self::input('number', $name, $attrs);
  284. }
  285.  
  286.  
  287. /**
  288.   * Shows a HTML5 number field, formatted for dollar prices
  289.   *
  290.   * @param string $name The name of the input field
  291.   * @param array $attrs Extra attributes for the input field; 'min' and 'max' being particularly relevant
  292.   * @return string INPUT element
  293.   */
  294. public static function money($name, array $attrs = [], array $options = [])
  295. {
  296. self::injectId($attrs);
  297. self::addAttr($attrs, 'class', 'textbox');
  298.  
  299. if(!array_key_exists('min', $attrs)){
  300. $attrs["min"] = "0";
  301. }
  302. if(!array_key_exists('step', $attrs)){
  303. $attrs["step"] = "0.01";
  304. }
  305.  
  306. if (isset($options['locale'])) {
  307. $locale = LocaleInfo::get($options['locale']);
  308. } else {
  309. $locale = LocaleInfo::auto();
  310. }
  311.  
  312. $out = '<div class="money-symbol money-symbol--' . Enc::id(strtolower($locale->getCurrencyName())) . '">';
  313. $out .= self::input('number', $name, $attrs);
  314. $out .= '</div>';
  315.  
  316. return $out;
  317. }
  318.  
  319.  
  320. /**
  321.   * Generates a HTML5 range field
  322.   * @param string $name Field name
  323.   * @param array $attrs Extra attributes for the INPUT element. A range element takes these attributes:
  324.   * 'min' The minimum value, default 0, set to NULL for no limit
  325.   * 'max' The maximum value, default 100, set to NULL for no limit
  326.   * 'step' Difference between each grade on the range
  327.   * @param array $options Ignored
  328.   * @return string HTML with elements including INPUT and SCRIPT
  329.   */
  330. public static function range($name, array $attrs = [], array $options = [])
  331. {
  332. self::injectId($attrs);
  333. self::addAttr($attrs, 'class', 'textbox');
  334. if (!isset($attrs['min'])) $attrs['min'] = 0;
  335. if (!isset($attrs['max'])) $attrs['max'] = 100;
  336.  
  337. if ($attrs['min'] === null) unset($attrs['min']);
  338. if ($attrs['max'] === null) unset($attrs['max']);
  339. if (empty($attrs['step'])) unset($attrs['step']);
  340.  
  341. $out = self::input('range', $name, $attrs);
  342.  
  343. $id = Enc::id($attrs['id']);
  344. $value = (float) Fb::getData($name);
  345. $div = "<div id=\"{$id}-count\">{$value}</div>";
  346. $out .= "<script type=\"text/javascript\">
  347. $(document).ready(function() {
  348. $(\"#{$id}\").after('{$div}');
  349. $(\"#{$id}-count\").text($(\"#{$id}\").val());
  350. $(\"#{$id}\").bind('change click', function() {
  351. $(\"#{$id}-count\").text($(this).val());
  352. });
  353. });
  354. </script>";
  355.  
  356. return $out;
  357. }
  358.  
  359.  
  360. /**
  361.   * Generates a password field
  362.   * @param string $name The name of the input field
  363.   * @param array $attrs Extra attributes for the input field
  364.   * @return string INPUT element
  365.   */
  366. public static function password($name, $attrs = [])
  367. {
  368. self::injectId($attrs);
  369. self::addAttr($attrs, 'class', 'textbox password');
  370. return self::input('password', $name, $attrs);
  371. }
  372.  
  373.  
  374. /**
  375.   * Generates a file upload field
  376.   * @param string $name The name of the input field
  377.   * @param array $attrs Extra attributes for the input field
  378.   * @return string INPUT element
  379.   */
  380. public static function upload($name, array $attrs = [])
  381. {
  382. self::injectId($attrs);
  383. self::addAttr($attrs, 'class', 'upload');
  384. return self::input('file', $name, $attrs);
  385. }
  386.  
  387.  
  388. /**
  389.   * Generates a file upload field with a progress bar
  390.   *
  391.   * To easily save the uploaded files in the form action function, see {@see File::replaceSet}
  392.   *
  393.   * @param string $name
  394.   * @param array $attrs
  395.   * @param array $params Must have 'sess_key' => session key, e.g. 'user-register'.
  396.   * Data regarding each uploaded file will typically be saved in
  397.   * $_SESSION['file_uploads'][$params['sess_key']][$name].
  398.   *
  399.   * May also have 'opts' which can contain any of the following:
  400.   * - 'begin_url' (string)
  401.   * - 'form_url' (string)
  402.   * - 'done_url' (string)
  403.   * - 'cancel_url' (string)
  404.   * - 'form_params' (array):
  405.   * - form_id (string)
  406.   * - field_name (string)
  407.   *
  408.   * May also specify 'multiple', which is a positive int (default: 1).
  409.   * If more than 1, multiple files are allowed, up to the number specified.
  410.   */
  411. public static function chunkedUpload($name, array $attrs = [], array $params = [])
  412. {
  413. Needs::fileGroup('fb');
  414. Needs::fileGroup('drag_drop_upload');
  415.  
  416. self::injectId($attrs);
  417.  
  418. $max_files = (int) @$params['multiple'];
  419. if ($max_files < 1) $max_files = 1;
  420.  
  421. $default_opts = [
  422. 'begin_url' => 'file_upload/upload_begin',
  423. 'form_url' => 'file_upload/upload_form',
  424. 'chunk_url' => 'file_upload/upload_chunk',
  425. 'done_url' => 'file_upload/upload_done',
  426. 'cancel_url' => 'file_upload/upload_cancel',
  427. 'form_params' => [
  428. 'form_id' => $params['sess_key'],
  429. 'field_name' => $name,
  430. ],
  431. 'max_files' => $max_files,
  432. ];
  433.  
  434. // Override default opts with any specified via $params
  435. if (!empty($params['opts'])) {
  436. $opts = $params['opts'];
  437. foreach ($default_opts as $key => $val) {
  438. if (empty($opts[$key])) {
  439. $opts[$key] = $default_opts[$key];
  440. }
  441. }
  442. } else {
  443. $opts = $default_opts;
  444. }
  445.  
  446. $out = '<div class="fb-chunked-upload" data-opts="' . Enc::html(json_encode($opts)) . '">';
  447.  
  448. $upload_params = ['class' => 'file-upload__input', 'id' => $attrs['id']];
  449. if ($opts['max_files'] > 1) $upload_params['multiple'] = 'multiple';
  450. $out .= self::upload($name . '_upload', $upload_params);
  451.  
  452. $out .= '<div class="file-upload__area textbox">';
  453.  
  454. $files = ($opts['max_files'] == 1 ? 'file' : 'files');
  455. $out .= '<div class="file-upload__helptext">';
  456. $out .= "<p>Drop {$files} here ";
  457. $out .= '<span class="file-upload__helptext__line2">or click to upload</span></p>';
  458. $out .= '</div>';
  459.  
  460.  
  461. $out .= '<div class="file-upload__uploads">';
  462.  
  463. // Show uploaded file(s) if there's uploaded file data in the session
  464. // Otherwise it just gets thrown away if there's a form error
  465. $friendly_vals = self::getData($name);
  466. $temp_vals = self::getData($name . '_temp');
  467.  
  468. $files = [];
  469. if (is_array($friendly_vals) and is_array($temp_vals)) {
  470. // Zip!
  471. $vals = array_map(null, $friendly_vals, $temp_vals);
  472. foreach ($vals as $item) {
  473. list($friendly, $temp) = $item;
  474.  
  475. $temp = preg_replace('/[^a-z0-9_\-\.]/i', '', $temp);
  476. if (!$friendly or !$temp) continue;
  477.  
  478. $temp_path = APPPATH . 'temp/' . $temp;
  479. if (!file_exists($temp_path)) continue;
  480.  
  481. $temp_parts = explode('-', $temp, 3);
  482. $files[] = [
  483. 'original' => $friendly,
  484. 'temp' => $temp,
  485. 'code' => preg_replace('/\.dat$/', '', $temp_parts[2]),
  486. ];
  487. }
  488. }
  489.  
  490. if (is_string($friendly_vals)) {
  491. $files[] = $friendly_vals;
  492. $out .= '<input class="js-delete-notify" type="hidden" name="' . Enc::html($name) . '_deleted">';
  493. }
  494. foreach ($files as $file) {
  495. // Temp uploaded files stored in session
  496. if (is_array($file)) {
  497. $temp_path = APPPATH . 'temp/' . $file['temp'];
  498. $view = new View('sprout/file_confirm');
  499. $view->orig_file = ['name' => $file['original'], 'size' => filesize($temp_path)];
  500. $type = File::getType($file['original']);
  501.  
  502. // Existing file stored on disk
  503. } else if ($file) {
  504. $temp_path = DOCROOT . 'files/' . $file;
  505. $view = new View('sprout/file_confirm');
  506. $view->orig_file = ['name' => 'Existing file', 'size' => filesize($temp_path)];
  507. $type = File::getType($temp_path);
  508. } else {
  509. continue;
  510. }
  511.  
  512. if ($type == FileConstants::TYPE_IMAGE) {
  513. try {
  514. $view->shrunk_img = File::base64Thumb($temp_path, 200, 200);
  515. } catch (Exception $ex) {
  516. $view->image_too_large = true;
  517. }
  518. }
  519.  
  520. $out .= '<div class="file-upload__item"';
  521. if (!empty($file['code'])) $out .= ' data-code="' . Enc::html($file['code']) . '"';
  522. $out .= '>';
  523. $out .= $view->render();
  524. $out .= '</div>';
  525. }
  526.  
  527. // Don't try and save an existing file which is already on disk
  528. if (is_string($friendly_vals)) {
  529. $files = [];
  530. }
  531.  
  532. $out .= '</div>'; // .file-upload__uploads
  533. $out .= '</div>'; // .file-upload__area
  534.  
  535. $out .= '<div class="file-upload__data">';
  536. foreach ($files as $file) {
  537. $out .= '<input type="hidden" name="' . Enc::html($name) . '[]" class="original" value="' . Enc::html($file['original']) . '" data-code="' . Enc::html($file['code']) . '">';
  538. $out .= '<input type="hidden" name="' . Enc::html($name) . '_temp[]" class="temp" value="' . Enc::html($file['temp']) . '" data-code="' . Enc::html($file['code']) . '">';
  539. }
  540. $out .= '</div>'; // .file-upload__data
  541.  
  542. $out .= '</div>'; // .fb-chunked-upload
  543. return $out;
  544. }
  545.  
  546.  
  547. /**
  548.   * Renders a HTML5 email field
  549.   * @param string $name Field name
  550.   * @param array $attrs Extra attributes for the INPUT element
  551.   * @param array $options Ignored
  552.   * @return string
  553.   */
  554. public static function email($name, array $attrs = [], array $options = [])
  555. {
  556. self::injectId($attrs);
  557. self::addAttr($attrs, 'class', 'textbox email');
  558.  
  559. return self::input('email', $name, $attrs);
  560. }
  561.  
  562. /**
  563.   * Renders a HTML5 phone number field (type=tel)
  564.   * @param string $name Field name
  565.   * @param array $attrs Extra attributes for the INPUT element
  566.   * @param array $params Ignored
  567.   * @return string INPUT element
  568.   */
  569. public static function phone($name, array $attrs = [], array $params = [])
  570. {
  571. self::injectId($attrs);
  572. Fb::addAttr($attrs, 'class', 'textbox phone');
  573.  
  574. return self::input('tel', $name, $attrs);
  575. }
  576.  
  577. /**
  578.   * Render the UI for our multi-type link fields
  579.   *
  580.   * @wrap-in-fieldset
  581.   * @param string $name The name of the field
  582.   * @param array $attrs Unused
  583.   * @param array $options Includes the following:
  584.   * 'required': (bool) true if the field is required
  585.   * @return string HTML
  586.   */
  587. public static function lnk($name, array $attrs = [], array $options = [])
  588. {
  589. Needs::fileGroup('fb');
  590.  
  591. $value = self::getData($name);
  592. return Lnk::editform($name, $value, !empty($options['required']));
  593. }
  594.  
  595.  
  596. /**
  597.   * A file selection field, for use in the admin only.
  598.   *
  599.   * @param string $name Field name
  600.   * @param array $attrs Unused.
  601.   * @param array $options Includes the following:
  602.   * 'filter': (int) One of the filters, e.g. {@see FileConstants}::TYPE_IMAGE
  603.   * 'required': (bool) true if the field is required
  604.   * 'req_category': (int) Category field required. Default of 1: required. 0: not required
  605.   * @return string HTML
  606.   */
  607. public static function fileSelector($name, array $attrs = [], array $options = [])
  608. {
  609. Needs::fileGroup('fb');
  610.  
  611. $value = self::getData($name);
  612.  
  613. $options['filter'] = (int) @$options['filter'];
  614. $options['required'] = (bool) @$options['required'];
  615. $options['req_category'] = isset($options['req_category']) ? (int) $options['req_category'] : 1;
  616.  
  617. $classes = ['fb-file-selector', 'fs', '-clearfix'];
  618. if ($value) {
  619. $classes[] = 'fs-file-selected';
  620. }
  621. $classes = implode(' ', $classes);
  622.  
  623. $filename = '';
  624. if (preg_match('/^[0-9]+$/', $value)) {
  625. try {
  626. $filename = Pdb::q("SELECT filename FROM ~files WHERE id = ?", [$value], 'val');
  627. } catch (RowMissingException $ex) {
  628. }
  629. }
  630.  
  631. $out = '<span class="' . Enc::html($classes) . '" data-filter="' . $options['filter'] . '"';
  632. $out .= ' data-init="false" data-filename="' . Enc::html($filename) . '" data-req-category="' . Enc::html($options['req_category']) . '">';
  633. $out .= '<button type="button" class="fs-select-button button button-blue popup-button icon-after icon-insert_drive_file">Select a file</button>';
  634. $out .= '<input class="fs-hidden" type="hidden" name="' . Enc::html($name) . '" value="' . Enc::html($value) . '">';
  635.  
  636. $out .= '<span class="fs-preview-wrapper">';
  637.  
  638. if ($options['filter'] == FileConstants::TYPE_IMAGE or strpos(File::mimetype($value), 'image/') === 0) {
  639. $out .= '<span class="fs-preview">';
  640. if ($value) {
  641. $out .= '<img src="' . Enc::html(File::resizeUrl($value, 'c50x50')) . '" alt="">';
  642. }
  643. $out .= '</span>';
  644. }
  645.  
  646. $out .= '<span class="fs-filename">';
  647. $out .= ($value ? Enc::html($value) : 'No file selected');
  648. $out .= '</span>';
  649.  
  650. if (!$options['required']) {
  651. $out .= '<button class="fs-remove" type="button"><span class="-vis-hidden">Remove</span></button>';
  652. }
  653.  
  654. $out .= '</span>'; // preview wrapper
  655. $out .= '</span>'; // outer wrap
  656.  
  657. return $out;
  658. }
  659.  
  660.  
  661. /**
  662.   * Generates a richtext field - i.e. TinyMCE
  663.   *
  664.   * @wrap-in-fieldset
  665.   * @param string $name The field name for this richtext field.
  666.   * @param array $attrs Including 'height' and 'width' in pixels
  667.   * @param array $items Specify 'type' for RichText, e.g. 'TinyMCE4', or 'TinyMCE4:Lite'
  668.   * @return string HTML containing a TEXTAREA element and an associated SCRIPT element which to converts it
  669.   * into a richtext field
  670.   */
  671. public static function richtext($name, array $attrs = [], array $items = [])
  672. {
  673. $value = self::getData($name);
  674.  
  675. $width = (int) @$attrs['width'];
  676. if ($width <= 0) $width = 600;
  677.  
  678. $height = (int) @$attrs['height'];
  679. if ($height <= 0) $height = 400;
  680.  
  681. return RichText::draw($name, $value, $width, $height, @$items['type']);
  682. }
  683.  
  684. /**
  685.   * Generates a multiline text field
  686.   * @param string $name The field name for this field.
  687.   * @param array $attrs Extra attributes for the TEXTAREA element
  688.   * @return string TEXTAREA element
  689.   */
  690. public static function multiline($name, array $attrs = [])
  691. {
  692. self::injectId($attrs);
  693. self::addAttr($attrs, 'class', 'textbox multiline');
  694. $attrs['name'] = $name;
  695. return self::tag('textarea', $attrs, ['plain' => self::getData($name)]);
  696. }
  697.  
  698.  
  699. /**
  700.   * Generates a dropdown selection menu
  701.   * @todo Use a generic method to generate the SELECT tag and its attributes
  702.   * @param string $name The field name
  703.   * @param array $attrs Extra attributes for the SELECT element
  704.   * The special attribute "-dropdown-top" sets the label for the top item
  705.   * Use an empty string for no top item
  706.   * @param array $options Data for the OPTION elements in value-label pairs,
  707.   * e.g. [0 => 'Inactive', 1 => 'Active']
  708.   * @return string
  709.   */
  710. public static function dropdown($name, array $attrs = [], array $options = [])
  711. {
  712. if (isset($attrs['-dropdown-top'])) {
  713. self::$dropdown_top = $attrs['-dropdown-top'];
  714. unset($attrs['-dropdown-top']);
  715. }
  716.  
  717. $is_multi = false;
  718. foreach ($attrs as $key => $val) {
  719. if (strcasecmp($key, 'multiple') == 0) {
  720. $is_multi = true;
  721. break;
  722. }
  723. if (is_int($key) and strcasecmp($val, 'multiple') == 0) {
  724. $is_multi = true;
  725. break;
  726. }
  727. }
  728.  
  729. self::injectId($attrs);
  730. $value = self::getData($name);
  731. $extra = self::addAttr($attrs, 'class', 'dropdown');
  732.  
  733. if ($is_multi and substr($name, -2) != '[]') {
  734. $name .= '[]';
  735. }
  736.  
  737. $attrs['name'] = $name;
  738. $field = self::tag('select', $attrs);
  739.  
  740. if (self::$dropdown_top and !$is_multi) {
  741. $field .= '<option value="" class="dropdown-top">';
  742. $field .= Enc::html(self::$dropdown_top) . '</option>';
  743. }
  744.  
  745. $field .= self::dropdownItems($options, $value);
  746.  
  747. $field .= '</select> ';
  748.  
  749. // Revert to default top dropdown item
  750. self::$dropdown_top = 'Select an option';
  751.  
  752. return $field;
  753. }
  754.  
  755.  
  756. /**
  757.   * Returns HTML for a list of OPTIONs, and depending on the input array, OPTGROUP tags.
  758.   * @param array $options The options. Any element that is an array will become an optgroup, with its inner elements
  759.   * becoming options.
  760.   * @param string|array $selected The value of the selected option
  761.   * @return string
  762.   */
  763. public static function dropdownItems(array $options, $selected = null)
  764. {
  765. $out = '';
  766. foreach ($options as $val => $label) {
  767. $val_enc = Enc::html($val);
  768.  
  769. if (is_array($label)) {
  770. $out .= "<optgroup label=\"{$val_enc}\">";
  771. $out .= self::dropdownItems($label, $selected);
  772. $out .= "</optgroup>";
  773.  
  774. } else {
  775. $label = Enc::html($label);
  776. $label = str_replace(' ', '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;', $label);
  777.  
  778. if ($val == $selected) {
  779. $out .= "<option value=\"{$val_enc}\" selected>{$label}</option>";
  780. } else if (is_array($selected) and in_array($val, $selected)) {
  781. $out .= "<option value=\"{$val_enc}\" selected>{$label}</option>";
  782. } else {
  783. $out .= "<option value=\"{$val_enc}\">{$label}</option>";
  784. }
  785. }
  786. }
  787. return $out;
  788. }
  789.  
  790.  
  791. /**
  792.   * Returns HTML for an autocomplete selection menu.
  793.   * Expects to be provided AJAX data in a format defined by jQuery UI (see 'url' option below).
  794.   * By default, this expects to stores an ID value for a foreign key. If this isn't the desired
  795.   * behaviour, a plain text value can be stored by setting the 'save_id' option to false.
  796.   *
  797.   * The URL which handles the lookups needs to use the 'term' GET param to find matching values.
  798.   * If 'save_id' is true, then it also needs to accept the 'id' GET param to fetch the label for the relevant id.
  799.   * A call to the URL should return JSON which contains an array of hashes, each with the following keys:
  800.   * - 'value': data for the text input.
  801.   * - 'label': data for display in the drop-down list (if different from value).
  802.   * This is used as the value if 'value' isn't specified.
  803.   * - 'id': id value to save. This should only be specified if 'save_id' is true; see below.
  804.   *
  805.   * @param string $name The field name
  806.   * @param array $attrs Extra attributes for the INPUT element
  807.   * 'data-callback' Function name to call once a selection has been made, which should accept a param of {int} item_id
  808.   * @param array $options Keys as follows:
  809.   * 'url' (string, required) URL to access when fetching matches via AJAX.
  810.   * 'save_id' (bool, defaults to true) Save the data as an ID value or similar unique key, and look up the
  811.   * label for display upon page by calling the URL with the 'id' GET param set
  812.   * 'multiline' (bool, defaults to false) Use a textarea to support multiline text
  813.   * 'chars' (int, defaults to 2) Minimum number of characters required before first AJAX lookup fires.
  814.   * If zero, the lookup will happen on focus.
  815.   * @return string An INPUT element and associated SCRIPT element
  816.   */
  817. public static function autocomplete($name, array $attrs = [], array $options = [])
  818. {
  819. Needs::fileGroup('fb');
  820.  
  821. if (empty($options['url'])) {
  822. throw new InvalidArgumentException("\$options['url'] must be specified");
  823. }
  824. if (!isset($options['chars']) or !is_numeric($options['chars'])) {
  825. $chars = 2;
  826. } else {
  827. $chars = (int) $options['chars'];
  828. if ($chars < 0) $chars = 0;
  829. }
  830.  
  831. self::injectId($attrs);
  832. self::addAttr($attrs, 'class', 'textbox');
  833. self::addAttr($attrs, 'class', 'autocomplete');
  834. self::addAttr($attrs, 'data-lookup-url', $options['url']);
  835. self::addAttr($attrs, 'data-chars', $chars);
  836.  
  837. // Automatically add a hidden field with the 'id' value if available. This is probably the most
  838. // desirable behaviour, as an autocomplete field is usually for a Foreign Key column
  839. if (!array_key_exists('save_id', $options)) $options['save_id'] = true;
  840. $attrs['data-save-id'] = (int) (bool) $options['save_id'];
  841.  
  842. if (!empty($options['multiline'])) {
  843. $attrs['name'] = $name;
  844. $input = self::tag('textarea', $attrs, ['plain' => self::getData($name)]);
  845. } else {
  846. $input = self::input('text', $name, $attrs);
  847. }
  848.  
  849. return '<div class="autocomplete-symbol">' . $input . '</div>';
  850. }
  851.  
  852.  
  853. /**
  854.   * Render autocomplete multi-list
  855.   *
  856.   * @param string $name Form field name
  857.   * @param array $attrs Extra attributes for the INPUT element
  858.   * 'data-callback' (string )Function name to call once a selection has been made,
  859.   * which should accept the same params as endpoint response listed below.
  860.   *
  861.   * @param array $options Keys as follows:
  862.   * 'url' (string) ajax called endpoint to populate autocomplete list
  863.   * Endpoint to acccept:
  864.   * $_GET['q'] (string) Search keyword
  865.   * $_GET['ids'] (string) CSV of record IDs to poplate on page-load
  866.   * Endpoint to return (json):
  867.   * 'id' (int) Record ID
  868.   * 'value' (string) Record label
  869.   *
  870.   * 'chars' (int, defaults to 2) Minimum number of characters required before first AJAX lookup fires.
  871.   * If zero, the lookup will happen on focus.
  872.   *
  873.   * @return string A HTML INPUT element and associated SCRIPT element
  874.   */
  875. public static function autocompleteList($name, array $attrs = [], array $options = [])
  876. {
  877. Needs::fileGroup('fb');
  878.  
  879. if (empty($options['limit'])) $options['limit'] = 0;
  880. if (empty($options['reorder'])) $options['reorder'] = false;
  881. if (empty($options['title'])) $options['title'] = 'Items';
  882. if (empty($options['chars']) or !is_numeric($options['chars']) or $options['chars'] < 0) $options['chars'] = 2;
  883.  
  884. self::injectId($attrs);
  885. self::addAttr($attrs, 'class', 'textbox');
  886. self::addAttr($attrs, 'class', 'autocomplete-list');
  887. self::addAttr($attrs, 'data-url', $options['url']);
  888. self::addAttr($attrs, 'data-chars', $options['chars']);
  889. self::addAttr($attrs, 'data-values', self::getData($name));
  890. self::addAttr($attrs, 'data-name', $name);
  891.  
  892. $view = new View('sprout/components/fb_autocomplete_list');
  893. $view->input = self::input('text', "{$name}_search", $attrs);
  894. $view->id = $attrs['id'];
  895.  
  896. return $view->render();
  897. }
  898.  
  899.  
  900. /**
  901.   * Returns HTML for a bunch of radiobuttons
  902.   *
  903.   * @wrap-in-fieldset
  904.   * @param string $name The field name
  905.   * @param array $attrs Attributes for all input elements, e.g. ['class' => 'super-input', 'style' => 'font-style: italic']
  906.   * @param array $options each is a value => label mapping, e.g. ['A' => 'Apple crumble', 'B' => 'Banana split']
  907.   * @return string HTML containing DIV tags containing INPUT and LABEL tags.
  908.   */
  909. public static function multiradio($name, array $attrs = [], array $options = [])
  910. {
  911. $value = self::getData($name);
  912.  
  913. $content = '';
  914. foreach ($options as $opt_value => $label) {
  915. $id = self::genId();
  916.  
  917. $content .= '<div class="fieldset-input">';
  918. $input_attrs = [
  919. 'type' => 'radio',
  920. 'id' => $id,
  921. 'name' => $name,
  922. 'value' => $opt_value,
  923. ];
  924. if ($opt_value == $value) $input_attrs['checked'] = 'checked';
  925.  
  926. $tag_attrs = array_merge($attrs, $input_attrs);
  927. $content .= self::tag('input', $tag_attrs);
  928.  
  929. $content .= "<label for=\"{$id}\">";
  930. $content .= Enc::html($label);
  931. $content .= "</label>";
  932. $content .= "</div>";
  933. }
  934.  
  935. return $content;
  936. }
  937.  
  938.  
  939. /**
  940.   * Returns HTML containing multiple boolean checkboxes
  941.   *
  942.   * @wrap-in-fieldset
  943.   * @param string $name Ignored; each checkbox specifies its own name in $settings
  944.   * @param array $attrs Unused but remains for compatibility with other methods
  945.   * @param array $settings Keys are the names of the checkbox fields, and values their labels.
  946.   * @return string HTML containing DIV tags containing INPUT and LABEL tags.
  947.   */
  948. public static function checkboxBoolList($name, array $attrs = [], array $settings = [])
  949. {
  950. $out = '';
  951.  
  952. foreach ($settings as $name => $label) {
  953. $selected = !empty(self::getData($name));
  954. $out .= self::checkbox($name, $label, 1, $selected, $attrs);
  955. }
  956.  
  957. return $out;
  958. }
  959.  
  960.  
  961. /**
  962.   * Returns HTML containing multiple checkboxes, with values to store in a SET column or similar
  963.   *
  964.   * @wrap-in-fieldset
  965.   * @param string $name Name for each INPUT element. Empty brackets will be appended if not supplied, i.e. [].
  966.   * @param array $attrs Attributes for all input elements, e.g. ['class' => 'super-input', 'style' => 'font-style: italic']
  967.   * @param array $settings Keys are the values available in the set, and values their labels. These can match, and
  968.   * can be easily filled by a call to {@see Pdb::extractEnumArr}
  969.   * @return string HTML containing DIV tags containing INPUT and LABEL tags.
  970.   *
  971.   * @example
  972.   * Form::nextFieldDetails('Colours to include', false);
  973.   * echo Form::checkboxSet('colours', [], [
  974.   * 'red' => 'Red, the colour of cherries and strawberries',
  975.   * 'green' => 'Green, the colour of leaves',
  976.   * 'blue' => 'Blue, the colour of the sky and ocean',
  977.   * ]);
  978.   */
  979. public static function checkboxSet($name, array $attrs = [], array $settings = [])
  980. {
  981. $out = '';
  982.  
  983. $selected = self::getData($name);
  984. if (!is_array($selected)) $selected = preg_split('/,\s*/', trim($selected));
  985.  
  986. if (substr($name, -2) != '[]') $name .= '[]';
  987. $id = Enc::id($name);
  988. $name = Enc::html($name);
  989.  
  990. foreach ($settings as $value => $label) {
  991. $is_selected = in_array($value, $selected);
  992. $out .= static::checkbox($name, $label, $value, $is_selected, $attrs);
  993. }
  994.  
  995. return $out;
  996. }
  997.  
  998. /**
  999.   * Returns the HTML for a single checkbox
  1000.   *
  1001.   * @note This typically isn't used directly; instead use @see Form::checkboxList,
  1002.   * @see Fb::checkboxBoolList, @see Fb::checkboxSet
  1003.   * @param string $name The name of the checkbox
  1004.   * @param string $label The label for the checkbox; supports minimal HTML, {@see Text::limitedSubsetHtml}
  1005.   * @param int|string $value The checkbox's value
  1006.   * @param bool $selected Whether or not the checkbox is ticked
  1007.   * @param array $attrs Extra attributes attached to the input tag
  1008.   */
  1009. protected static function checkbox($name, $label, $value, $selected, array $attrs = [])
  1010. {
  1011. $out = '';
  1012.  
  1013. $out .= '<div class="fieldset-input">';
  1014. $input_attrs = [
  1015. 'type' => 'checkbox',
  1016. 'id' => self::genId(),
  1017. 'name' => $name,
  1018. 'value' => $value,
  1019. ];
  1020.  
  1021. if ($selected) {
  1022. $input_attrs['checked'] = 'checked';
  1023. }
  1024.  
  1025. $tag_attrs = array_merge($attrs, $input_attrs);
  1026.  
  1027. $out .= self::tag('input', $tag_attrs);
  1028. $out .= self::tag('label', ['for' => $input_attrs['id']]);
  1029. $out .= Text::limitedSubsetHtml($label);
  1030. $out .= '</label>';
  1031. $out .= '</div>';
  1032.  
  1033. return $out;
  1034. }
  1035.  
  1036. /**
  1037.   * Generates a page dropdown selection menu
  1038.   *
  1039.   * Just a wrapper for {@see Fb::dropdownTree}
  1040.   *
  1041.   * @param string $name The field name
  1042.   * @param array $attrs Extra attributes for the SELECT element
  1043.   * @param array $options Zero or more field options;
  1044.   * 'subsite' int Subsite ID to load; default is current subsite
  1045.   * 'exclude' array Node IDs to exclude from rendering
  1046.   * @return string HTML
  1047.   */
  1048. public static function pageDropdown($name, array $attrs = [], array $options = [])
  1049. {
  1050. if (empty($options['subsite'])) {
  1051. $options['subsite'] = $_SESSION['admin']['active_subsite'];
  1052. }
  1053. $options['root'] = Navigation::loadPageTree($options['subsite'], true, false);
  1054. return self::dropdownTree($name, $attrs, $options);
  1055. }
  1056.  
  1057.  
  1058. /**
  1059.   * Generates a dropdown selection menu from a Treenode and its children
  1060.   *
  1061.   * @param string $name The field name
  1062.   * @param array $attrs Extra attributes for the SELECT element
  1063.   * The special attribute "-dropdown-top" sets the label for the top item
  1064.   * Use an empty string for no top item
  1065.   * @param array $options One or more field options;
  1066.   * 'root' Treenode Tree root - Required
  1067.   * 'exclude' array Node IDs to exclude from rendering
  1068.   * @return string HTML
  1069.   */
  1070. public static function dropdownTree($name, array $attrs = [], array $options = [])
  1071. {
  1072. if (isset($attrs['-dropdown-top'])) {
  1073. self::$dropdown_top = $attrs['-dropdown-top'];
  1074. unset($attrs['-dropdown-top']);
  1075. }
  1076.  
  1077. $value = self::getData($name);
  1078.  
  1079. if (empty($options['root']) or !($options['root'] instanceof Treenode)) {
  1080. throw new InvalidArgumentException('Option "root" is required and must be a Treenode');
  1081. }
  1082. if (empty($options['exclude'])) {
  1083. $options['exclude'] = [];
  1084. }
  1085.  
  1086. $attrs['name'] = $name;
  1087. $field = self::tag('select', $attrs);
  1088.  
  1089. if (self::$dropdown_top) {
  1090. $field .= '<option value="" class="dropdown-top">';
  1091. $field .= Enc::html(self::$dropdown_top) . '</option>';
  1092. }
  1093.  
  1094. foreach ($options['root']->children as $child) {
  1095. $field .= self::dropdownTreeItem($child, 0, $value, $options['exclude']);
  1096. }
  1097.  
  1098. $field .= '</select>';
  1099.  
  1100. // Revert to default top dropdown item
  1101. self::$dropdown_top = 'Select an option';
  1102.  
  1103. return $field;
  1104. }
  1105.  
  1106.  
  1107. /**
  1108.   * Used internally for recursive dropdown generation.
  1109.   *
  1110.   * @param Pagenode $node The node to display
  1111.   * @param int $depth The depth of the node
  1112.   * @param int $selected The id of the page to select
  1113.   * @param array $exclude Node IDs of the to exclude from the list
  1114.   * @return string HTML
  1115.   */
  1116. private static function dropdownTreeItem($node, $depth, $selected, $exclude)
  1117. {
  1118. $space = str_repeat('&nbsp;&nbsp;&nbsp;&nbsp;', $depth);
  1119. $name = Enc::html($node['name']);
  1120.  
  1121. if (in_array($node['id'], $exclude)) return '';
  1122.  
  1123. if ($node['id'] == $selected) {
  1124. $out = "<option value=\"{$node['id']}\" selected>{$space}{$name}</option>";
  1125. } else {
  1126. $out = "<option value=\"{$node['id']}\">{$space}{$name}</option>";
  1127. }
  1128.  
  1129. foreach ($node->children as $child) {
  1130. $out .= self::dropdownTreeItem($child, $depth + 1, $selected, $exclude);
  1131. }
  1132.  
  1133. return $out;
  1134. }
  1135.  
  1136.  
  1137. /**
  1138.   * Renders HTML containing a date selection UI. Output field value is in YYYY-MM-DD
  1139.   *
  1140.   * @todo Mobile to use a "date" field instead, for native UI
  1141.   * @throws ValidationException If 'min', 'max' or 'incr' options are invalid
  1142.   * @param string $name The field name
  1143.   * @param array $attrs Attributes for the input element, e.g. ['id' => 'my-timepicker', class' => 'super-input', 'style' => 'font-style: italic']
  1144.   * @param array $options Customisation options
  1145.   * 'min' => the earliest selectable date, format YYYY-MM-DD
  1146.   * 'max' => the latest selectable date, format YYYY-MM-DD
  1147.   * 'dropdowns' => bool, true to include dropdowns for the month and year
  1148.   * @return string HTML
  1149.   */
  1150. public static function datepicker($name, array $attrs = [], array $options = [])
  1151. {
  1152. Needs::fileGroup('moment');
  1153. Needs::fileGroup('daterangepicker');
  1154. Needs::fileGroup('fb');
  1155.  
  1156. $value = self::getData($name);
  1157. if ($value == '0000-00-00') $value = '';
  1158.  
  1159. if (isset($options['min'])) Validity::dateMySQL($options['min']);
  1160. if (isset($options['max'])) Validity::dateMySQL($options['max']);
  1161. if (!isset($attrs['autocomplete'])) $attrs['autocomplete'] = 'off';
  1162.  
  1163. self::injectId($attrs);
  1164. self::addAttr($attrs, 'class', 'textbox fb-datepicker');
  1165. self::addAttr($attrs, 'type', 'text');
  1166.  
  1167. foreach ($options as $key => $val) {
  1168. $attrs['data-' . $key] = $val;
  1169. }
  1170. $out = '<div class="field-clearable__wrap">';
  1171. $out .= self::tag('input', [
  1172. 'name' => $name, 'value' => $value, 'type' => 'hidden', 'class' => 'fb-hidden'
  1173. ]);
  1174. $out .= self::tag('input', $attrs);
  1175. $out .= self::tag('button', [
  1176. 'type' => 'button',
  1177. 'class' => 'field-clearable__clear fb-clear',
  1178. ]);
  1179. $out .= '</div>';
  1180.  
  1181. return $out;
  1182. }
  1183.  
  1184.  
  1185. /**
  1186.   * Renders a date range picker. Output is in the form of two fields (given in name as a comma separated list) e.g.
  1187.   * name => date_start, date_end will result in two fields: date_start => YYYY-MM-DD, date_end => YYYY-MM-DD
  1188.   *
  1189.   * @example
  1190.   * echo Fb::daterangepicker('date_from,date_to', [], ['min' => '2000-01-01']);
  1191.   *
  1192.   * @param string $name The field name prefix
  1193.   * @param array $attrs Attributes for the input element, e.g. ['class' => 'super-input', 'style' => 'font-style: italic']
  1194.   * @param array $options Customisation options
  1195.   * 'min' => the minimum of this date range.
  1196.   * 'max' => the maximum of this date range.
  1197.   * 'dropdowns' => display the dropdown date selectors.
  1198.   * 'dir' => Either "future" or "past", for the direction of the pre-configured ranges. Default "future"
  1199.   * @return string The rendered HTML
  1200.   */
  1201. public static function daterangepicker($name, array $attrs = [], array $options = [])
  1202. {
  1203. Needs::fileGroup('moment');
  1204. Needs::fileGroup('daterangepicker');
  1205. Needs::fileGroup('fb');
  1206.  
  1207. $names = explode(',', $name);
  1208.  
  1209. if (count($names) != 2) {
  1210. throw new InvalidArgumentException("daterangepicker expects name ({$name}) to be in the form of two comma-separated identifiers; e.g. 'date_start,date_end'");
  1211. }
  1212.  
  1213. list($name_start, $name_end) = $names;
  1214.  
  1215. if (!isset($options['dir'])) $options['dir'] = 'future';
  1216.  
  1217. if (isset($options['min'])) Validity::dateMySQL($options['min']);
  1218. if (isset($options['max'])) Validity::dateMySQL($options['max']);
  1219. if (!isset($attrs['autocomplete'])) $attrs['autocomplete'] = 'off';
  1220.  
  1221. self::injectId($attrs);
  1222. self::addAttr($attrs, 'class', 'textbox fb-daterangepicker');
  1223.  
  1224. foreach ($options as $key => $val) {
  1225. $attrs['data-' . $key] = $val;
  1226. }
  1227.  
  1228. $out = self::input('hidden', $name_start, ['class' => 'fb-hidden fb-daterangepicker--start']);
  1229. $out .= self::input('hidden', $name_end, ['class' => 'fb-hidden fb-daterangepicker--end']);
  1230. $out .= self::input('text', $name_start . '_to_' . $name_end . '_picker', $attrs);
  1231.  
  1232. return $out;
  1233. }
  1234.  
  1235.  
  1236. /**
  1237.   * Renders simplified date range picker,
  1238.   * Output is in the form of two fields (given in name as a comma separated list) e.g.
  1239.   * name => date_start, date_end will result in two fields: date_start => YYYY-MM-DD, date_end => YYYY-MM-DD
  1240.   *
  1241.   * @param string $name The field name prefix
  1242.   * @param array $attrs Attributes for the input element, e.g. ['class' => 'super-input', 'style' => 'font-style: italic']
  1243.   * 'data-callback' => 'myCallBack' JS function name to be called upon dates updated
  1244.   * Useage: myCallBack(date_from, date_to) { date_from = date_from.format('YYYY-M-D'); date_to = date_to.format('YYYY-M-D'); }
  1245.   * @param array $options Customisation options
  1246.   * 'min' => the minimum of this date range.
  1247.   * 'max' => the maximum of this date range.
  1248.   *
  1249.   * @return string The rendered HTML
  1250.   */
  1251. public static function simpledaterangepicker($name, array $attrs = [], array $options = [])
  1252. {
  1253. Needs::fileGroup('moment');
  1254. Needs::fileGroup('simpledaterangepicker');
  1255. Needs::fileGroup('fb');
  1256.  
  1257. $names = explode(',', $name);
  1258.  
  1259. if (count($names) != 2) {
  1260. throw new InvalidArgumentException("simpledaterangepicker expects name ({$name}) to be in the form of two comma-separated identifiers; e.g. 'date_start,date_end'");
  1261. }
  1262.  
  1263. list($name_start, $name_end) = $names;
  1264.  
  1265. if (isset($options['min'])) Validity::dateMySQL($options['min']);
  1266. if (isset($options['max'])) Validity::dateMySQL($options['max']);
  1267. if (!isset($attrs['autocomplete'])) $attrs['autocomplete'] = 'off';
  1268. if (!isset($attrs['data-callback'])) $attrs['data-callback'] = '';
  1269.  
  1270. self::injectId($attrs);
  1271. self::addAttr($attrs, 'class', 'textbox fb-simpledaterangepicker');
  1272.  
  1273. foreach ($options as $key => $val) {
  1274. if ($key != 'locale') {
  1275. $attrs['data-' . $key] = $val;
  1276. } else {
  1277. $attrs['data-locale'] = json_encode($options['locale']);
  1278. }
  1279. }
  1280.  
  1281. $out = self::input('hidden', $name_start, ['class' => 'fb-hidden fb-daterangepicker--start']);
  1282. $out .= self::input('hidden', $name_end, ['class' => 'fb-hidden fb-daterangepicker--end']);
  1283. $out .= self::input('text', $name_start . '_to_' . $name_end . '_picker', $attrs);
  1284.  
  1285. return $out;
  1286. }
  1287.  
  1288.  
  1289. /**
  1290.   * Renders a datetime range picker
  1291.   *
  1292.   * Output is in the form of two fields (given in name as a comma separated list) e.g.
  1293.   * $name = 'date_start,date_end' will result in two fields:
  1294.   * date_start => YYYY-MM-DD HH:MM:SS, date_end => YYYY-MM-DD HH:MM:SS
  1295.   *
  1296.   * @param string $name The field name prefix
  1297.   * @param array $attrs Attributes for the input element, e.g. ['class' => 'super-input', 'style' => 'font-style: italic']
  1298.   * @param array $options Additional options:
  1299.   * 'min' Minimum datetime in YYYY-MM-DD HH:MM:SS format
  1300.   * 'max' Maximum datetime in YYYY-MM-DD HH:MM:SS format
  1301.   * 'incr' Time increment in minutes. Default 1
  1302.   * 'dir' Either "future" or "past", for the direction of the pre-configured ranges. Default "future"
  1303.   * 'dropdowns' => display the dropdown date selectors.
  1304.   * @return string The rendered HTML
  1305.   */
  1306. public static function datetimerangepicker($name, array $attrs = [], array $options = [])
  1307. {
  1308. Needs::fileGroup('moment');
  1309. Needs::fileGroup('daterangepicker');
  1310. Needs::fileGroup('fb');
  1311.  
  1312. $names = explode(',', $name);
  1313. if (count($names) != 2) {
  1314. throw new InvalidArgumentException("datetimerangepicker expects name ({$name}) to be in the form of
  1315. two comma-separated identifiers; e.g. 'datetime_start,datetime_end'");
  1316. }
  1317.  
  1318. list($name_start, $name_end) = $names;
  1319.  
  1320. if (!isset($options['dir'])) $options['dir'] = 'future';
  1321.  
  1322. if (isset($options['min'])) Validity::dateTimeMySQL($options['min']);
  1323. if (isset($options['max'])) Validity::dateTimeMySQL($options['max']);
  1324. if (!isset($attrs['autocomplete'])) $attrs['autocomplete'] = 'off';
  1325.  
  1326. self::injectId($attrs);
  1327. self::addAttr($attrs, 'class', 'textbox fb-datetimerangepicker');
  1328.  
  1329. foreach ($options as $key => $val) {
  1330. $attrs['data-' . $key] = $val;
  1331. }
  1332.  
  1333. $out = self::input('hidden', $name_start, ['class' => 'fb-hidden fb-datetimerangepicker--start']);
  1334. $out .= self::input('hidden', $name_end, ['class' => 'fb-hidden fb-datetimerangepicker--end']);
  1335. $out .= self::input('text', $name_start . '_to_' . $name_end . '_picker', $attrs);
  1336.  
  1337. return $out;
  1338. }
  1339.  
  1340.  
  1341. /**
  1342.   * Renders a timepicker field inside a SPAN, which displays a dropdown date selection box when clicked
  1343.   *
  1344.   * @param string $name The name of the field
  1345.   * @param array $attrs Attributes for the input element, e.g. ['id' => 'my-timepicker', class' => 'super-input', 'style' => 'font-style: italic']
  1346.   * @param array $params Additional options:
  1347.   * 'min' Minimum allowed time in 24-hour format with a colon, e.g. '07:00' for 7am
  1348.   * 'max' Maximum allowed time in 24-hour format with a colon, e.g. '20:30' for 8:30pm
  1349.   * 'increment' Time increments e.g. 30 is 30 minute increments
  1350.   * @return string HTML
  1351.   */
  1352. public static function timepicker($name, array $attrs = [], array $params = [])
  1353. {
  1354. $value = self::getData($name);
  1355.  
  1356. if (!isset($params['min'])) $params['min'] = '00:00';
  1357. if (!isset($params['max'])) $params['max'] = '23:59';
  1358. if (!isset($params['increment'])) $params['increment'] = 30;
  1359. if (!isset($attrs['autocomplete'])) $attrs['autocomplete'] = 'off';
  1360. $params['increment'] = (int) $params['increment'];
  1361.  
  1362. self::injectId($attrs);
  1363. $id = Enc::id($attrs['id']);
  1364.  
  1365. Needs::fileGroup('fb');
  1366. Needs::fileGroup('date');
  1367. Needs::fileGroup('jquery.timepicker');
  1368.  
  1369. $out = "<span id=\"{$id}_wrap\" class=\"fb-timepicker\" data-config=\"" . Enc::html(json_encode($params)) . "\">";
  1370.  
  1371. self::addAttr($attrs, 'name', $name . '_widget');
  1372. self::addAttr($attrs, 'type', 'text');
  1373. self::addAttr($attrs, 'class', 'textbox timepicker tm');
  1374. self::addAttr($attrs, 'autocomplete', 'off');
  1375.  
  1376. $out .= self::tag('input', $attrs, []);
  1377. $out .= "<input type=\"hidden\" name=\"{$name}\" value=\"" . Enc::html($value) . "\" class=\"hid\">";
  1378. $out .= "</span>";
  1379.  
  1380. return $out;
  1381. }
  1382.  
  1383.  
  1384. /**
  1385.   * Renders HTML containing a date-time selection UI. Output field value is in YYYY-MM-DD HH:MM:SS
  1386.   *
  1387.   * @todo Mobile to use a "datetime-local" field instead, for native UI
  1388.   * @throws ValidationException If 'min', 'max' or 'incr' options are invalid
  1389.   * @param string $name The field name
  1390.   * @param array $attrs Attributes for the input element, e.g. ['id' => 'my-timepicker', class' => 'super-input', 'style' => 'font-style: italic']
  1391.   * @param array $settings Various settings
  1392.   * 'min' Minimum datetime in YYYY-MM-DD HH:MM:SS format
  1393.   * 'max' Maximum datetime in YYYY-MM-DD HH:MM:SS format
  1394.   * 'incr' Time increment in minutes. Default 1
  1395.   * 'dropdowns' => display the dropdown date selectors.
  1396.   * @return string HTML
  1397.   */
  1398. public static function datetimepicker($name, array $attrs = [], array $options = [])
  1399. {
  1400. Needs::fileGroup('moment');
  1401. Needs::fileGroup('daterangepicker');
  1402. Needs::fileGroup('fb');
  1403.  
  1404. if (isset($options['min'])) Validity::datetimeMySQL($options['min']);
  1405. if (isset($options['max'])) Validity::datetimeMySQL($options['max']);
  1406. if (isset($options['incr'])) Validity::range($options['incr'], 1, 59);
  1407. if (!isset($attrs['autocomplete'])) $attrs['autocomplete'] = 'off';
  1408.  
  1409. self::injectId($attrs);
  1410. self::addAttr($attrs, 'class', 'textbox fb-datetimepicker');
  1411.  
  1412. foreach ($options as $key => $val) {
  1413. $attrs['data-' . $key] = $val;
  1414. }
  1415.  
  1416. $out = self::input('hidden', $name, ['class' => 'fb-hidden']);
  1417. $out .= self::input('text', $name . '_picker', $attrs);
  1418.  
  1419. return $out;
  1420. }
  1421.  
  1422. /**
  1423.   * Renders HTML containing a total selector UI. Output field value for the total is in
  1424.   * a hidden field. The specific counts for each are also available
  1425.   *
  1426.   * @todo Does this need validation exceptions? I.e. min/max attributes invalid?
  1427.   * @param string $name The field name
  1428.   * @param array $attrs Attributes for the input element,
  1429.   * e.g. ['id' => 'my-totalselector', class' => 'super-input', 'style' => 'font-style: italic']
  1430.   * @param array $options Various options
  1431.   * 'singular' Label for total
  1432.   * 'plural' Plural label for total
  1433.   * 'fields' Array of fields that contribute to the total count
  1434.   * 'name' Internal name of field, plaintext
  1435.   * 'label' Field label (Sentence case), plaintext
  1436.   * 'helptext' Additional helptext for the field, optional, limited subset html
  1437.   * 'min' Minimum allowed value, optional, default 0
  1438.   * 'max' Maximum allowed value, optional, default unlimited
  1439.   * @return string HTML
  1440.   */
  1441. public static function totalselector($name, array $attrs = [], array $options = [])
  1442. {
  1443. Needs::fileGroup('total-selector');
  1444.  
  1445. self::injectId($attrs);
  1446. self::addAttr($attrs, 'class', 'textbox total-selector__output');
  1447. self::addAttr($attrs, 'readonly', true);
  1448.  
  1449. if (isset($options['fields'])) {
  1450. $fields = $options['fields'];
  1451. unset($options['fields']);
  1452. }
  1453.  
  1454. foreach ($options as $key => $val) {
  1455. $attrs['data-' . $key] = $val;
  1456. }
  1457.  
  1458. $out = self::input('text', $name, $attrs) . PHP_EOL;
  1459.  
  1460.  
  1461. $out .= '<div class="field-element--totalselector__fields">' . PHP_EOL;
  1462.  
  1463. foreach ($fields as $val) {
  1464. $sub_attrs = [];
  1465. $sub_attrs['type'] = 'number';
  1466. $sub_attrs['class'] = 'textbox';
  1467. $sub_attrs['id'] = $attrs['id'] . '-' . strtolower($val['name']);
  1468. $sub_attrs['name'] = $val['name'];
  1469. $sub_attrs['value'] = self::getData($val['name']);
  1470. $sub_attrs['min'] = (int) @$val['min'];
  1471. if (isset($val['max'])) {
  1472. $sub_attrs['max'] = (int) @$val['max'];
  1473. }
  1474.  
  1475. $out .= '<div class="field-element field-element--number">' . PHP_EOL;
  1476. $out .= '<div class="field-label">' . PHP_EOL;
  1477. $out .= '<label for="' . Enc::html($sub_attrs['id']) .'">' . Enc::html($val['label']) . '</label>' . PHP_EOL;
  1478. if (!empty($val['helptext'])) {
  1479. $out .= '<div class="field-helper">' . Text::limitedSubsetHtml($val['helptext']) . '</div>' . PHP_EOL;
  1480. }
  1481. $out .= '</div>' . PHP_EOL;
  1482. $out .= '<div class="field-input">' . PHP_EOL;
  1483. $out .= Fb::tag('input', $sub_attrs) . PHP_EOL;
  1484. $out .= '</div>' . PHP_EOL;
  1485. $out .= '</div>' . PHP_EOL;
  1486. }
  1487.  
  1488. $out .= '</div>' . PHP_EOL;
  1489.  
  1490.  
  1491. return $out;
  1492. }
  1493.  
  1494.  
  1495. /**
  1496.   * Renders a colour picker
  1497.   *
  1498.   * Uses the HTML5 'color' input type, and loads a JS fallback (spectrum)
  1499.   * http://bgrins.github.io/spectrum/
  1500.   * Note that spectrum requires jQuery 1.6 or later
  1501.   *
  1502.   * @param string $name The name of the input field
  1503.   * @param array $attrs Extra attributes for the input field
  1504.   * @param array $params Additional options; unused
  1505.   * @return string
  1506.   */
  1507. public static function colorpicker($name, array $attrs = [], array $params = [])
  1508. {
  1509. Needs::fileGroup('spectrum');
  1510. self::injectId($attrs);
  1511. self::addAttr($attrs, 'class', 'textbox colorpicker');
  1512. return self::input('color', $name, $attrs);
  1513. }
  1514.  
  1515.  
  1516. /**
  1517.   * Render map location selector
  1518.   * Zoom field is optional
  1519.   *
  1520.   * @wrap-in-fieldset
  1521.   * @param string $name Field names, comma separated, latitude,longitude,zoom
  1522.   * @param array $attrs Unused
  1523.   * @param array $params Unused
  1524.   * @return string HTML
  1525.   */
  1526. public static function googleMap($name, array $attrs = [], array $params = [])
  1527. {
  1528. Needs::fileGroup('fb');
  1529.  
  1530. $view = new View('sprout/components/fb_google_map');
  1531. $view->names = explode(',', $name);
  1532. $view->unique = md5(microtime(true));
  1533.  
  1534. $view->values = [];
  1535. foreach ($view->names as $name) {
  1536. $view->values[] = self::getData($name);
  1537. }
  1538.  
  1539. // Remove zero values to avoid a pin in the middle of the ocean
  1540. if ($view->values[0] == 0 and $view->values[1] == 0) {
  1541. $view->values[0] = '';
  1542. $view->values[1] = '';
  1543. }
  1544.  
  1545. return $view->render();
  1546. }
  1547.  
  1548.  
  1549. /**
  1550.   * A conditions list, which is an interface for building rules for
  1551.   * use in dynamic IF-statement style systems.
  1552.   *
  1553.   * Output POST data will be a JSON string of the condition rules,
  1554.   * as an array of objects with the keys 'field', 'op', 'val' for
  1555.   * each condition.
  1556.   *
  1557.   * There are two parameters:
  1558.   * fields array Available field types, name => label
  1559.   * url string AJAX lookup method which returns the
  1560.   * operator and value lists
  1561.   *
  1562.   * The lookup url is provided GET params 'field', 'op', 'val' and
  1563.   * should output JSON with two keys, 'op' and 'val', which are both
  1564.   * strings containing HTML for the fields; the op field should be
  1565.   * a SELECT and the val field should be an INPUT or a SELECT.
  1566.   *
  1567.   * @wrap-in-fieldset
  1568.   * @param string $name Field name
  1569.   * @param array $attrs Unused
  1570.   * @param array $params Array with two params, 'fields' and 'url'
  1571.   * @return string HTML
  1572.   */
  1573. public static function conditionsList($name, array $attrs = [], array $params = [])
  1574. {
  1575. $data = self::getData($name);
  1576. if (empty($data)) $data = '[]';
  1577.  
  1578. Needs::fileGroup('underscore');
  1579. Needs::fileGroup('fb');
  1580.  
  1581. $view = new View('sprout/components/fb_conditions_list');
  1582. $view->name = $name;
  1583. $view->params = $params;
  1584. $view->data = $data;
  1585. return $view->render();
  1586. }
  1587.  
  1588.  
  1589. /**
  1590.   * Renders google autocomplete address fields
  1591.   *
  1592.   * @param string $name Field
  1593.   * @param array $attrs Attributes for the input element
  1594.   * @param array $params Config options
  1595.   * ```js
  1596.   * {
  1597.   * fields: {street: field-name, city: field-name, state: field-name, postcode: field-name, country: field-name},
  1598.   * restrictions: { country: ['AU'] }
  1599.   * }
  1600.   * ```
  1601.   * OR assume $param is just the $fields component (fallback, deprecated)
  1602.   *
  1603.   * Note: 'restrictions' are defined here:
  1604.   * https://developers.google.com/maps/documentation/javascript/reference/places-autocomplete-service#ComponentRestrictions
  1605.   * @return string HTML
  1606.   */
  1607. public static function autoCompleteAddress($name, array $attrs = [], array $options = [])
  1608. {
  1609. Needs::fileGroup('fb');
  1610. Needs::googlePlaces();
  1611.  
  1612. self::injectId($attrs);
  1613. self::addAttr($attrs, 'class', 'textbox js-autocomplete-address');
  1614. self::addAttr($attrs, 'autocorrect', 'off');
  1615.  
  1616. if (!isset($options['fields']) and !isset($options['restrictions'])) {
  1617. $options = ['fields' => $options];
  1618. }
  1619.  
  1620. $view = new View('sprout/components/fb_autocomplete_address');
  1621. $view->options = $options;
  1622. $view->form_field = self::input('text', $name, $attrs);
  1623.  
  1624. return $view->render();
  1625. }
  1626.  
  1627.  
  1628. /**
  1629.   * Renders place name geocoding fields
  1630.   *
  1631.   * @param string $name Field
  1632.   * @param array $attrs Attributes for the input elements
  1633.   * @param array $options Config options
  1634.   * ```js
  1635.   * {
  1636.   * fields: {street: field-name, city: field-name, state: field-name, postcode: field-name, country: field-name},
  1637.   * restrictions: { country: 'AU' }
  1638.   * }
  1639.   * ```
  1640.   * Beware: The restrictions cannot accept a country list like autoCompleteAddress().
  1641.   *
  1642.   * Note: 'restrictions are defined here:
  1643.   * https://developers.google.com/maps/documentation/javascript/reference/geocoder#GeocoderComponentRestrictions
  1644.   *
  1645.   * @return string HTML
  1646.   */
  1647. public static function geocodeAddress($name, array $attrs = [], array $options = [])
  1648. {
  1649. Needs::fileGroup('fb');
  1650. Needs::googlePlaces();
  1651.  
  1652. self::injectId($attrs);
  1653. self::addAttr($attrs, 'class', 'textbox js-geocode-address');
  1654. self::addAttr($attrs, 'autocorrect', 'off');
  1655.  
  1656. $view = new View('sprout/components/fb_geocode_address');
  1657. $view->options = $options;
  1658. $view->form_field = self::input('text', $name, $attrs);
  1659.  
  1660. return $view->render();
  1661. }
  1662.  
  1663. /**
  1664.   * Render a 'generate code' button + text field
  1665.   *
  1666.   * @param mixed $name Field
  1667.   * @param array $attrs Attributes for the input element
  1668.   * @param array $options Settings
  1669.   * @return void
  1670.   */
  1671. public static function randomCode($name, array $attrs = [], array $options = [])
  1672. {
  1673. self::injectId($attrs);
  1674. self::addAttr($attrs, 'class', 'textbox column column-9');
  1675. self::addAttr($attrs, 'autocorrect', 'off');
  1676. self::addAttr($attrs, 'autocomplete', 'off');
  1677.  
  1678. $defaults = [
  1679. 'size' => 10,
  1680. 'readable' => false,
  1681. 'uppercase' => true,
  1682. 'lowercase' => true,
  1683. 'numbers' => true,
  1684. 'symbols' => false,
  1685. ];
  1686.  
  1687. $view = new View('sprout/components/fb_random_code');
  1688. $view->options = array_merge($defaults, $options);
  1689. $view->form_id = $attrs['id'];
  1690. $view->form_field = self::input('text', $name, $attrs);
  1691.  
  1692. return $view->render();
  1693. }
  1694.  
  1695.  
  1696.  
  1697. /**
  1698.   * UI for selecting or drag-and-drop uploading one or more files.
  1699.   *
  1700.   * The field (refrenced by $name) is an array. If it's passed a a string, it will be comma-separated into an array.
  1701.   * As JsonForm will auto-convert arrays into comma-separated strings, this field can easily be used with a MySQL
  1702.   * field of type TEXT.
  1703.   *
  1704.   * You cannot have more than one of these on the page at a time
  1705.   *
  1706.   * This field WILL NOT operate in a non-admin environment
  1707.   *
  1708.   * @param string $name Field name. If [] is not at the end, this will be appended.
  1709.   * @param array $attrs Unused
  1710.   * @param array $options Includes the following:
  1711.   * 'filter': (int) One of the filters, e.g. {@see FileConstants}::TYPE_IMAGE
  1712.   * @return string HTML
  1713.   */
  1714. public static function multipleFileSelect($name, array $attrs = [], array $options = [])
  1715. {
  1716. $data = self::getData($name);
  1717. if (empty($data)) $data = [];
  1718.  
  1719. if (is_string($data)) {
  1720. $data = explode(',', $data);
  1721. }
  1722.  
  1723. $ids = [];
  1724. foreach ($data as $id) {
  1725. if (preg_match('/^[0-9]+$/', $id)) $ids[] = (int) $id;
  1726. }
  1727.  
  1728. $filenames = [];
  1729. if (count($ids) > 0) {
  1730. $params = [];
  1731. $where = Pdb::buildClause([['id', 'IN', $ids]], $params);
  1732. $filenames = Pdb::q("SELECT id, filename FROM ~files", $params, 'map');
  1733. }
  1734.  
  1735. if (substr($name, -2) != '[]') $name .= '[]';
  1736.  
  1737. $opts = array();
  1738. $opts['chunk_url'] = 'admin/call/file/ajaxDragdropChunk';
  1739. $opts['done_url'] = 'admin/call/file/ajaxDragdropDone';
  1740. $opts['form_url'] = 'admin/call/file/ajaxDragdropForm';
  1741. $opts['cancel_url'] = 'admin/call/file/ajaxDragdropCancel';
  1742. $opts['form_params'] = [];
  1743. $opts['max_files'] = 100;
  1744.  
  1745. $view = new View('sprout/components/multiple_file_select');
  1746. $view->opts = $opts;
  1747. $view->name = $name;
  1748. $view->data = $data;
  1749. $view->filenames = $filenames;
  1750. $view->filter = (int) @$options['filter'];
  1751.  
  1752. return $view->render();
  1753. }
  1754.  
  1755.  
  1756. /**
  1757.   * Generates the title for a field, possibly enclosing it in a label, possibly with a generated ID
  1758.   *
  1759.   * @deprecated This method is likely to be removed at any given moment.
  1760.   * Please use {@see Form::nextFieldDetails} instead.
  1761.   *
  1762.   * @param string $title The title of the field
  1763.   * @param string|null $id The id to use. Empty string to auto-generate an id; false to disable the enclosing label,
  1764.   * e.g. for a field which needs multiple inputs (such as a datepicker). The id will be used on the next
  1765.   * input to be generated.
  1766.   * @return string Possibly a LABEL element, or otherwise HTML text
  1767.   */
  1768. public static function title($title, $id = '')
  1769. {
  1770. if ($id === false) return Enc::html($title);
  1771.  
  1772. if ($id) {
  1773. self::$field_id = $id;
  1774. } else {
  1775. self::$field_id = $id = self::genId();
  1776. }
  1777. return '<label for="' . Enc::html(self::$field_id) . '">' . Enc::html($title) . '</label>';
  1778. }
  1779.  
  1780.  
  1781. /**
  1782.   * Renders a set of hidden fields
  1783.   * @param array $fields Field name-value pairs
  1784.   * @return string Several INPUT fields of type hidden
  1785.   */
  1786. public static function hiddenFields(array $fields)
  1787. {
  1788. $out = '';
  1789. foreach ($fields as $key => $val) {
  1790. $key = Enc::html($key);
  1791. $val = Enc::html($val);
  1792. $out .= "<input type=\"hidden\" name=\"{$key}\" value=\"{$val}\">\n";
  1793. }
  1794. return $out;
  1795. }
  1796.  
  1797. }
  1798.