<?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>.
*/
namespace Sprout\Helpers;
use Exception;
use InvalidArgumentException;
use karmabunny\pdb\Exceptions\RowMissingException;
use Sprout\Helpers\Locales\LocaleInfo;
/**
* Quick and easy form builder
**/
class Fb
{
/** ID for use on the field label and input/select/etc. element */
public static $field_id = '';
/** Extra class(es) for use on div.field-wrapper, div.label, and div.field to support additional styling.
N.B. If this is set, all inputs will be wrapper in a div.field */
public static $include_class = '';
public static $data = [];
public static $scope = 'admin';
public static $dropdown_top = 'Select an option';
/**
* @var string A prefix for generated IDs
*/
public static $id_prefix = '';
/**
* Sets the data that is used for form-building
* This is typically from a database row or saved session data
* To set a single field, instead use {@see Fb::setFieldValue}
*
* @param array $data Field name => value pairs
* @return void
*/
public static function setData($data)
{
self::$data = $data;
}
/**
* Sets the value for a single field
* This is the non-array version of {@see Fb::setData}
*
* @param array $field Field name, e.g. 'first_name'
* @param array $value Field value, e.g. 'John'
* @return void
*/
public static function setFieldValue($field, $value)
{
self::$data[$field] = $value;
}
/**
* Sets the text for the top item of dropdown lists.
* Set to an empty string to not show the top item.
* This will be reset to the default value after every call to {@see Fb::dropdown}
* @deprecated Set the special attribute "-dropdown-top" when calling the dropdowns
* @param string $label The data to put in the first OPTION, with its value being an empty string
* @return void
*/
public static function setDropdownTop($label)
{
self::$dropdown_top = $label;
}
/**
* Gets the data for a single field from the data array
*
* Properly handles PHP sub-arrays (e.g. 'options[food]'), doesn't handle
* anon arrays though (e.g. 'options[]').
* @param string $name The field name
* @return mixed The value (often a string)
*/
public static function getData($name)
{
if (strpos($name, '[') === false) { return @self::$data[$name];
}
// Get a list of keys
foreach ($keys as &$key) {
if ($key == '') return ''; // anon arrays aren't supported
}
// Loop through the keys till we get the value we want
$v = self::$data;
foreach ($keys as $k) {
$v = @$v[$k];
}
return $v;
}
/**
* Generates a heading using a H3 tag
* @param string $heading
* @return string H3 element
*/
public static function heading($heading)
{
return '<h3>' . Enc::html($heading) . '</h3>';
}
/**
* Generate a unique id
* @return string 'fb?', where ? is an incrementing number starting at zero
*/
private static function genId()
{
static $inc = 0;
return static::$id_prefix . 'fb' . $inc++;
}
/**
* Injects the current auto-generated id into an array of attributes.
* Only applies if an auto-generated id exists and an id isn't already set in the attributes.
* The auto-generated id is then cleared.
* @param array $attrs The attributes
* @return void
*/
protected static
function injectId
(array &$attrs) {
if (isset($attrs['id'])) return; if (!self::$field_id) return;
$attrs['id'] = self::$field_id;
self::$field_id = '';
}
/**
* Adds an HTML attribute to the list of attributes.
* If the attribute has already been set, it will be left alone.
* N.B. the 'class' attribute is always appended
* @param array $attrs The list of attributes to modify
* @param string $name The name of the attribute, e.g. 'style'
* @param string $value The value of the attribute, e.g. 'display: none;'
* @return void
*/
protected static
function addAttr
(array &$attrs, $name, $value) {
if (isset($attrs[$name])) { if ($name == 'class') {
if ($attrs['class'] and $value != '') $attrs['class'] .= ' ';
$attrs['class'] .= $value;
}
return;
}
$attrs[$name] = $value;
}
/**
* Generates an HTML opening tag, and possibly its closing tag, depending on the params specified
*
* You can specify either HTML or plain-text content, but not both
*
* @param string $name The name of the tag, e.g. 'textarea'
* @param array $attrs Attributes for the tag, e.g. ['class' => 'super-input', 'style' => 'font-style: italic']
* @param string $params Additional options, as follows:
* - 'html' (string): Specifies HTML content between the opening and closing tags, which MUST be
* properly encoded.
* - 'plain' (string): Specifies non-encoded content content between the opening and closing tags.
* @return string The generated tag
*/
public static
function tag
($name, array $attrs = [], array $params = []) {
$tag = '<' . Enc::html($name);
foreach ($attrs as $attr => $val) {
// Support boolean attributes
if (is_int($attr)) $attr = $val;
$tag .= ' ' . Enc::html($attr) . '="' . Enc::html($val) . '"';
}
$tag .= '>';
$close = false;
$tag .= $params['html'];
$close = true;
$tag .= Enc::html($params['plain']);
$close = true;
}
if ($close) {
$tag .= '</' . Enc::html($name) . '>';
}
return $tag;
}
/**
* Generates an HTML INPUT tag using {@see Fb::tag}, with auto-determined value
*
* @param string $type The type of input, e.g. 'text', 'hidden', ...
* @param string $name The name of the input
* @param array $attrs Attributes for the tag
* @return string INPUT element
* @throws InvalidArgumentException if $name is empty
*/
public static
function input
($type, $name, array $attrs = []) {
throw new InvalidArgumentException('An INPUT without a name is invalid');
}
$attrs['type'] = $type;
$attrs['name'] = $name;
if ($type != 'file') {
$attrs['value'] = self::getData($name);
}
return self::tag('input', $attrs);
}
/**
* Outputs the value of the field directly, in a span
* @param array $attrs Extra attributes for the input field
* @return string SPAN element
*/
public static
function output
($name, array $attrs = []) {
$value = self::getData($name);
self::addAttr($attrs, 'class', 'field-output');
self::addAttr($attrs, 'name', $name);
return self::tag('span', $attrs, ['plain' => $value]);
}
/**
* Generates a text field
* @todo Use a generic method to generate the INPUT tag and its attributes
* @param string $name The name of the input field
* @param array $attrs Extra attributes for the input field
* @return string INPUT element
*/
public static
function text
($name, array $attrs = []) {
self::injectId($attrs);
self::addAttr($attrs, 'class', 'textbox');
return self::input('text', $name, $attrs);
}
/**
* Shows a HTML5 number field
*
* @param string $name The name of the input field
* @param array $attrs Extra attributes for the input field; 'min' and 'max' being particularly relevant
* @return string INPUT element
*/
public static
function number
($name, array $attrs = []) {
self::injectId($attrs);
self::addAttr($attrs, 'class', 'textbox');
return self::input('number', $name, $attrs);
}
/**
* Shows a HTML5 number field, formatted for dollar prices
*
* @param string $name The name of the input field
* @param array $attrs Extra attributes for the input field; 'min' and 'max' being particularly relevant
* @return string INPUT element
*/
public static
function money
($name, array $attrs = [], array $options = []) {
self::injectId($attrs);
self::addAttr($attrs, 'class', 'textbox');
$attrs["min"] = "0";
}
$attrs["step"] = "0.01";
}
if (isset($options['locale'])) { $locale = LocaleInfo::get($options['locale']);
} else {
$locale = LocaleInfo::auto();
}
$out = '<div class="money-symbol money-symbol--' . Enc
::id(strtolower($locale->getCurrencyName())) . '">'; $out .= self::input('number', $name, $attrs);
$out .= '</div>';
return $out;
}
/**
* Generates a HTML5 range field
* @param string $name Field name
* @param array $attrs Extra attributes for the INPUT element. A range element takes these attributes:
* 'min' The minimum value, default 0, set to NULL for no limit
* 'max' The maximum value, default 100, set to NULL for no limit
* 'step' Difference between each grade on the range
* @param array $options Ignored
* @return string HTML with elements including INPUT and SCRIPT
*/
public static
function range($name, array $attrs = [], array $options = []) {
self::injectId($attrs);
self::addAttr($attrs, 'class', 'textbox');
if (!isset($attrs['min'])) $attrs['min'] = 0; if (!isset($attrs['max'])) $attrs['max'] = 100;
if ($attrs['min'] === null) unset($attrs['min']); if ($attrs['max'] === null) unset($attrs['max']);
$out = self::input('range', $name, $attrs);
$id = Enc::id($attrs['id']);
$value = (float) Fb::getData($name);
$div = "<div id=\"{$id}-count\">{$value}</div>";
$out .= "<script type=\"text/javascript\">
$(document).ready(function() {
$(\"#{$id}\").after('{$div}');
$(\"#{$id}-count\").text($(\"#{$id}\").val());
$(\"#{$id}\").bind('change click', function() {
$(\"#{$id}-count\").text($(this).val());
});
});
</script>";
return $out;
}
/**
* Generates a password field
* @param string $name The name of the input field
* @param array $attrs Extra attributes for the input field
* @return string INPUT element
*/
public static function password($name, $attrs = [])
{
self::injectId($attrs);
self::addAttr($attrs, 'class', 'textbox password');
return self::input('password', $name, $attrs);
}
/**
* Generates a file upload field
* @param string $name The name of the input field
* @param array $attrs Extra attributes for the input field
* @return string INPUT element
*/
public static
function upload
($name, array $attrs = []) {
self::injectId($attrs);
self::addAttr($attrs, 'class', 'upload');
return self::input('file', $name, $attrs);
}
/**
* Generates a file upload field with a progress bar
*
* To easily save the uploaded files in the form action function, see {@see File::replaceSet}
*
* @param string $name
* @param array $attrs
* @param array $params Must have 'sess_key' => session key, e.g. 'user-register'.
* Data regarding each uploaded file will typically be saved in
* $_SESSION['file_uploads'][$params['sess_key']][$name].
*
* May also have 'opts' which can contain any of the following:
* - 'begin_url' (string)
* - 'form_url' (string)
* - 'done_url' (string)
* - 'cancel_url' (string)
* - 'form_params' (array):
* - form_id (string)
* - field_name (string)
*
* May also specify 'multiple', which is a positive int (default: 1).
* If more than 1, multiple files are allowed, up to the number specified.
*/
public static
function chunkedUpload
($name, array $attrs = [], array $params = []) {
self::injectId($attrs);
$max_files = (int) @$params['multiple'];
if ($max_files < 1) $max_files = 1;
$default_opts = [
'begin_url' => 'file_upload/upload_begin',
'form_url' => 'file_upload/upload_form',
'chunk_url' => 'file_upload/upload_chunk',
'done_url' => 'file_upload/upload_done',
'cancel_url' => 'file_upload/upload_cancel',
'form_params' => [
'form_id' => $params['sess_key'],
'field_name' => $name,
],
'max_files' => $max_files,
];
// Override default opts with any specified via $params
if (!empty($params['opts'])) { $opts = $params['opts'];
foreach ($default_opts as $key => $val) {
if (empty($opts[$key])) { $opts[$key] = $default_opts[$key];
}
}
} else {
$opts = $default_opts;
}
$out = '<div class="fb-chunked-upload" data-opts="' . Enc
::html(json_encode($opts)) . '">';
$upload_params = ['class' => 'file-upload__input', 'id' => $attrs['id']];
if ($opts['max_files'] > 1) $upload_params['multiple'] = 'multiple';
$out .= self::upload($name . '_upload', $upload_params);
$out .= '<div class="file-upload__area textbox">';
$files = ($opts['max_files'] == 1 ? 'file' : 'files');
$out .= '<div class="file-upload__helptext">';
$out .= "<p>Drop {$files} here ";
$out .= '<span class="file-upload__helptext__line2">or click to upload</span></p>';
$out .= '</div>';
$out .= '<div class="file-upload__uploads">';
// Show uploaded file(s) if there's uploaded file data in the session
// Otherwise it just gets thrown away if there's a form error
$friendly_vals = self::getData($name);
$temp_vals = self::getData($name . '_temp');
$files = [];
// Zip!
$vals = array_map(null, $friendly_vals, $temp_vals); foreach ($vals as $item) {
list($friendly, $temp) = $item;
if (!$friendly or !$temp) continue;
$temp_path = APPPATH . 'temp/' . $temp;
$temp_parts = explode('-', $temp, 3); $files[] = [
'original' => $friendly,
'temp' => $temp,
];
}
}
$files[] = $friendly_vals;
$out .= '<input class="js-delete-notify" type="hidden" name="' . Enc::html($name) . '_deleted">';
}
foreach ($files as $file) {
// Temp uploaded files stored in session
$temp_path = APPPATH . 'temp/' . $file['temp'];
$view = new View('sprout/file_confirm');
$view->orig_file = ['name' => $file['original'], 'size' => filesize($temp_path)];
// Existing file stored on disk
} else if ($file) {
$temp_path = DOCROOT . 'files/' . $file;
$view = new View('sprout/file_confirm');
$view->orig_file = ['name' => 'Existing file', 'size' => filesize($temp_path)]; } else {
continue;
}
if ($type == FileConstants::TYPE_IMAGE) {
try {
$view->shrunk_img = File::base64Thumb($temp_path, 200, 200); } catch (Exception $ex) {
$view->image_too_large = true;
}
}
$out .= '<div class="file-upload__item"';
if (!empty($file['code'])) $out .= ' data-code="' . Enc
::html($file['code']) . '"'; $out .= '>';
$out .= $view->render();
$out .= '</div>';
}
// Don't try and save an existing file which is already on disk
$files = [];
}
$out .= '</div>'; // .file-upload__uploads
$out .= '</div>'; // .file-upload__area
$out .= '<div class="file-upload__data">';
foreach ($files as $file) {
$out .= '<input type="hidden" name="' . Enc::html($name) . '[]" class="original" value="' . Enc::html($file['original']) . '" data-code="' . Enc::html($file['code']) . '">';
$out .= '<input type="hidden" name="' . Enc::html($name) . '_temp[]" class="temp" value="' . Enc::html($file['temp']) . '" data-code="' . Enc::html($file['code']) . '">';
}
$out .= '</div>'; // .file-upload__data
$out .= '</div>'; // .fb-chunked-upload
return $out;
}
/**
* Renders a HTML5 email field
* @param string $name Field name
* @param array $attrs Extra attributes for the INPUT element
* @param array $options Ignored
* @return string
*/
public static
function email
($name, array $attrs = [], array $options = []) {
self::injectId($attrs);
self::addAttr($attrs, 'class', 'textbox email');
return self::input('email', $name, $attrs);
}
/**
* Renders a HTML5 phone number field (type=tel)
* @param string $name Field name
* @param array $attrs Extra attributes for the INPUT element
* @param array $params Ignored
* @return string INPUT element
*/
public static
function phone
($name, array $attrs = [], array $params = []) {
self::injectId($attrs);
Fb::addAttr($attrs, 'class', 'textbox phone');
return self::input('tel', $name, $attrs);
}
/**
* Render the UI for our multi-type link fields
*
* @wrap-in-fieldset
* @param string $name The name of the field
* @param array $attrs Unused
* @param array $options Includes the following:
* 'required': (bool) true if the field is required
* @return string HTML
*/
public static
function lnk
($name, array $attrs = [], array $options = []) {
$value = self::getData($name);
return Lnk
::editform($name, $value, !empty($options['required'])); }
/**
* A file selection field, for use in the admin only.
*
* @param string $name Field name
* @param array $attrs Unused.
* @param array $options Includes the following:
* 'filter': (int) One of the filters, e.g. {@see FileConstants}::TYPE_IMAGE
* 'required': (bool) true if the field is required
* 'req_category': (int) Category field required. Default of 1: required. 0: not required
* @return string HTML
*/
public static
function fileSelector
($name, array $attrs = [], array $options = []) {
$value = self::getData($name);
$options['filter'] = (int) @$options['filter'];
$options['required'] = (bool) @$options['required'];
$options['req_category'] = isset($options['req_category']) ?
(int
) $options['req_category'] : 1;
$classes = ['fb-file-selector', 'fs', '-clearfix'];
if ($value) {
$classes[] = 'fs-file-selected';
}
$filename = '';
try {
$filename = Pdb::q("SELECT filename FROM ~files WHERE id = ?", [$value], 'val');
} catch (RowMissingException $ex) {
}
}
$out = '<span class="' . Enc::html($classes) . '" data-filter="' . $options['filter'] . '"';
$out .= ' data-init="false" data-filename="' . Enc::html($filename) . '" data-req-category="' . Enc::html($options['req_category']) . '">';
$out .= '<button type="button" class="fs-select-button button button-blue popup-button icon-after icon-insert_drive_file">Select a file</button>';
$out .= '<input class="fs-hidden" type="hidden" name="' . Enc::html($name) . '" value="' . Enc::html($value) . '">';
$out .= '<span class="fs-preview-wrapper">';
if ($options['filter'] == FileConstants
::TYPE_IMAGE or
strpos(File::mimetype($value), 'image/') === 0) { $out .= '<span class="fs-preview">';
if ($value) {
$out .= '<img src="' . Enc
::html(File::resizeUrl($value, 'c50x50')) . '" alt="">'; }
$out .= '</span>';
}
$out .= '<span class="fs-filename">';
$out .= ($value ? Enc::html($value) : 'No file selected');
$out .= '</span>';
if (!$options['required']) {
$out .= '<button class="fs-remove" type="button"><span class="-vis-hidden">Remove</span></button>';
}
$out .= '</span>'; // preview wrapper
$out .= '</span>'; // outer wrap
return $out;
}
/**
* Generates a richtext field - i.e. TinyMCE
*
* @wrap-in-fieldset
* @param string $name The field name for this richtext field.
* @param array $attrs Including 'height' and 'width' in pixels
* @param array $items Specify 'type' for RichText, e.g. 'TinyMCE4', or 'TinyMCE4:Lite'
* @return string HTML containing a TEXTAREA element and an associated SCRIPT element which to converts it
* into a richtext field
*/
public static
function richtext
($name, array $attrs = [], array $items = []) {
$value = self::getData($name);
$width = (int) @$attrs['width'];
if ($width <= 0) $width = 600;
$height = (int) @$attrs['height'];
if ($height <= 0) $height = 400;
return RichText::draw($name, $value, $width, $height, @$items['type']);
}
/**
* Generates a multiline text field
* @param string $name The field name for this field.
* @param array $attrs Extra attributes for the TEXTAREA element
* @return string TEXTAREA element
*/
public static
function multiline
($name, array $attrs = []) {
self::injectId($attrs);
self::addAttr($attrs, 'class', 'textbox multiline');
$attrs['name'] = $name;
return self::tag('textarea', $attrs, ['plain' => self::getData($name)]);
}
/**
* Generates a dropdown selection menu
* @todo Use a generic method to generate the SELECT tag and its attributes
* @param string $name The field name
* @param array $attrs Extra attributes for the SELECT element
* The special attribute "-dropdown-top" sets the label for the top item
* Use an empty string for no top item
* @param array $options Data for the OPTION elements in value-label pairs,
* e.g. [0 => 'Inactive', 1 => 'Active']
* @return string
*/
public static
function dropdown
($name, array $attrs = [], array $options = []) {
if (isset($attrs['-dropdown-top'])) { self::$dropdown_top = $attrs['-dropdown-top'];
unset($attrs['-dropdown-top']); }
$is_multi = false;
foreach ($attrs as $key => $val) {
$is_multi = true;
break;
}
$is_multi = true;
break;
}
}
self::injectId($attrs);
$value = self::getData($name);
$extra = self::addAttr($attrs, 'class', 'dropdown');
if ($is_multi and
substr($name, -2) != '[]') { $name .= '[]';
}
$attrs['name'] = $name;
$field = self::tag('select', $attrs);
if (self::$dropdown_top and !$is_multi) {
$field .= '<option value="" class="dropdown-top">';
$field .= Enc::html(self::$dropdown_top) . '</option>';
}
$field .= self::dropdownItems($options, $value);
$field .= '</select> ';
// Revert to default top dropdown item
self::$dropdown_top = 'Select an option';
return $field;
}
/**
* Returns HTML for a list of OPTIONs, and depending on the input array, OPTGROUP tags.
* @param array $options The options. Any element that is an array will become an optgroup, with its inner elements
* becoming options.
* @param string|array $selected The value of the selected option
* @return string
*/
public static
function dropdownItems
(array $options, $selected = null) {
$out = '';
foreach ($options as $val => $label) {
$val_enc = Enc::html($val);
$out .= "<optgroup label=\"{$val_enc}\">";
$out .= self::dropdownItems($label, $selected);
$out .= "</optgroup>";
} else {
$label = Enc::html($label);
$label = str_replace(' ', ' ', $label);
if ($val == $selected) {
$out .= "<option value=\"{$val_enc}\" selected>{$label}</option>";
$out .= "<option value=\"{$val_enc}\" selected>{$label}</option>";
} else {
$out .= "<option value=\"{$val_enc}\">{$label}</option>";
}
}
}
return $out;
}
/**
* Returns HTML for an autocomplete selection menu.
* Expects to be provided AJAX data in a format defined by jQuery UI (see 'url' option below).
* By default, this expects to stores an ID value for a foreign key. If this isn't the desired
* behaviour, a plain text value can be stored by setting the 'save_id' option to false.
*
* The URL which handles the lookups needs to use the 'term' GET param to find matching values.
* If 'save_id' is true, then it also needs to accept the 'id' GET param to fetch the label for the relevant id.
* A call to the URL should return JSON which contains an array of hashes, each with the following keys:
* - 'value': data for the text input.
* - 'label': data for display in the drop-down list (if different from value).
* This is used as the value if 'value' isn't specified.
* - 'id': id value to save. This should only be specified if 'save_id' is true; see below.
*
* @param string $name The field name
* @param array $attrs Extra attributes for the INPUT element
* 'data-callback' Function name to call once a selection has been made, which should accept a param of {int} item_id
* @param array $options Keys as follows:
* 'url' (string, required) URL to access when fetching matches via AJAX.
* 'save_id' (bool, defaults to true) Save the data as an ID value or similar unique key, and look up the
* label for display upon page by calling the URL with the 'id' GET param set
* 'multiline' (bool, defaults to false) Use a textarea to support multiline text
* 'chars' (int, defaults to 2) Minimum number of characters required before first AJAX lookup fires.
* If zero, the lookup will happen on focus.
* @return string An INPUT element and associated SCRIPT element
*/
public static
function autocomplete
($name, array $attrs = [], array $options = []) {
if (empty($options['url'])) { throw new InvalidArgumentException("\$options['url'] must be specified");
}
$chars = 2;
} else {
$chars = (int) $options['chars'];
if ($chars < 0) $chars = 0;
}
self::injectId($attrs);
self::addAttr($attrs, 'class', 'textbox');
self::addAttr($attrs, 'class', 'autocomplete');
self::addAttr($attrs, 'data-lookup-url', $options['url']);
self::addAttr($attrs, 'data-chars', $chars);
// Automatically add a hidden field with the 'id' value if available. This is probably the most
// desirable behaviour, as an autocomplete field is usually for a Foreign Key column
$attrs['data-save-id'] = (int) (bool) $options['save_id'];
if (!empty($options['multiline'])) { $attrs['name'] = $name;
$input = self::tag('textarea', $attrs, ['plain' => self::getData($name)]);
} else {
$input = self::input('text', $name, $attrs);
}
return '<div class="autocomplete-symbol">' . $input . '</div>';
}
/**
* Render autocomplete multi-list
*
* @param string $name Form field name
* @param array $attrs Extra attributes for the INPUT element
* 'data-callback' (string )Function name to call once a selection has been made,
* which should accept the same params as endpoint response listed below.
*
* @param array $options Keys as follows:
* 'url' (string) ajax called endpoint to populate autocomplete list
* Endpoint to acccept:
* $_GET['q'] (string) Search keyword
* $_GET['ids'] (string) CSV of record IDs to poplate on page-load
* Endpoint to return (json):
* 'id' (int) Record ID
* 'value' (string) Record label
*
* 'chars' (int, defaults to 2) Minimum number of characters required before first AJAX lookup fires.
* If zero, the lookup will happen on focus.
*
* @return string A HTML INPUT element and associated SCRIPT element
*/
public static
function autocompleteList
($name, array $attrs = [], array $options = []) {
if (empty($options['limit'])) $options['limit'] = 0; if (empty($options['reorder'])) $options['reorder'] = false; if (empty($options['title'])) $options['title'] = 'Items'; if (empty($options['chars']) or
!is_numeric($options['chars']) or
$options['chars'] < 0) $options['chars'] = 2;
self::injectId($attrs);
self::addAttr($attrs, 'class', 'textbox');
self::addAttr($attrs, 'class', 'autocomplete-list');
self::addAttr($attrs, 'data-url', $options['url']);
self::addAttr($attrs, 'data-chars', $options['chars']);
self::addAttr($attrs, 'data-values', self::getData($name));
self::addAttr($attrs, 'data-name', $name);
$view = new View('sprout/components/fb_autocomplete_list');
$view->input = self::input('text', "{$name}_search", $attrs);
$view->id = $attrs['id'];
return $view->render();
}
/**
* Returns HTML for a bunch of radiobuttons
*
* @wrap-in-fieldset
* @param string $name The field name
* @param array $attrs Attributes for all input elements, e.g. ['class' => 'super-input', 'style' => 'font-style: italic']
* @param array $options each is a value => label mapping, e.g. ['A' => 'Apple crumble', 'B' => 'Banana split']
* @return string HTML containing DIV tags containing INPUT and LABEL tags.
*/
public static
function multiradio
($name, array $attrs = [], array $options = []) {
$value = self::getData($name);
$content = '';
foreach ($options as $opt_value => $label) {
$id = self::genId();
$content .= '<div class="fieldset-input">';
$input_attrs = [
'type' => 'radio',
'id' => $id,
'name' => $name,
'value' => $opt_value,
];
if ($opt_value == $value) $input_attrs['checked'] = 'checked';
$content .= self::tag('input', $tag_attrs);
$content .= "<label for=\"{$id}\">";
$content .= Enc::html($label);
$content .= "</label>";
$content .= "</div>";
}
return $content;
}
/**
* Returns HTML containing multiple boolean checkboxes
*
* @wrap-in-fieldset
* @param string $name Ignored; each checkbox specifies its own name in $settings
* @param array $attrs Unused but remains for compatibility with other methods
* @param array $settings Keys are the names of the checkbox fields, and values their labels.
* @return string HTML containing DIV tags containing INPUT and LABEL tags.
*/
public static
function checkboxBoolList
($name, array $attrs = [], array $settings = []) {
$out = '';
foreach ($settings as $name => $label) {
$selected = !empty(self::getData($name)); $out .= self::checkbox($name, $label, 1, $selected, $attrs);
}
return $out;
}
/**
* Returns HTML containing multiple checkboxes, with values to store in a SET column or similar
*
* @wrap-in-fieldset
* @param string $name Name for each INPUT element. Empty brackets will be appended if not supplied, i.e. [].
* @param array $attrs Attributes for all input elements, e.g. ['class' => 'super-input', 'style' => 'font-style: italic']
* @param array $settings Keys are the values available in the set, and values their labels. These can match, and
* can be easily filled by a call to {@see Pdb::extractEnumArr}
* @return string HTML containing DIV tags containing INPUT and LABEL tags.
*
* @example
* Form::nextFieldDetails('Colours to include', false);
* echo Form::checkboxSet('colours', [], [
* 'red' => 'Red, the colour of cherries and strawberries',
* 'green' => 'Green, the colour of leaves',
* 'blue' => 'Blue, the colour of the sky and ocean',
* ]);
*/
public static
function checkboxSet
($name, array $attrs = [], array $settings = []) {
$out = '';
$selected = self::getData($name);
if (substr($name, -2) != '[]') $name .= '[]'; $id = Enc::id($name);
$name = Enc::html($name);
foreach ($settings as $value => $label) {
$is_selected = in_array($value, $selected); $out .= static::checkbox($name, $label, $value, $is_selected, $attrs);
}
return $out;
}
/**
* Returns the HTML for a single checkbox
*
* @note This typically isn't used directly; instead use @see Form::checkboxList,
* @see Fb::checkboxBoolList, @see Fb::checkboxSet
* @param string $name The name of the checkbox
* @param string $label The label for the checkbox; supports minimal HTML, {@see Text::limitedSubsetHtml}
* @param int|string $value The checkbox's value
* @param bool $selected Whether or not the checkbox is ticked
* @param array $attrs Extra attributes attached to the input tag
*/
protected static
function checkbox
($name, $label, $value, $selected, array $attrs = []) {
$out = '';
$out .= '<div class="fieldset-input">';
$input_attrs = [
'type' => 'checkbox',
'id' => self::genId(),
'name' => $name,
'value' => $value,
];
if ($selected) {
$input_attrs['checked'] = 'checked';
}
$out .= self::tag('input', $tag_attrs);
$out .= self::tag('label', ['for' => $input_attrs['id']]);
$out .= Text::limitedSubsetHtml($label);
$out .= '</label>';
$out .= '</div>';
return $out;
}
/**
* Generates a page dropdown selection menu
*
* Just a wrapper for {@see Fb::dropdownTree}
*
* @param string $name The field name
* @param array $attrs Extra attributes for the SELECT element
* @param array $options Zero or more field options;
* 'subsite' int Subsite ID to load; default is current subsite
* 'exclude' array Node IDs to exclude from rendering
* @return string HTML
*/
public static
function pageDropdown
($name, array $attrs = [], array $options = []) {
if (empty($options['subsite'])) { $options['subsite'] = $_SESSION['admin']['active_subsite'];
}
$options['root'] = Navigation::loadPageTree($options['subsite'], true, false);
return self::dropdownTree($name, $attrs, $options);
}
/**
* Generates a dropdown selection menu from a Treenode and its children
*
* @param string $name The field name
* @param array $attrs Extra attributes for the SELECT element
* The special attribute "-dropdown-top" sets the label for the top item
* Use an empty string for no top item
* @param array $options One or more field options;
* 'root' Treenode Tree root - Required
* 'exclude' array Node IDs to exclude from rendering
* @return string HTML
*/
public static
function dropdownTree
($name, array $attrs = [], array $options = []) {
if (isset($attrs['-dropdown-top'])) { self::$dropdown_top = $attrs['-dropdown-top'];
unset($attrs['-dropdown-top']); }
$value = self::getData($name);
if (empty($options['root']) or
!($options['root'] instanceof Treenode
)) { throw new InvalidArgumentException('Option "root" is required and must be a Treenode');
}
if (empty($options['exclude'])) { $options['exclude'] = [];
}
$attrs['name'] = $name;
$field = self::tag('select', $attrs);
if (self::$dropdown_top) {
$field .= '<option value="" class="dropdown-top">';
$field .= Enc::html(self::$dropdown_top) . '</option>';
}
foreach ($options['root']->children as $child) {
$field .= self::dropdownTreeItem($child, 0, $value, $options['exclude']);
}
$field .= '</select>';
// Revert to default top dropdown item
self::$dropdown_top = 'Select an option';
return $field;
}
/**
* Used internally for recursive dropdown generation.
*
* @param Pagenode $node The node to display
* @param int $depth The depth of the node
* @param int $selected The id of the page to select
* @param array $exclude Node IDs of the to exclude from the list
* @return string HTML
*/
private static function dropdownTreeItem($node, $depth, $selected, $exclude)
{
$space = str_repeat(' ', $depth); $name = Enc::html($node['name']);
if (in_array($node['id'], $exclude)) return '';
if ($node['id'] == $selected) {
$out = "<option value=\"{$node['id']}\" selected>{$space}{$name}</option>";
} else {
$out = "<option value=\"{$node['id']}\">{$space}{$name}</option>";
}
foreach ($node->children as $child) {
$out .= self::dropdownTreeItem($child, $depth + 1, $selected, $exclude);
}
return $out;
}
/**
* Renders HTML containing a date selection UI. Output field value is in YYYY-MM-DD
*
* @todo Mobile to use a "date" field instead, for native UI
* @throws ValidationException If 'min', 'max' or 'incr' options are invalid
* @param string $name The field name
* @param array $attrs Attributes for the input element, e.g. ['id' => 'my-timepicker', class' => 'super-input', 'style' => 'font-style: italic']
* @param array $options Customisation options
* 'min' => the earliest selectable date, format YYYY-MM-DD
* 'max' => the latest selectable date, format YYYY-MM-DD
* 'dropdowns' => bool, true to include dropdowns for the month and year
* @return string HTML
*/
public static
function datepicker
($name, array $attrs = [], array $options = []) {
$value = self::getData($name);
if ($value == '0000-00-00') $value = '';
if (isset($options['min'])) Validity
::dateMySQL($options['min']); if (isset($options['max'])) Validity
::dateMySQL($options['max']); if (!isset($attrs['autocomplete'])) $attrs['autocomplete'] = 'off';
self::injectId($attrs);
self::addAttr($attrs, 'class', 'textbox fb-datepicker');
self::addAttr($attrs, 'type', 'text');
foreach ($options as $key => $val) {
$attrs['data-' . $key] = $val;
}
$out = '<div class="field-clearable__wrap">';
$out .= self::tag('input', [
'name' => $name, 'value' => $value, 'type' => 'hidden', 'class' => 'fb-hidden'
]);
$out .= self::tag('input', $attrs);
$out .= self::tag('button', [
'type' => 'button',
'class' => 'field-clearable__clear fb-clear',
]);
$out .= '</div>';
return $out;
}
/**
* Renders a date range picker. Output is in the form of two fields (given in name as a comma separated list) e.g.
* name => date_start, date_end will result in two fields: date_start => YYYY-MM-DD, date_end => YYYY-MM-DD
*
* @example
* echo Fb::daterangepicker('date_from,date_to', [], ['min' => '2000-01-01']);
*
* @param string $name The field name prefix
* @param array $attrs Attributes for the input element, e.g. ['class' => 'super-input', 'style' => 'font-style: italic']
* @param array $options Customisation options
* 'min' => the minimum of this date range.
* 'max' => the maximum of this date range.
* 'dropdowns' => display the dropdown date selectors.
* 'dir' => Either "future" or "past", for the direction of the pre-configured ranges. Default "future"
* @return string The rendered HTML
*/
public static
function daterangepicker
($name, array $attrs = [], array $options = []) {
if (count($names) != 2) { throw new InvalidArgumentException("daterangepicker expects name ({$name}) to be in the form of two comma-separated identifiers; e.g. 'date_start,date_end'");
}
list($name_start, $name_end) = $names;
if (!isset($options['dir'])) $options['dir'] = 'future';
if (isset($options['min'])) Validity
::dateMySQL($options['min']); if (isset($options['max'])) Validity
::dateMySQL($options['max']); if (!isset($attrs['autocomplete'])) $attrs['autocomplete'] = 'off';
self::injectId($attrs);
self::addAttr($attrs, 'class', 'textbox fb-daterangepicker');
foreach ($options as $key => $val) {
$attrs['data-' . $key] = $val;
}
$out = self::input('hidden', $name_start, ['class' => 'fb-hidden fb-daterangepicker--start']);
$out .= self::input('hidden', $name_end, ['class' => 'fb-hidden fb-daterangepicker--end']);
$out .= self::input('text', $name_start . '_to_' . $name_end . '_picker', $attrs);
return $out;
}
/**
* Renders simplified date range picker,
* Output is in the form of two fields (given in name as a comma separated list) e.g.
* name => date_start, date_end will result in two fields: date_start => YYYY-MM-DD, date_end => YYYY-MM-DD
*
* @param string $name The field name prefix
* @param array $attrs Attributes for the input element, e.g. ['class' => 'super-input', 'style' => 'font-style: italic']
* 'data-callback' => 'myCallBack' JS function name to be called upon dates updated
* Useage: myCallBack(date_from, date_to) { date_from = date_from.format('YYYY-M-D'); date_to = date_to.format('YYYY-M-D'); }
* @param array $options Customisation options
* 'min' => the minimum of this date range.
* 'max' => the maximum of this date range.
*
* @return string The rendered HTML
*/
public static
function simpledaterangepicker
($name, array $attrs = [], array $options = []) {
if (count($names) != 2) { throw new InvalidArgumentException("simpledaterangepicker expects name ({$name}) to be in the form of two comma-separated identifiers; e.g. 'date_start,date_end'");
}
list($name_start, $name_end) = $names;
if (isset($options['min'])) Validity
::dateMySQL($options['min']); if (isset($options['max'])) Validity
::dateMySQL($options['max']); if (!isset($attrs['autocomplete'])) $attrs['autocomplete'] = 'off'; if (!isset($attrs['data-callback'])) $attrs['data-callback'] = '';
self::injectId($attrs);
self::addAttr($attrs, 'class', 'textbox fb-simpledaterangepicker');
foreach ($options as $key => $val) {
if ($key != 'locale') {
$attrs['data-' . $key] = $val;
} else {
$attrs['data-locale'] = json_encode($options['locale']); }
}
$out = self::input('hidden', $name_start, ['class' => 'fb-hidden fb-daterangepicker--start']);
$out .= self::input('hidden', $name_end, ['class' => 'fb-hidden fb-daterangepicker--end']);
$out .= self::input('text', $name_start . '_to_' . $name_end . '_picker', $attrs);
return $out;
}
/**
* Renders a datetime range picker
*
* Output is in the form of two fields (given in name as a comma separated list) e.g.
* $name = 'date_start,date_end' will result in two fields:
* date_start => YYYY-MM-DD HH:MM:SS, date_end => YYYY-MM-DD HH:MM:SS
*
* @param string $name The field name prefix
* @param array $attrs Attributes for the input element, e.g. ['class' => 'super-input', 'style' => 'font-style: italic']
* @param array $options Additional options:
* 'min' Minimum datetime in YYYY-MM-DD HH:MM:SS format
* 'max' Maximum datetime in YYYY-MM-DD HH:MM:SS format
* 'incr' Time increment in minutes. Default 1
* 'dir' Either "future" or "past", for the direction of the pre-configured ranges. Default "future"
* 'dropdowns' => display the dropdown date selectors.
* @return string The rendered HTML
*/
public static
function datetimerangepicker
($name, array $attrs = [], array $options = []) {
if (count($names) != 2) { throw new InvalidArgumentException("datetimerangepicker expects name ({$name}) to be in the form of
two comma-separated identifiers; e.g. 'datetime_start,datetime_end'");
}
list($name_start, $name_end) = $names;
if (!isset($options['dir'])) $options['dir'] = 'future';
if (isset($options['min'])) Validity
::dateTimeMySQL($options['min']); if (isset($options['max'])) Validity
::dateTimeMySQL($options['max']); if (!isset($attrs['autocomplete'])) $attrs['autocomplete'] = 'off';
self::injectId($attrs);
self::addAttr($attrs, 'class', 'textbox fb-datetimerangepicker');
foreach ($options as $key => $val) {
$attrs['data-' . $key] = $val;
}
$out = self::input('hidden', $name_start, ['class' => 'fb-hidden fb-datetimerangepicker--start']);
$out .= self::input('hidden', $name_end, ['class' => 'fb-hidden fb-datetimerangepicker--end']);
$out .= self::input('text', $name_start . '_to_' . $name_end . '_picker', $attrs);
return $out;
}
/**
* Renders a timepicker field inside a SPAN, which displays a dropdown date selection box when clicked
*
* @param string $name The name of the field
* @param array $attrs Attributes for the input element, e.g. ['id' => 'my-timepicker', class' => 'super-input', 'style' => 'font-style: italic']
* @param array $params Additional options:
* 'min' Minimum allowed time in 24-hour format with a colon, e.g. '07:00' for 7am
* 'max' Maximum allowed time in 24-hour format with a colon, e.g. '20:30' for 8:30pm
* 'increment' Time increments e.g. 30 is 30 minute increments
* @return string HTML
*/
public static
function timepicker
($name, array $attrs = [], array $params = []) {
$value = self::getData($name);
if (!isset($params['min'])) $params['min'] = '00:00'; if (!isset($params['max'])) $params['max'] = '23:59'; if (!isset($params['increment'])) $params['increment'] = 30; if (!isset($attrs['autocomplete'])) $attrs['autocomplete'] = 'off'; $params['increment'] = (int) $params['increment'];
self::injectId($attrs);
$id = Enc::id($attrs['id']);
$out = "<span id=\"{$id}_wrap\" class=\"fb-timepicker\" data-config=\"" . Enc
::html(json_encode($params)) . "\">";
self::addAttr($attrs, 'name', $name . '_widget');
self::addAttr($attrs, 'type', 'text');
self::addAttr($attrs, 'class', 'textbox timepicker tm');
self::addAttr($attrs, 'autocomplete', 'off');
$out .= self::tag('input', $attrs, []);
$out .= "<input type=\"hidden\" name=\"{$name}\" value=\"" . Enc::html($value) . "\" class=\"hid\">";
$out .= "</span>";
return $out;
}
/**
* Renders HTML containing a date-time selection UI. Output field value is in YYYY-MM-DD HH:MM:SS
*
* @todo Mobile to use a "datetime-local" field instead, for native UI
* @throws ValidationException If 'min', 'max' or 'incr' options are invalid
* @param string $name The field name
* @param array $attrs Attributes for the input element, e.g. ['id' => 'my-timepicker', class' => 'super-input', 'style' => 'font-style: italic']
* @param array $settings Various settings
* 'min' Minimum datetime in YYYY-MM-DD HH:MM:SS format
* 'max' Maximum datetime in YYYY-MM-DD HH:MM:SS format
* 'incr' Time increment in minutes. Default 1
* 'dropdowns' => display the dropdown date selectors.
* @return string HTML
*/
public static
function datetimepicker
($name, array $attrs = [], array $options = []) {
if (isset($options['min'])) Validity
::datetimeMySQL($options['min']); if (isset($options['max'])) Validity
::datetimeMySQL($options['max']); if (isset($options['incr'])) Validity
::range($options['incr'], 1, 59); if (!isset($attrs['autocomplete'])) $attrs['autocomplete'] = 'off';
self::injectId($attrs);
self::addAttr($attrs, 'class', 'textbox fb-datetimepicker');
foreach ($options as $key => $val) {
$attrs['data-' . $key] = $val;
}
$out = self::input('hidden', $name, ['class' => 'fb-hidden']);
$out .= self::input('text', $name . '_picker', $attrs);
return $out;
}
/**
* Renders HTML containing a total selector UI. Output field value for the total is in
* a hidden field. The specific counts for each are also available
*
* @todo Does this need validation exceptions? I.e. min/max attributes invalid?
* @param string $name The field name
* @param array $attrs Attributes for the input element,
* e.g. ['id' => 'my-totalselector', class' => 'super-input', 'style' => 'font-style: italic']
* @param array $options Various options
* 'singular' Label for total
* 'plural' Plural label for total
* 'fields' Array of fields that contribute to the total count
* 'name' Internal name of field, plaintext
* 'label' Field label (Sentence case), plaintext
* 'helptext' Additional helptext for the field, optional, limited subset html
* 'min' Minimum allowed value, optional, default 0
* 'max' Maximum allowed value, optional, default unlimited
* @return string HTML
*/
public static
function totalselector
($name, array $attrs = [], array $options = []) {
self::injectId($attrs);
self::addAttr($attrs, 'class', 'textbox total-selector__output');
self::addAttr($attrs, 'readonly', true);
if (isset($options['fields'])) { $fields = $options['fields'];
unset($options['fields']); }
foreach ($options as $key => $val) {
$attrs['data-' . $key] = $val;
}
$out = self::input('text', $name, $attrs) . PHP_EOL;
$out .= '<div class="field-element--totalselector__fields">' . PHP_EOL;
foreach ($fields as $val) {
$sub_attrs = [];
$sub_attrs['type'] = 'number';
$sub_attrs['class'] = 'textbox';
$sub_attrs['id'] = $attrs['id'] . '-' . strtolower($val['name']); $sub_attrs['name'] = $val['name'];
$sub_attrs['value'] = self::getData($val['name']);
$sub_attrs['min'] = (int) @$val['min'];
if (isset($val['max'])) { $sub_attrs['max'] = (int) @$val['max'];
}
$out .= '<div class="field-element field-element--number">' . PHP_EOL;
$out .= '<div class="field-label">' . PHP_EOL;
$out .= '<label for="' . Enc::html($sub_attrs['id']) .'">' . Enc::html($val['label']) . '</label>' . PHP_EOL;
if (!empty($val['helptext'])) { $out .= '<div class="field-helper">' . Text::limitedSubsetHtml($val['helptext']) . '</div>' . PHP_EOL;
}
$out .= '</div>' . PHP_EOL;
$out .= '<div class="field-input">' . PHP_EOL;
$out .= Fb::tag('input', $sub_attrs) . PHP_EOL;
$out .= '</div>' . PHP_EOL;
$out .= '</div>' . PHP_EOL;
}
$out .= '</div>' . PHP_EOL;
return $out;
}
/**
* Renders a colour picker
*
* Uses the HTML5 'color' input type, and loads a JS fallback (spectrum)
* http://bgrins.github.io/spectrum/
* Note that spectrum requires jQuery 1.6 or later
*
* @param string $name The name of the input field
* @param array $attrs Extra attributes for the input field
* @param array $params Additional options; unused
* @return string
*/
public static
function colorpicker
($name, array $attrs = [], array $params = []) {
self::injectId($attrs);
self::addAttr($attrs, 'class', 'textbox colorpicker');
return self::input('color', $name, $attrs);
}
/**
* Render map location selector
* Zoom field is optional
*
* @wrap-in-fieldset
* @param string $name Field names, comma separated, latitude,longitude,zoom
* @param array $attrs Unused
* @param array $params Unused
* @return string HTML
*/
public static
function googleMap
($name, array $attrs = [], array $params = []) {
$view = new View('sprout/components/fb_google_map');
$view->names = explode(',', $name);
$view->values = [];
foreach ($view->names as $name) {
$view->values[] = self::getData($name);
}
// Remove zero values to avoid a pin in the middle of the ocean
if ($view->values[0] == 0 and $view->values[1] == 0) {
$view->values[0] = '';
$view->values[1] = '';
}
return $view->render();
}
/**
* A conditions list, which is an interface for building rules for
* use in dynamic IF-statement style systems.
*
* Output POST data will be a JSON string of the condition rules,
* as an array of objects with the keys 'field', 'op', 'val' for
* each condition.
*
* There are two parameters:
* fields array Available field types, name => label
* url string AJAX lookup method which returns the
* operator and value lists
*
* The lookup url is provided GET params 'field', 'op', 'val' and
* should output JSON with two keys, 'op' and 'val', which are both
* strings containing HTML for the fields; the op field should be
* a SELECT and the val field should be an INPUT or a SELECT.
*
* @wrap-in-fieldset
* @param string $name Field name
* @param array $attrs Unused
* @param array $params Array with two params, 'fields' and 'url'
* @return string HTML
*/
public static
function conditionsList
($name, array $attrs = [], array $params = []) {
$data = self::getData($name);
if (empty($data)) $data = '[]';
$view = new View('sprout/components/fb_conditions_list');
$view->name = $name;
$view->params = $params;
$view->data = $data;
return $view->render();
}
/**
* Renders google autocomplete address fields
*
* @param string $name Field
* @param array $attrs Attributes for the input element
* @param array $params Config options
* ```js
* {
* fields: {street: field-name, city: field-name, state: field-name, postcode: field-name, country: field-name},
* restrictions: { country: ['AU'] }
* }
* ```
* OR assume $param is just the $fields component (fallback, deprecated)
*
* Note: 'restrictions' are defined here:
* https://developers.google.com/maps/documentation/javascript/reference/places-autocomplete-service#ComponentRestrictions
* @return string HTML
*/
public static
function autoCompleteAddress
($name, array $attrs = [], array $options = []) {
Needs::googlePlaces();
self::injectId($attrs);
self::addAttr($attrs, 'class', 'textbox js-autocomplete-address');
self::addAttr($attrs, 'autocorrect', 'off');
if (!isset($options['fields']) and
!isset($options['restrictions'])) { $options = ['fields' => $options];
}
$view = new View('sprout/components/fb_autocomplete_address');
$view->options = $options;
$view->form_field = self::input('text', $name, $attrs);
return $view->render();
}
/**
* Renders place name geocoding fields
*
* @param string $name Field
* @param array $attrs Attributes for the input elements
* @param array $options Config options
* ```js
* {
* fields: {street: field-name, city: field-name, state: field-name, postcode: field-name, country: field-name},
* restrictions: { country: 'AU' }
* }
* ```
* Beware: The restrictions cannot accept a country list like autoCompleteAddress().
*
* Note: 'restrictions are defined here:
* https://developers.google.com/maps/documentation/javascript/reference/geocoder#GeocoderComponentRestrictions
*
* @return string HTML
*/
public static
function geocodeAddress
($name, array $attrs = [], array $options = []) {
Needs::googlePlaces();
self::injectId($attrs);
self::addAttr($attrs, 'class', 'textbox js-geocode-address');
self::addAttr($attrs, 'autocorrect', 'off');
$view = new View('sprout/components/fb_geocode_address');
$view->options = $options;
$view->form_field = self::input('text', $name, $attrs);
return $view->render();
}
/**
* Render a 'generate code' button + text field
*
* @param mixed $name Field
* @param array $attrs Attributes for the input element
* @param array $options Settings
* @return void
*/
public static
function randomCode
($name, array $attrs = [], array $options = []) {
self::injectId($attrs);
self::addAttr($attrs, 'class', 'textbox column column-9');
self::addAttr($attrs, 'autocorrect', 'off');
self::addAttr($attrs, 'autocomplete', 'off');
$defaults = [
'size' => 10,
'readable' => false,
'uppercase' => true,
'lowercase' => true,
'numbers' => true,
'symbols' => false,
];
$view = new View('sprout/components/fb_random_code');
$view->form_id = $attrs['id'];
$view->form_field = self::input('text', $name, $attrs);
return $view->render();
}
/**
* UI for selecting or drag-and-drop uploading one or more files.
*
* The field (refrenced by $name) is an array. If it's passed a a string, it will be comma-separated into an array.
* As JsonForm will auto-convert arrays into comma-separated strings, this field can easily be used with a MySQL
* field of type TEXT.
*
* You cannot have more than one of these on the page at a time
*
* This field WILL NOT operate in a non-admin environment
*
* @param string $name Field name. If [] is not at the end, this will be appended.
* @param array $attrs Unused
* @param array $options Includes the following:
* 'filter': (int) One of the filters, e.g. {@see FileConstants}::TYPE_IMAGE
* @return string HTML
*/
public static
function multipleFileSelect
($name, array $attrs = [], array $options = []) {
$data = self::getData($name);
if (empty($data)) $data = [];
}
$ids = [];
foreach ($data as $id) {
if (preg_match('/^[0-9]+$/', $id)) $ids[] = (int
) $id; }
$filenames = [];
$params = [];
$where = Pdb::buildClause([['id', 'IN', $ids]], $params);
$filenames = Pdb::q("SELECT id, filename FROM ~files", $params, 'map');
}
if (substr($name, -2) != '[]') $name .= '[]';
$opts['chunk_url'] = 'admin/call/file/ajaxDragdropChunk';
$opts['done_url'] = 'admin/call/file/ajaxDragdropDone';
$opts['form_url'] = 'admin/call/file/ajaxDragdropForm';
$opts['cancel_url'] = 'admin/call/file/ajaxDragdropCancel';
$opts['form_params'] = [];
$opts['max_files'] = 100;
$view = new View('sprout/components/multiple_file_select');
$view->opts = $opts;
$view->name = $name;
$view->data = $data;
$view->filenames = $filenames;
$view->filter = (int) @$options['filter'];
return $view->render();
}
/**
* Generates the title for a field, possibly enclosing it in a label, possibly with a generated ID
*
* @deprecated This method is likely to be removed at any given moment.
* Please use {@see Form::nextFieldDetails} instead.
*
* @param string $title The title of the field
* @param string|null $id The id to use. Empty string to auto-generate an id; false to disable the enclosing label,
* e.g. for a field which needs multiple inputs (such as a datepicker). The id will be used on the next
* input to be generated.
* @return string Possibly a LABEL element, or otherwise HTML text
*/
public static function title($title, $id = '')
{
if ($id === false) return Enc::html($title);
if ($id) {
self::$field_id = $id;
} else {
self::$field_id = $id = self::genId();
}
return '<label for="' . Enc::html(self::$field_id) . '">' . Enc::html($title) . '</label>';
}
/**
* Renders a set of hidden fields
* @param array $fields Field name-value pairs
* @return string Several INPUT fields of type hidden
*/
public static
function hiddenFields
(array $fields) {
$out = '';
foreach ($fields as $key => $val) {
$key = Enc::html($key);
$val = Enc::html($val);
$out .= "<input type=\"hidden\" name=\"{$key}\" value=\"{$val}\">\n";
}
return $out;
}
}