artabro/wire/core/Inputfield.php
2024-08-27 11:35:37 +02:00

2175 lines
74 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php namespace ProcessWire;
/**
* ProcessWire Inputfield - base class for Inputfield modules.
*
* ProcessWire 3.x, Copyright 2021 by Ryan Cramer
* https://processwire.com
*
* An Inputfield for an actual form input field widget, and this is provided as the base class
* for different kinds of form input widgets provided as modules.
*
* The class supports a parent/child hierarchy so that a given Inputfield can contain Inputfields
* below it. An example would be the relationship between fieldsets and fields in a form.
* Parent Inputfields are almost always of type InputfieldWrapper.
*
* An Inputfield is typically associated with a Fieldtype module when used for ProcessWire fields.
* Most Inputfields can also be used on their own.
*
* #pw-order-groups attribute-methods,attribute-properties,settings,traversal,labels,appearance,uikit,behavior,other,output,input,states
* #pw-use-constants
* #pw-summary Inputfield is the base class for modules that collect user input for fields.
* #pw-summary-attribute-properties These properties are retrieved or manipulated via the attribute methods above.
* #pw-summary-textFormat-constants Constants for formats allowed in description, notes, label.
* #pw-summary-collapsed-constants Constants allowed for the `Inputfield::collapsed` property.
* #pw-summary-skipLabel-constants Constants allowed for the `Inputfield::skipLabel` property.
* #pw-summary-renderValue-constants Options for `Inputfield::renderValueFlags` property, applicable `Inputfield::renderValue()` method call.
* #pw-summary-module Methods primarily of interest during module development.
* #pw-summary-uikit Settings for Inputfields recognized and used by AdminThemeUikit.
*
* #pw-body =
* ~~~~~
* // Create an Inputfield
* $inputfield = $modules->get('InputfieldText');
* $inputfield->label = 'Your Name';
* $inputfield->attr('name', 'your_name');
* $inputfield->attr('value', 'Roderigo');
* // Add to a $form (InputfieldForm or InputfieldWrapper)
* $form->add($inputfield);
* ~~~~~
* #pw-body
*
* ATTRIBUTES
* ==========
* @property string $name HTML 'name' attribute for Inputfield (required). #pw-group-attribute-properties
* @property string $id HTML 'id' attribute for the Inputfield (if not yet, determined automatically). #pw-group-attribute-properties
* @property mixed $value HTML 'value' attribute for the Inputfield. #pw-group-attribute-properties
* @property string $class HTML 'class' attribute for the Inputfield. #pw-group-attribute-properties
*
* @method string|Inputfield name($name = null) Get or set the name attribute. @since 3.0.110 #pw-group-attribute-methods
* @method string|Inputfield id($id = null) Get or set the id attribute. @since 3.0.110 #pw-group-attribute-methods
* @method string|Inputfield class($class = null) Get class attribute or add a class to the class attribute. @since 3.0.110 #pw-group-attribute-methods
*
* LABELS & CONTENT
* ================
* @property string $label Primary label text that appears above the input. #pw-group-labels
* @property string $description Optional description that appears under label to provide more detailed information. #pw-group-labels
* @property string $notes Optional notes that appear under input area to provide additional notes. #pw-group-labels
* @property string $detail Optional text details that appear under notes. @since 3.0.140 #pw-group-labels
* @property string $icon Optional font-awesome icon name to accompany label (excluding the "fa-") part). #pw-group-labels
* @property string $requiredLabel Optional custom label to display when missing required value. @since 3.0.98 #pw-group-labels
* @property string $head Optional text that appears below label but above description (only used by some Inputfields). #pw-internal
* @property string $tabLabel Label for tab if Inputfield rendered in its own tab via Inputfield::collapsedTab* setting. @since 3.0.201 #pw-group-labels
* @property string|null $prependMarkup Optional markup to prepend to the Inputfield content container. #pw-group-other
* @property string|null $appendMarkup Optional markup to append to the Inputfield content container. #pw-group-other
*
* @method string|Inputfield label($label = null) Get or set the 'label' property via method. @since 3.0.110 #pw-group-labels
* @method string|Inputfield description($description = null) Get or set the 'description' property via method. @since 3.0.110 #pw-group-labels
* @method string|Inputfield notes($notes = null) Get or set the 'notes' property via method. @since 3.0.110 #pw-group-labels
* @method string|Inputfield icon($icon = null) Get or set the 'icon' property via method. @since 3.0.110 #pw-group-labels
* @method string|Inputfield requiredLabel($requiredLabel = null) Get or set the 'requiredLabel' property via method. @since 3.0.110 #pw-group-labels
* @method string|Inputfield head($head = null) Get or set the 'head' property via method. @since 3.0.110 #pw-group-labels
* @method string|Inputfield prependMarkup($markup = null) Get or set the 'prependMarkup' property via method. @since 3.0.110 #pw-group-labels
* @method string|Inputfield appendMarkup($markup = null) Get or set the 'appendMarkup' property via method. @since 3.0.110 #pw-group-labels
*
* APPEARANCE
* ==========
* @property int $collapsed Whether the field is collapsed or visible, using one of the "collapsed" constants. #pw-group-appearance
* @property string $showIf Optional conditions under which the Inputfield appears in the form (selector string). #pw-group-appearance
* @property int $columnWidth Width of column for this Inputfield 10-100 percent. 0 is assumed to be 100 (default). #pw-group-appearance
* @property int $skipLabel Skip display of the label? See the "skipLabel" constants for options. #pw-group-appearance
*
* @method int|Inputfield collapsed($collapsed = null) Get or set collapsed property via method. @since 3.0.110 #pw-group-appearance
* @method string|Inputfield showIf($showIf = null) Get or set showIf selector property via method. @since 3.0.110 #pw-group-appearance
* @method int|Inputfield columnWidth($columnWidth = null) Get or set columnWidth property via method. @since 3.0.110 #pw-group-appearance
* @method int|Inputfield skipLabel($skipLabel = null) Get or set the skipLabel constant property via method. @since 3.0.110 #pw-group-appearance
*
* UIKIT THEME
* ===========
* @property bool|string $themeOffset Offset/margin for Inputfield, one of 's', 'm', or 'l'. #pw-group-uikit
* @property string $themeBorder Border style for Inputfield, one of 'none', 'card', 'hide' or 'line'. #pw-group-uikit
* @property string $themeInputSize Input size height/font within Inputfield, one of 's', 'm', or 'l'. #pw-group-uikit
* @property string $themeInputWidth Input width for text-type inputs, one of 'xs', 's', 'm', 'l', or 'f' (for full-width). #pw-group-uikit
* @property string $themeColor Color theme for Inputfield, one of 'primary', 'secondary', 'warning', 'danger', 'success', 'highlight', 'none'. #pw-group-uikit
* @property bool $themeBlank Makes <input> element display with no minimal container / no border when true. #pw-group-uikit
*
* SETTINGS & BEHAVIOR
* ===================
* @property int|bool $required Set to true (or 1) to make input required, or false (or 0) to make not required (default=0). #pw-group-behavior
* @property string $requiredIf Optional conditions under which input is required (selector string). #pw-group-behavior
* @property int|bool|null $requiredAttr Use HTML5 “required” attribute when used by Inputfield and $required is true? Default=null. #pw-group-behavior
* @property InputfieldWrapper|null $parent The parent InputfieldWrapper for this Inputfield or null if not set. #pw-internal
* @property null|bool|Fieldtype $hasFieldtype The Fieldtype using this Inputfield, or boolean false when known not to have a Fieldtype, or null when not known. #pw-group-other
* @property null|Field $hasField The Field object associated with this Inputfield, or null when not applicable or not known. #pw-group-other
* @property null|Page $hasPage The Page object associated with this Inputfield, or null when not applicable or not known. #pw-group-other
* @property null|Inputfield $hasInputfield If this Inputfield is owned/managed by another (other than parent/child relationship), it may be set here. 3.0.176+ #pw-group-other
* @property bool|null $useLanguages When multi-language support active, can be set to true to make it provide inputs for each language, where supported (default=false). #pw-group-behavior
* @property null|bool|int $entityEncodeLabel Set to boolean false to specifically disable entity encoding of field header/label (default=true). #pw-group-output
* @property null|bool $entityEncodeText Set to boolean false to specifically disable entity encoding for other text: description, notes, etc. (default=true). #pw-group-output
* @property int $renderFlags Options that can be applied to render, see "render*" constants (default=0). #pw-group-output 3.0.204+
* @property int $renderValueFlags Options that can be applied to renderValue mode, see "renderValue" constants (default=0). #pw-group-output
* @property string $wrapClass Optional class name (CSS) to apply to the HTML element wrapping the Inputfield. #pw-group-other
* @property string $headerClass Optional class name (CSS) to apply to the InputfieldHeader element #pw-group-other
* @property string $contentClass Optional class name (CSS) to apply to the InputfieldContent element #pw-group-other
* @property string $addClass Formatted class string letting you add class to any of the above (see addClass method). #pw-group-other 3.0.204+
* @property int|null $textFormat Text format to use for description/notes text in Inputfield (see textFormat constants) #pw-group-output
*
* @method string|Inputfield required($required = null) Get or set required state. @since 3.0.110 #pw-group-behavior
* @method string|Inputfield requiredIf($requiredIf = null) Get or set required-if selector. @since 3.0.110 #pw-group-behavior
*
* @method string|Inputfield wrapClass($class = null) Get wrapper class attribute or add a class to it. @since 3.0.110 #pw-group-other
* @method string|Inputfield headerClass($class = null) Get header class attribute or add a class to it. @since 3.0.110 #pw-group-other
* @method string|Inputfield contentClass($class = null) Get content class attribute or add a class to it. @since 3.0.110 #pw-group-other
*
*
* HOOKABLE METHODS
* ================
* @method string render()
* @method string renderValue()
* @method void renderReadyHook(Inputfield $parent, $renderValueMode)
* @method Inputfield processInput(WireInputData $input)
* @method InputfieldWrapper getConfigInputfields()
* @method array getConfigArray()
* @method array getConfigAllowContext(Field $field)
* @method array exportConfigData(array $data) #pw-internal
* @method array importConfigData(array $data) #pw-internal
*
*/
abstract class Inputfield extends WireData implements Module {
/**
* Not collapsed (display as "open", default)
* #pw-group-collapsed-constants
*
*/
const collapsedNo = 0;
/**
* Collapsed unless opened
* #pw-group-collapsed-constants
*
*/
const collapsedYes = 1;
/**
* Collapsed only when blank
* #pw-group-collapsed-constants
*
*/
const collapsedBlank = 2;
/**
* Hidden, not rendered in the form at all
* #pw-group-collapsed-constants
*
*/
const collapsedHidden = 4;
/**
* Collapsed only when populated
* #pw-group-collapsed-constants
*
*/
const collapsedPopulated = 5;
/**
* Not collapsed, value visible but not editable
* #pw-group-collapsed-constants
*
*/
const collapsedNoLocked = 6;
/**
* Collapsed when blank, value visible but not editable
* #pw-group-collapsed-constants
*
*/
const collapsedBlankLocked = 7;
/**
* Collapsed unless opened (value becomes visible but not editable)
* #pw-group-collapsed-constants
*
*/
const collapsedYesLocked = 8;
/**
* Same as collapsedYesLocked, for backwards compatibility
* #pw-internal
*
*/
const collapsedLocked = 8;
/**
* Never collapsed, and not collapsible
* #pw-group-collapsed-constants
*
*/
const collapsedNever = 9;
/**
* Collapsed and dynamically loaded by AJAX when opened
* #pw-group-collapsed-constants
*
*/
const collapsedYesAjax = 10;
/**
* Collapsed only when blank, and dynamically loaded by AJAX when opened
* #pw-group-collapsed-constants
*
*/
const collapsedBlankAjax = 11;
/**
* Collapsed into a separate tab
* #pw-group-collapsed-constants
* @since 3.0.201
*
*/
const collapsedTab = 20;
/**
* Collapsed into a separate tab and AJAX loaded
* #pw-group-collapsed-constants
* @since 3.0.201
*
*/
const collapsedTabAjax = 21;
/**
* Collapsed into a separate tab and locked (not editable)
* #pw-group-collapsed-constants
* @since 3.0.201
*
*/
const collapsedTabLocked = 22;
/**
* Don't skip the label (default)
* #pw-group-skipLabel-constants
*
*/
const skipLabelNo = false;
/**
* Don't use a "for" attribute with the <label>
* #pw-group-skipLabel-constants
*
*/
const skipLabelFor = true;
/**
* Don't show a visible header (likewise, do not show the label)
* #pw-group-skipLabel-constants
*
*/
const skipLabelHeader = 2;
/**
* Skip rendering of the label when it is blank
* #pw-group-skipLabel-constants
*
*/
const skipLabelBlank = 4;
/**
* Do not render any markup for the header/label at all
* #pw-group-skipLabel-constants
* @since 3.0.139
*
*/
const skipLabelMarkup = 8;
/**
* Plain text: no type of markdown or HTML allowed
* #pw-group-textFormat-constants
*
*/
const textFormatNone = 2;
/**
* Only allow basic/inline markdown, and no HTML (default)
* #pw-group-textFormat-constants
*
*/
const textFormatBasic = 4;
/**
* Full markdown support with HTML also allowed
* #pw-group-textFormat-constants
*
*/
const textFormatMarkdown = 8;
/**
* Render flags: place first in render
* #pw-group-render-constants
*
*/
const renderFirst = 1;
/**
* Render flags: place last in render
* #pw-group-render-constants
*
*/
const renderLast = 2;
/**
* Render only the minimum output when in "renderValue" mode.
* #pw-group-renderValue-constants
*
*/
const renderValueMinimal = 2;
/**
* Indicates a parent InputfieldWrapper is not required when rendering value.
* #pw-group-renderValue-constants
*
*/
const renderValueNoWrap = 4;
/**
* If there are multiple items, only render the first (where supported by the Inputfield).
* #pw-group-renderValue-constants
*
*/
const renderValueFirst = 8;
/**
* The total number of Inputfield instances, kept as a way of generating unique 'id' attributes
*
* #pw-internal
*
*/
static protected $numInstances = 0;
/**
* Custom html for Inputfield output, if supported, and default overridden
*
* In the string specify {attr} to substitute a string of all attributes, or to
* specify attributes individually, specify name="{name}" replacing "name" in both
* cases with the actual name of the attribute.
*
* @var string
*
private $html = '';
*/
/**
* Attributes specified for the HTML output, like class, rows, cols, etc.
*
*/
protected $attributes = array();
/**
* Attributes that accompany this Inputfield's wrapping element
*
* @var array
*
*/
protected $wrapAttributes = array();
/**
* The parent Inputfield, if applicable
*
*/
protected $parent = null;
/**
* The default ID attribute assigned to this field
*
*/
protected $defaultID = '';
/**
* Whether this Inputfield is editable
*
* When false, its processInput method won't be called by InputfieldWrapper's processInput
*
* @var bool
*
*/
protected $editable = true;
/**
* Construct the Inputfield, setting defaults for all properties
*
*/
public function __construct() {
self::$numInstances++;
$this->set('label', ''); // primary clickable label
$this->set('description', ''); // descriptive copy, below label
$this->set('icon', ''); // optional icon name to accompany label
$this->set('notes', ''); // highlighted descriptive copy, below output of input field
$this->set('detail', ''); // text details that appear below notes
$this->set('head', ''); // below label, above description
$this->set('tabLabel', ''); // alternate label for tab when Inputfield::collapsedTab* in use
$this->set('required', 0); // set to 1 to make value required for this field
$this->set('requiredIf', ''); // optional conditions to make it required
$this->set('collapsed', ''); // see the collapsed* constants at top of class (use blank string for unset value)
$this->set('showIf', ''); // optional conditions selector
$this->set('columnWidth', ''); // percent width of the field. blank or 0 = 100.
$this->set('skipLabel', self::skipLabelNo); // See the skipLabel constants
$this->set('wrapClass', ''); // optional class to apply to the Inputfield wrapper (contains InputfieldHeader + InputfieldContent)
$this->set('headerClass', ''); // optional class to apply to InputfieldHeader wrapper
$this->set('contentClass', ''); // optional class to apply to InputfieldContent wrapper
$this->set('addClass', ''); // space-separated classes to add, optionally specifying element (see addClassString method)
$this->set('textFormat', self::textFormatBasic); // format applied to description and notes
$this->set('renderFlags', 0); // See render* constants
$this->set('renderValueFlags', 0); // see renderValue* constants, applicable to renderValue mode only
$this->set('prependMarkup', ''); // markup to prepend to Inputfield output
$this->set('appendMarkup', ''); // markup to append to Inputfield output
// default ID attribute if no 'id' attribute set
$this->defaultID = $this->className() . self::$numInstances;
$this->setAttribute('id', $this->defaultID);
$this->setAttribute('class', '');
$this->setAttribute('name', '');
$value = $this instanceof InputfieldHasArrayValue ? array() : null;
$this->setAttribute('value', $value);
parent::__construct();
}
/**
* Per the Module interface, init() is called after any configuration data has been populated to the Inputfield, but before render.
*
* #pw-group-module
*
*/
public function init() { }
/**
* Per the Module interface, this method is called when this Inputfield is installed
*
* #pw-group-module
*
*/
public function ___install() { }
/**
* Per the Module interface, uninstall() is called when this Inputfield is uninstalled
*
* #pw-group-module
*
*/
public function ___uninstall() { }
/**
* Multiple instances of a given Inputfield may be needed
*
* #pw-internal
*
*/
public function isSingular() {
return false;
}
/**
* Inputfields are not loaded until requested
*
* #pw-internal
*
*/
public function isAutoload() {
return false;
}
/**
* Set a property or attribute to the Inputfield
*
* - Use this for setting properties like parent, collapsed, required, columnWidth, etc.
* - You can also set properties directly via `$inputfield->property = $value`.
* - If setting an attribute (like name, id, etc.) this will work, but it is preferable to use the `Inputfield::attr()` method.
* - If setting any kind of "class" it is preferable to use the `Inputfield::addClass()` method.
*
* #pw-group-settings
*
* @param string $key Name of property to set
* @param mixed $value Value of property
* @return Inputfield|WireData
*
*/
public function set($key, $value) {
if($key === 'parent') {
if($value instanceof InputfieldWrapper) return $this->setParent($value);
} else if($key === 'collapsed') {
if($value === true) $value = self::collapsedYes;
$value = (int) $value;
} else if(array_key_exists($key, $this->attributes)) {
return $this->setAttribute($key, $value);
} else if($key === 'required' && $value && !is_object($value)) {
$this->addClass('required');
} else if($key === 'columnWidth') {
$value = (int) $value;
if($value < 10 || $value > 99) $value = '';
} else if($key === 'addClass') {
if(is_string($value) && !ctype_alnum($value)) {
$test = str_replace(array(' ', ':', ',', '-', '+', '=', '!', '_', '.', '@', "\n"), '', $value);
if(!ctype_alnum($test)) $value = preg_replace('/[^-+_:=@!,. a-zA-Z0-9\n]/', '', $value);
}
$this->addClass($value);
}
return parent::set($key, $value);
}
/**
* Get a property or attribute from the Inputfield
*
* - This can also be accessed directly, i.e. `$value = $inputfield->property;`.
*
* - For getting attribute values, this will work, but it is preferable to use the `Inputfield::attr()` method.
*
* - For getting non-attribute values that have potential name conflicts with attributes (or just as a
* reliable alternative), use the `Inputfield::getSetting()` method instead, which excludes the possibility
* of overlap with attributes.
*
* #pw-group-settings
*
* @param string $key Name of property or attribute to retrieve.
* @return mixed|null Value of property or attribute, or NULL if not found.
*
*/
public function get($key) {
if($key === 'label') {
$value = parent::get('label');
if(strlen($value)) return $value;
if($this->skipLabel & self::skipLabelBlank) return '';
return $this->attributes['name'];
}
if($key === 'description' || $key === 'notes') return parent::get($key);
if($key === 'name' || $key === 'value' || $key === 'id') return $this->getAttribute($key);
if($key === 'attributes') return $this->attributes;
if($key === 'parent') return $this->parent;
if(($value = $this->wire($key)) !== null) return $value;
if(array_key_exists($key, $this->attributes)) return $this->attributes[$key];
return parent::get($key);
}
/**
* Gets a setting (or API variable) from the Inputfield, while ignoring attributes.
*
* This is good to use in cases where there are potential name conflicts, like when there is a field literally
* named "collapsed" or "required".
*
* #pw-group-settings
*
* @param string $key Name of setting or API variable to retrieve.
* @return mixed Value of setting or API variable, or NULL if not found.
*
*/
public function getSetting($key) {
return parent::get($key);
}
/**
* Set the parent (InputfieldWrapper) of this Inputfield.
*
* #pw-group-traversal
*
* @param InputfieldWrapper $parent
* @return $this
* @see Inputfield::getParent()
*
*/
public function setParent(InputfieldWrapper $parent) {
$this->parent = $parent;
return $this;
}
/**
* Unset any previously set parent
*
* #pw-internal
* @return $this
*
*/
public function unsetParent() {
$this->parent = null;
return $this;
}
/**
* Get this Inputfields parent InputfieldWrapper, or NULL if it doesnt have one.
*
* #pw-group-traversal
*
* @return InputfieldWrapper|null
* @see Inputfield::setParent()
*
*/
public function getParent() {
return $this->parent;
}
/**
* Get array of all parents of this Inputfield.
*
* #pw-group-traversal
*
* @return array of InputfieldWrapper elements.
* @see Inputfield::getParent(), Inputfield::setParent()
*
*/
public function getParents() {
/** @var InputfieldWrapper|null $parent */
$parent = $this->getParent();
if(!$parent) return array();
$parents = array($parent);
foreach($parent->getParents() as $p) $parents[] = $p;
return $parents;
}
/**
* Get or set parent of Inputfield
*
* This convenience method performs the same thing as getParent() and setParent().
*
* To get parent, specify no arguments. It will return null if no parent assigned, or an
* InputfieldWrapper instance of the parent.
*
* To set parent, specify an InputfieldWrapper for the $parent argument. The return value
* is the current Inputfield for fluent interface.
*
* #pw-group-traversal
*
* @param null|InputfieldWrapper $parent
* @return null|Inputfield|InputfieldWrapper
* @since 3.0.110
*
*/
public function parent($parent = null) {
if($parent === null) {
return $this->getParent();
} else {
return $this->setParent($parent);
}
}
/**
* Get array of all parents of this Inputfield
*
* This is identical to and an alias of the getParents() method.
*
* #pw-group-traversal
*
* @return array
* @since 3.0.110
*
*/
public function parents() {
return $this->getParents();
}
/**
* Get the root parent InputfieldWrapper element (farthest parent, commonly InputfieldForm)
*
* This returns null only if Inputfield it is called from has not yet been added to an InputfieldWrapper.
*
* #pw-group-traversal
*
* @return InputfieldForm|InputfieldWrapper|null
* @since 3.0.106
*
*/
public function getRootParent() {
$parents = $this->getParents();
return count($parents) ? end($parents) : null;
}
/**
* Get the InputfieldForm element that contains this field or null if not yet defined
*
* This is the same as the `getRootParent()` method except that it returns null if root parent
* is not an InputfieldForm.
*
* #pw-group-traversal
*
* @return InputfieldForm|null
* @since 3.0.106
*
*/
public function getForm() {
$form = $this instanceof InputfieldForm ? $this : $this->getRootParent();
return ($form instanceof InputfieldForm ? $form : null);
}
/**
* Set an attribute
*
* - For most public API use, you might consider using the shorter `Inputfield::attr()` method instead.
*
* - When setting the `class` attribute it is preferable to use the `Inputfield::addClass()` method.
*
* - The `$key` argument may contain multiple keys by being specified as an array, or by being a string with multiple
* keys separated by "+" or "|", for example: `$inputfield->setAttribute("id+name", "template")`.
*
* - If the `$value` argument is an array, it will instruct the attribute to hold multiple values.
* Future calls to setAttribute() will enforce the array type for that attribute.
*
* ~~~~~
* // Set the name attribute
* $inputfield->setAttribute('name', 'my_field_name');
*
* // Set the name and id attributes at the same time
* $inputfield->setAttribute('name+id', 'my_field_name');
* ~~~~~
*
* #pw-internal Giving public API preference to the attr() method instead
*
* @param string|array $key Specify one of the following:
* - Name of attribute (string)
* - Names of attributes (array)
* - String with names of attributes split by "+" or "|"
* @param string|int|array|bool $value Value of attribute to set.
* @return $this
* @see Inputfield::attr(), Inputfield::removeAttr(), Inputfield::addClass()
*
*/
public function setAttribute($key, $value) {
if(is_array($key)) {
$keys = $key;
} else if(strpos($key, '+') !== false) {
$keys = explode('+', $key);
} else if(strpos($key, '|') !== false) {
$keys = explode('|', $key);
} else {
$keys = array($key);
}
if(is_bool($value) && !in_array($key, array('name', 'id', 'class', 'value', 'type'))) {
$booleanValue = $value;
} else {
$booleanValue = null;
}
foreach($keys as $key) {
if(!ctype_alpha("$key")) $key = $this->wire('sanitizer')->attrName($key);
if(empty($key)) continue;
if($booleanValue !== null) {
if($booleanValue === true) {
// boolean true attribute sets value as attribute name (i.e. checked='checked')
$value = $key;
} else if($booleanValue === false) {
// boolean false attribute implies remove attribute
$this->removeAttribute($key);
continue;
}
}
if($key === 'name' && strlen($value)) {
$idAttr = $this->getAttribute('id');
$nameAttr = $this->getAttribute('name');
if($idAttr == $this->defaultID || $idAttr == $nameAttr || $idAttr == "Inputfield_$nameAttr") {
// formulate an ID attribute that consists of the className and name attribute
$this->setAttribute('id', "Inputfield_$value");
}
}
if(!array_key_exists($key, $this->attributes)) {
$this->attributes[$key] = '';
}
if(is_array($this->attributes[$key]) && !is_array($value)) {
// If the attribute is already established as an array, then we'll keep it as an array
// and stack any newly added values into the array.
// Examples would be stacking of class attributes, or stacking of value attributes for
// an Inputfield that carries multiple values
$this->attributes[$key][] = $value;
} else {
$this->attributes[$key] = $value;
}
}
return $this;
}
/**
* Remove an attribute
*
* #pw-group-attribute-methods
*
* @param string $key Name of attribute to remove.
* @return $this
* @see Inputfield::attr(), Inputfield::removeClass()
*
*/
public function removeAttr($key) {
unset($this->attributes[$key]);
return $this;
}
/**
* Remove an attribute (alias)
*
* #pw-internal
*
* @param string $key
* @return $this
*
*/
public function removeAttribute($key) {
return $this->removeAttr($key);
}
/**
* Set multiple attributes at once with an associative array.
*
* #pw-internal
*
* @param array $attributes Associative array of attributes to set.
* @return $this
*
*/
public function setAttributes(array $attributes) {
foreach($attributes as $key => $value) $this->setAttribute($key, $value);
return $this;
}
/**
* Get or set an attribute (or multiple attributes)
*
* - To get an attribute call this method with just the attribute name.
* - To set an attribute call this method with the attribute name and value to set.
* - You can also set multiple attributes at once, see examples below.
* - To get all attributes, just specify boolean true as first argument (since 3.0.16).
*
* ~~~~~
* // Get the "value" attribute
* $value = $inputfield->attr('value');
*
* // Set the "value" attribute
* $inputfield->attr('value', 'Foo and Bar');
*
* // Set multiple attributes
* $inputfield->attr([
* 'name' => 'foobar',
* 'value' => 'Foo and Bar',
* 'class' => 'foo-bar',
* ]);
*
* // Set name and id attribute to "foobar"
* $inputfield->attr("name+id", "foobar");
*
* // Get all attributes in associative array (since 3.0.16)
* $attrs = $inputfield->attr(true);
* ~~~~~
*
* #pw-group-attribute-methods
*
* @param string|array|bool $key Specify one of the following:
* - Name of attribute to get (if getting an attribute).
* - Name of attribute to set (if setting an attribute, and also specifying a value).
* - Aassociative array to set multiple attributes.
* - String with attributes split by "+" or "|" to set them all to have the same value.
* - Specify boolean true to get all attributes in an associative array.
* @param string|int|bool|null $value Value to set (if setting), omit otherwise.
* @return Inputfield|array|string|int|object|float If setting an attribute, it returns this instance. If getting an attribute, the attribute is returned.
* @see Inputfield::removeAttr(), Inputfield::addClass(), Inputfield::removeClass()
*
*/
public function attr($key, $value = null) {
if(is_null($value)) {
if(is_array($key)) {
return $this->setAttributes($key);
} else if(is_bool($key)) {
return $this->getAttributes();
} else {
return $this->getAttribute($key);
}
}
return $this->setAttribute($key, $value);
}
/**
* Shortcut for getting or setting “value” attribute
*
* When setting a value, it returns $this (for fluent interface).
*
* ~~~~~
* $value = $inputfield->val(); * // Getting
* $inputfield->val('foo'); * // Setting
* ~~~~~
*
* @param string|null $value
* @return string|int|float|array|object|Wire|WireData|WireArray|Inputfield
*
*/
public function val($value = null) {
if($value === null) return $this->getAttribute('value');
return $this->setAttribute('value', $value);
}
/**
* If method call resulted in no handler, this hookable method is called.
*
* We use this to allow for attributes and properties to be set via method, useful primarily
* for fluent interface calls.
*
* #pw-internal
*
* @param string $method Requested method name
* @param array $arguments Arguments provided
* @return null|mixed Return value of method (if applicable)
* @throws WireException
* @since 3.0.110
*
*/
protected function ___callUnknown($method, $arguments) {
$arg = isset($arguments[0]) ? $arguments[0] : null;
if(isset($this->attributes[$method])) {
// get or set an attribute
return $arg === null ? $this->getAttribute($method) : $this->setAttribute($method, $arg);
} else if(($value = $this->getSetting($method)) !== null) {
// get or set a setting
if($arg === null) return $value;
if(stripos($method, 'class') !== false) {
// i.e. class, wrapClass, contentClass, etc.
return $this->addClass($arg, $method);
} else {
return $this->set($method, $arg);
}
}
return parent::___callUnknown($method, $arguments);
}
/**
* Get all attributes specified for this Inputfield
*
* #pw-internal
*
* @return array
*
*/
public function getAttributes() {
$attrs = $this->attributes;
if(!isset($attrs['required']) && $this->getSetting('required') && $this->getSetting('requiredAttr')) {
if(!$this->getSetting('showIf') && !$this->getSetting('requiredIf')) {
$attrs['required'] = 'required';
}
}
return $attrs;
}
/**
* Get a specified attribute for this Inputfield
*
* #pw-internal Public API should use the attr() method instead, but this is here for consistency with setAttribute()
*
* @param string $key
* @return mixed|null
*
*/
public function getAttribute($key) {
return isset($this->attributes[$key]) ? $this->attributes[$key] : null;
}
/**
* Get or set attribute for the element wrapping this Inputfield
*
* Use this method when you need to assign some attribute to the outer wrapper of the Inputfield.
*
* #pw-group-attribute-methods
*
* @param string|null|bool $key Specify one of the following for $key:
* - Specify string containing name of attribute to set.
* - Omit (or null or true) to get all wrap attributes as associative array.
* @param string|null|bool $value Specify one of the following for $value:
* - Omit if getting an attribute.
* - Value to set for $key of setting.
* - Boolean false to remove the attribute specified for $key.
* @return Inputfield|string|array|null Returns one of the following:
* - If getting, returns attribute value of NULL if not present.
* - If setting, returns $this.
* @see Inputfield::attr(), Inputfield::addClass()
*
*/
public function wrapAttr($key = null, $value = null) {
if(is_null($value)) {
if(is_null($key) || is_bool($key)) {
return $this->wrapAttributes;
} else {
return isset($this->wrapAttributes[$key]) ? $this->wrapAttributes[$key] : null;
}
} else if($value === false) {
unset($this->wrapAttributes[$key]);
return $this;
} else {
if(strlen($key)) $this->wrapAttributes[$key] = $value;
return $this;
}
}
/**
* Add a class or classes to this Inputfield (or a wrapping element)
*
* If given a class name thats already present, it wont be added again.
*
* ~~~~~
* // Add class "foobar" to input element
* $inputfield->addClass('foobar');
*
* // Add three classes to input element
* $inputfield->addClass('foo bar baz');
*
* // Add class "foobar" to .Inputfield wrapping element
* $inputfield->addClass('foobar', 'wrapClass');
*
* // Add classes while specifying Inputfield element (3.0.204+)
* $inputfield->addClass('wrap:card, header:card-header, content:card-body');
* ~~~~~
*
* **Formatted string option (3.0.204+):**
* Classes can be added by formatted string that dictates what Inputfield element they
* should be added to, in the format `element:classNames` like in this example below:
* ~~~~~
* wrap:card card-default
* header:card-header
* content:card-body
* input:form-input input-checkbox
* ~~~~~
* Each line represents a group containing an element name and one or more space-separated
* classes. Groups may be separated by newline (like above) or with a comma. The element
* name may be any one of the following:
*
* - `wrap`: The .Inputfield element that wraps the header and content
* - `header`: The .InputfieldHeader element, typically a `<label>`.
* - `content`: The .InputfieldContent element that wraps the input(s), typically a `<div>`.
* - `input`: The primary `<input>` element(s) that accept input for the Inputfield.
* - `class`: This is the same as the 'input' type, just an alias.
*
* Class names prefixed with a minus sign i.e. `-class` will be removed rather than added.
*
* #pw-group-attribute-methods
*
* @param string|array $class Specify one of the following:
* - Class name you want to add.
* - Multiple space-separated class names you want to add.
* - Array of class names you want to add (since 3.0.16).
* - Formatted string of classes as described in method description (since 3.0.204+).
* @param string $property Optionally specify the type of class you want to add:
* - Omit for the default (which is "class").
* - `class` (string): Add class to the input element (or whatever the Inputfield default is).
* - `wrapClass` (string): Add class to ".Inputfield" wrapping element, the most outer level element used for this Inputfield.
* - `headerClass` (string): Add class to ".InputfieldHeader" label element.
* - `contentClass` (string): Add class to ".InputfieldContent" wrapping element.
* - Or some other named class attribute designated by a descending Inputfield.
* - You can optionally omit the `Class` suffix in 3.0.204+, i.e. `wrap` rather than `wrapClass`.
* @return $this
* @see Inputfield::hasClass(), Inputfield::removeClass()
*
*/
public function addClass($class, $property = 'class') {
$force = strpos($property, '=') === 0; // force set, skip processing by addClassString
if($force) $property = ltrim($property, '=');
if(is_string($class) && !ctype_alnum($class) && !$force) {
if(strpos($class, ':') || strpos($class, "\n") || strpos($class, ",")) {
return $this->addClassString($class, $property);
}
}
$property = $this->getClassProperty($property);
$classes = $this->getClassArray($property, true);
// addClasses is array of classes being added
$addClasses = is_array($class) ? $class : explode(' ', $class);
// add to $classes array
foreach($addClasses as $addClass) {
$addClass = trim($addClass);
if(strlen($addClass)) $classes[$addClass] = $addClass;
}
// convert back to string
$value = trim(implode(' ', $classes));
// set back to Inputfield
if($property === 'class') {
$this->attributes['class'] = $value;
} else {
$this->set($property, $value);
}
return $this;
}
/**
* Add class(es) by formatted string that lets you specify where class should be added
*
* To use this in the public API use `addClass()` method or set the `addClass` property
* with a formatted string value as indicated here.
*
* Allows for setting via formatted string like:
* ~~~~~
* wrap:card card-default
* header:card-header
* content:card-body
* input:form-input input-checkbox
* ~~~~~
* Each line represents a group containing a element type, colon, and one or more space-
* separated classes. Groups may be separated by newline (like above) or with a comma.
* The element type may be any one of the following:
*
* - `wrap`: The .Inputfield element that wraps the header and content
* - `header`: The .InputfieldHeader element, typically a `<label>`.
* - `content`: The .InputfieldContent element that wraps the input(s), typically a `<div>`.
* - `input`: The primary `<input>` element(s) that accept input for the Inputfield.
* - `class`: This is the same as the 'input' type, just an alias.
* - `+foo`: Force adding your own new element type (i.e. “foo”) that is not indicated above.
*
* Class names prefixed with a minus sign i.e. `-class` will be removed rather than added.
*
* A string like `hello:world` where `hello` is not one of those element types listed above,
* and is not prefixed with a plus sign `+`, will be added as a literal class name with the
* colon in it (such as those used by Tailwind).
*
* @param string $class Formatted class string to parse class types and names from
* @param string $property Default/fallback element/property if not indicated in string
* @return self
* @since 3.0.204
*
*
*/
protected function addClassString($class, $property = 'class') {
if(ctype_alnum($class)) return $this->addClass($class, $property);
$typeNames = array('wrap', 'header', 'content', 'input', 'class');
$class = trim($class);
if(strpos($class, "\n")) $class = str_replace("\n", ",", $class);
$groups = strpos($class, ',') ? explode(',', $class) : array($class);
foreach($groups as $group) {
$type = $property;
$group = trim($group);
$classes = explode(' ', $group);
foreach($classes as $class) {
if(empty($class)) continue;
if(strpos($class, ':')) {
// setting new element type i.e. wrap:myclass or +foo:myclass
list($typeName, $className) = explode(':', $class, 2);
$typeName = trim($typeName);
if(in_array($typeName, $typeNames) || strpos($typeName, '+') === 0) {
// accepted as element/type for adding classes
$type = ltrim($typeName, '+');
$class = trim($className);
} else {
// literal class name with a colon in it such as "lg:bg-red-400'
}
}
if(strpos($class, '-') === 0) {
$this->removeClass(ltrim($class, '-'), $type);
} else {
$this->addClass($class, "=$type"); // "=type" prevents further processing
}
}
}
return $this;
}
/**
* Does this Inputfield have the given class name (or names)?
*
* ~~~~~
* if($inputfield->hasClass('foo')) {
* // This Inputfield has a class attribute with "foo"
* }
*
* if($inputfield->hasClass('bar', 'wrapClass')) {
* // This .Inputfield wrapper has a class attribute with "bar"
* }
*
* if($inputfield->hasClass('foo bar')) {
* // This Inputfield has both "foo" and "bar" classes (Since 3.0.16)
* }
* ~~~~~
*
* #pw-group-attribute-methods
*
* @param string|array $class Specify class name or one of the following:
* - String containing name of class you want to check (string).
* - String containing Space separated string class names you want to check, all must be present for
* this method to return true. (Since 3.0.16)
* - Array of class names you want to check, all must be present for this method to return true. (Since 3.0.16)
* @param string $property Optionally specify property you want to pull class from:
* - `class` (string): Default setting. Class for the input element (or whatever the Inputfield default is).
* - `wrapClass` (string): Class for the ".Inputfield" wrapping element, the most outer level element used for this Inputfield.
* - `headerClass` (string): Class for the ".InputfieldHeader" label element.
* - `contentClass` (string): Class for the ".InputfieldContent" wrapping element.
* - Or some other class property defined by a descending Inputfield class.
* @return bool
* @see Inputfield::addClass(), Inputfield::removeClass()
*
*/
public function hasClass($class, $property = 'class') {
if(is_string($class)) $class = trim($class);
// checking multiple classes
if(is_array($class) || strpos($class, ' ')) {
$classes = is_string($class) ? explode(' ', $class) : $class;
if(!count($classes)) return false;
$n = 0;
foreach($classes as $c) {
$c = trim($c);
if(empty($c) || $this->hasClass($c, $property)) $n++;
}
// return whether it had all the given classes
return $n === count($classes);
}
// checking single class
$classes = $this->getClassArray($property, true);
return isset($classes[$class]);
}
/**
* Get classes in array for given class property
*
* @param string $property One of 'wrap', 'header', 'content' or 'input' (or alias 'class')
* @param bool $assoc Return as associative array where both keys and values are class names? (default=false)
* @return array
* @since 3.0.204
*
*/
public function getClassArray($property = 'class', $assoc = false) {
$property = $this->getClassProperty($property);
$value = ($property === 'class' ? $this->attr('class') : $this->getSetting($property));
$value = trim("$value");
while(strpos($value, ' ') !== false) $value = str_replace(' ', ' ', $value);
$classes = strlen($value) ? explode(' ', $value) : array();
if($assoc) {
$a = array();
foreach($classes as $class) $a[$class] = $class;
$classes = $a;
}
return $classes;
}
/**
* Get the internal property name for given class property
*
* This converts things like 'wrap' to 'wrapClass', 'header' to 'headerClass', etc.
*
* @param string $property
* @return string
* @since 3.0.204
*
*/
protected function getClassProperty($property) {
if($property === 'class' || $property === 'input' || empty($property)) {
$property = 'class';
} else if(strpos($property, 'Class') === false) {
if(in_array($property, array('wrap', 'header', 'content'))) $property .= 'Class';
}
return $property;
}
/**
* Remove the given class (or classes) from this Inputfield
*
* ~~~~~
* // Remove the "foo" class
* $inputfield->removeClass('foo');
*
* // Remove both the "foo" and "bar" classes (since 3.0.16)
* $inputfield->removeClass('foo bar');
*
* // Remove the "bar" class from the wrapping .Inputfield element
* $inputfield->removeClass('bar', 'wrapClass');
* ~~~~~
*
* #pw-group-attribute-methods
*
* @param string|array $class Class name you want to remove or specify one of the following:
* - Single class name to remove.
* - Space-separated class names you want to remove (Since 3.0.16).
* - Array of class names you want to remove (Since 3.0.16).
* @param string $property Optionally specify the property you want to remove class from:
* - `class` (string): Default setting. Class for the input element (or whatever the Inputfield default is).
* - `wrapClass` (string): Class for the ".Inputfield" wrapping element, the most outer level element used for this Inputfield.
* - `headerClass` (string): Class for the ".InputfieldHeader" label element.
* - `contentClass` (string): Class for the ".InputfieldContent" wrapping element.
* - Or some other class property defined by a descending Inputfield class.
* @return $this
* @see Inputfield::addClass(), Inputfield::hasClass()
*
*/
public function removeClass($class, $property = 'class') {
$property = $this->getClassProperty($property);
$classes = $this->getClassArray($property, true);
$removeClasses = is_array($class) ? $class : explode(' ', $class);
foreach($removeClasses as $removeClass) {
if(strlen($removeClass)) unset($classes[$removeClass]);
}
if($property === 'class') {
$this->attributes['class'] = implode(' ', $classes);
} else {
$this->set($property, implode(' ', $classes));
}
return $this;
}
/**
* Get an HTML-ready string of all this Inputfields attributes
*
* ~~~~~
* // Outputs: name="foo" value="bar" class="baz"
* echo $inputfield->getAttributesString([
* 'name' => 'foo',
* 'value' => 'bar',
* 'class' => 'baz'
* ]);
*
* // Outputs actual attributes specified with this Inputfield
* echo $inputfield->getAttributesString();
* ~~~~~
*
* #pw-internal
*
* @param array $attributes Associative array of attributes to build the string from, or omit to use this Inputfield's attributes.
* @return string
*
*/
public function getAttributesString(array $attributes = null) {
$str = '';
// if no attributes provided then use the ones for this Inputfield by default
if(is_null($attributes)) $attributes = $this->getAttributes();
if($this instanceof InputfieldHasArrayValue) {
// fields that use arrays as values aren't going to be using a value attribute in this string, so skip it
unset($attributes['value']);
// tell PHP to return an array by adding [] to the name attribute, i.e. "myfield[]"
if(isset($attributes['name']) && substr($attributes['name'], -1) != ']') $attributes['name'] .= '[]';
}
foreach($attributes as $attr => $value) {
if(is_array($value)) {
// if an attribute has multiple values (like class), then bundle them into a string separated by spaces
$value = implode(' ', $value);
} else if(is_bool($value)) {
// boolean attribute uses only attribute name when true, or omit when false
if($value === true) $str .= "$attr ";
continue;
} else if(!strlen("$value") && strpos($attr, 'data-') !== 0) {
// skip over empty non-data attributes that are not arrays
// if(!$value = $this->attr($attr))) continue; // was in 3.0.132 and earlier
continue;
}
$str .= "$attr=\"" . htmlspecialchars($value, ENT_QUOTES, "UTF-8") . '" ';
}
return trim($str);
}
/**
* Render the HTML input element(s) markup, ready for insertion in an HTML form.
*
* This is an abstract method that descending Inputfield module classes are required to implement.
*
* #pw-group-output
*
* @return string
*
*/
abstract public function ___render();
/**
* Render just the value (not input) in text/markup for presentation purposes.
*
* #pw-group-output
*
* @return string Text or markup where applicable
*
*/
public function ___renderValue() {
// This is within the context of an InputfieldForm, where the rendered markup can have
// external CSS or JS dependencies (in Inputfield[Name].css or Inputfield[Name].js)
$value = $this->attr('value');
if(is_array($value)) {
if(!count($value)) return '';
$out = "<ul>";
foreach($value as $v) $out .= "<li>" . $this->wire()->sanitizer->entities($v) . "</li>";
$out .= "</ul>";
} else {
$out = $this->wire()->sanitizer->entities($value);
}
return $out;
}
/**
* Method called right before Inputfield markup is rendered, so that any dependencies can be loaded as well.
*
* Called before `Inputfield::render()` or `Inputfield::renderValue()` method by the parent `InputfieldWrapper`
* instance. If you are calling either of those methods yourself for some reason, make sure that you call this
* `renderReady()` method first.
*
* The default behavior of this method is to populate Inputfield-specific CSS and JS file assets into
* `$config->styles` and `$config->scripts`.
*
* The return value is true if assets were just added, and false if assets have already been added in a previous
* call. This distinction probably doesn't matter in most usages, but here just in case a descending class needs
* to know when/if to add additional assets (i.e. when this method returns true).
*
* #pw-group-output
*
* @param Inputfield|InputfieldWrapper|null The parent InputfieldWrapper that is rendering it, or null if no parent.
* @param bool $renderValueMode Specify true only if this is for `Inputfield::renderValue()` rather than `Inputfield::render()`.
* @return bool True if assets were just added, false if already added.
*
*/
public function renderReady(Inputfield $parent = null, $renderValueMode = false) {
$result = $this->wire()->modules->loadModuleFileAssets($this) > 0;
if($this->wire()->hooks->isMethodHooked($this, 'renderReadyHook')) {
$this->renderReadyHook($parent, $renderValueMode);
}
return $result;
}
/**
* Hookable version of renderReady(), not called unless 'renderReadyHook' is hooked
*
* Hook this method instead if you want to hook renderReady().
*
* @param Inputfield $parent
* @param bool $renderValueMode
*
*/
public function ___renderReadyHook(Inputfield $parent = null, $renderValueMode = false) { }
/**
* This hook was replaced by renderReady
*
* #pw-internal
*
* @param $event
* @deprecated
*
*/
public function hookRender($event) { }
/**
* Process input for this Inputfield directly from the POST (or GET) variables
*
* This method should pull the value from the given `$input` argument, sanitize/validate it, and
* populate it to the `value` attribute of this Inputfield.
*
* Inputfield modules should implement this method if the built-in one here doesn't solve their need.
* If this one does solve their need, then they should add any additional sanitization or validation
* to the `Inputfield::setAttribute('value', $value)` method to occur when given the `value` attribute.
*
* #pw-group-input
*
* @param WireInputData $input User input where value should be pulled from (typically `$input->post`)
* @return $this
*
*/
public function ___processInput(WireInputData $input) {
if(isset($input[$this->name])) {
$value = $input[$this->name];
} else if($this instanceof InputfieldHasArrayValue) {
$value = array();
} else {
$value = $input[$this->name];
}
$changed = false;
if($this instanceof InputfieldHasArrayValue && !is_array($value)) {
/** @var Inputfield $this */
$this->error("Expected an array value and did not receive it");
return $this;
}
$previousValue = $this->attr('value');
if(is_array($value)) {
// an array value was provided in the input
// note: only arrays one level deep are allowed
if(!$this instanceof InputfieldHasArrayValue) {
$this->error("Received an unexpected array value");
return $this;
}
$values = array();
foreach($value as $v) {
if(is_array($v)) continue; // skip over multldimensional arrays, not allowed
if(ctype_digit("$v") && (((int) $v) <= PHP_INT_MAX)) $v = (int) "$v"; // force digit strings as integers
$values[] = $v;
}
if($previousValue !== $values) {
// If it has changed, then update for the changed value
$changed = true;
/** @var Inputfield $this */
$this->setAttribute('value', $values);
}
} else {
// string value provided in the input
$this->setAttribute('value', $value);
$value = $this->attr('value');
if("$value" !== (string) $previousValue) {
$changed = true;
}
}
if($changed) {
$this->trackChange('value', $previousValue, $value);
// notify the parent of the change
$parent = $this->getParent();
if($parent) $parent->trackChange($this->name);
}
return $this;
}
/**
* Is this Inputfield empty? (Value attribute reflects an empty state)
*
* Return true if this field is empty (no value or blank value), or false if its not empty.
*
* Used by the 'required' check to see if the field is populated, and descending Inputfields may
* override this according to their own definition of 'empty'.
*
* #pw-group-attribute-methods
*
* @return bool
*
*/
public function isEmpty() {
$value = $this->attr('value');
if(is_array($value)) return count($value) == 0;
if(!strlen("$value")) return true;
// if($value === 0) return true;
return false;
}
/**
* Get any custom configuration fields for this Inputfield
*
* - Intended to be extended by each Inputfield as needed to add more config options.
*
* - The returned InputfieldWrapper ultimately ends up as the "Input" tab in the fields editor (admin).
*
* - Descending Inputfield classes should first call this method from the parent class to get the
* default configuration fields and the InputfieldWrapper they can add to.
*
* - Returned Inputfield instances with a name attribute that starts with an underscore, i.e. "_myname"
* are assumed to be for runtime use and are NOT stored in the database.
*
* - If you prefer, you may instead implement the `Inputfield::getConfigArray()` method as an alternative.
*
* ~~~~
* // Example getConfigInputfields() implementation
* public function ___getConfigInputfields() {
* // Get the defaults and $inputfields wrapper we can add to
* $inputfields = parent::___getConfigInputfields();
* // Add a new Inputfield to it
* $f = $this->wire('modules')->get('InputfieldText');
* $f->attr('name', 'first_name');
* $f->attr('value', $this->get('first_name'));
* $f->label = 'Your First Name';
* $inputfields->add($f);
* return $inputfields;
* }
* ~~~~
*
* #pw-group-module
*
* @return InputfieldWrapper Populated with Inputfield instances
* @see Inputfield::getConfigArray()
*
*/
public function ___getConfigInputfields() {
$conditionsText = $this->_('Conditions are expressed with a "field=value" selector containing fields and values to match. Multiple conditions should be separated by a comma.');
$conditionsNote = $this->_('Read more about [how to use this](https://processwire.com/api/selectors/inputfield-dependencies/).');
/** @var InputfieldWrapper $inputfields */
$inputfields = $this->wire(new InputfieldWrapper());
$fieldset = $inputfields->InputfieldFieldset;
$fieldset->label = $this->_('Visibility');
$fieldset->attr('name', 'visibility');
$fieldset->icon = 'eye';
if($this->collapsed == Inputfield::collapsedNo && !$this->getSetting('showIf')) {
$fieldset->collapsed = Inputfield::collapsedYes;
}
$inputfields->append($fieldset);
$f = $inputfields->InputfieldSelect;
$f->attr('name', 'collapsed');
$f->label = $this->_('Presentation');
$f->icon = 'eye-slash';
$f->description = $this->_("How should this field be displayed in the editor?");
$f->addOption(self::collapsedNo, $this->_('Open'));
$f->addOption(self::collapsedNever, $this->_('Open + Cannot be closed'));
$f->addOption(self::collapsedNoLocked, $this->_('Open + Locked (not editable)'));
$f->addOption(self::collapsedBlank, $this->_('Open when populated + Closed when blank'));
if($this->hasFieldtype !== false) {
$f->addOption(self::collapsedBlankAjax, $this->_('Open when populated + Closed when blank + Load only when opened (AJAX)') . "");
}
$f->addOption(self::collapsedBlankLocked, $this->_('Open when populated + Closed when blank + Locked (not editable)'));
$f->addOption(self::collapsedPopulated, $this->_('Open when blank + Closed when populated'));
$f->addOption(self::collapsedYes, $this->_('Closed'));
$f->addOption(self::collapsedYesLocked, $this->_('Closed + Locked (not editable)'));
if($this->hasFieldtype !== false) {
$f->addOption(self::collapsedYesAjax, $this->_('Closed + Load only when opened (AJAX)') . "");
$f->notes = sprintf($this->_('Options indicated with %s may not work with all input types or placements, test to ensure compatibility.'), '†');
$f->addOption(self::collapsedTab, $this->_('Tab'));
$f->addOption(self::collapsedTabAjax, $this->_('Tab + Load only when clicked (AJAX)') . "");
$f->addOption(self::collapsedTabLocked, $this->_('Tab + Locked (not editable)'));
}
$f->addOption(self::collapsedHidden, $this->_('Hidden (not shown in the editor)'));
$f->attr('value', (int) $this->collapsed);
$fieldset->append($f);
$f = $inputfields->InputfieldText;
$f->label = $this->_('Show this field only if');
$f->description = $this->_('Enter the conditions under which the field will be shown.') . ' ' . $conditionsText;
$f->notes = $conditionsNote;
$f->icon = 'question-circle';
$f->attr('name', 'showIf');
$f->attr('value', $this->getSetting('showIf'));
$f->collapsed = Inputfield::collapsedBlank;
$f->showIf = "collapsed!=" . self::collapsedHidden;
$fieldset->append($f);
$value = (int) $this->getSetting('columnWidth');
if($value < 10 || $value >= 100) $value = 100;
$f = $inputfields->InputfieldInteger;
$f->label = sprintf($this->_('Column width (%d%%)'), $value);
$f->icon = 'arrows-h';
$f->attr('id+name', 'columnWidth');
$f->addClass('columnWidthInput');
$f->attr('type', 'text');
$f->attr('maxlength', 4);
$f->attr('size', 4);
$f->attr('max', 100);
$f->attr('value', $value . '%');
$f->description = $this->_("The percentage width of this field's container (10%-100%). If placed next to other fields with reduced widths, it will create floated columns."); // Description of colWidth option
$f->notes = $this->_("Note that not all fields will work at reduced widths, so you should test the result after changing this."); // Notes for colWidth option
if(!$this->wire()->input->get('process_template') && $value == 100) $f->collapsed = Inputfield::collapsedYes;
$inputfields->append($f);
if(!$this instanceof InputfieldWrapper) {
$f = $inputfields->InputfieldCheckbox;
$f->label = $this->_('Required?');
$f->icon = 'asterisk';
$f->attr('name', 'required');
$f->attr('value', 1);
$f->attr('checked', $this->getSetting('required') ? 'checked' : '');
$f->description = $this->_("If checked, a value will be required for this field.");
$f->collapsed = $this->getSetting('required') ? Inputfield::collapsedNo : Inputfield::collapsedYes;
$inputfields->add($f);
$requiredAttr = $this->getSetting('requiredAttr');
if($requiredAttr !== null) {
// Inputfield must have set requiredAttr to some non-null value before this will appear as option in config
$f->columnWidth = 50; // required checkbox
$f = $inputfields->InputfieldCheckbox;
$f->attr('name', 'requiredAttr');
$f->label = $this->_('Also use HTML5 “required” attribute?');
$f->showIf = "required=1, showIf='', requiredIf=''";
$f->description = $this->_('Use only on fields *always* visible to the user.');
$f->icon = 'html5';
$f->columnWidth = 50;
if($requiredAttr) $f->attr('checked', 'checked');
$inputfields->add($f);
}
$f = $inputfields->InputfieldText;
$f->label = $this->_('Required only if');
$f->icon = 'asterisk';
$f->description = $this->_('Enter the conditions under which a value will be required for this field.') . ' ' . $conditionsText;
$f->notes = $conditionsNote;
$f->attr('name', 'requiredIf');
$f->attr('value', $this->getSetting('requiredIf'));
$f->collapsed = $f->attr('value') ? Inputfield::collapsedNo : Inputfield::collapsedYes;
$f->showIf = "required>0";
$inputfields->add($f);
}
if($this->hasFieldtype === false || $this->wire()->config->advanced) {
$f = $inputfields->InputfieldTextarea;
$f->attr('name', 'addClass');
$f->label = $this->_('Custom class attributes');
$f->description =
$this->_('Optionally add to the class attribute for specific elements in this Inputfield.') . ' ' .
$this->_('Format is one per line of `element:class` where `element` is one of: “wrap”, “header”, “content” or “input” and `class` is one or more class names.') . ' ' .
$this->_('If no element is specified then the “input” element is assumed.');
$f->notes = $this->_('Example:') . "`" .
"\nwrap:card card-default" .
"\nheader:card-header" .
"\ncontent:card-body" .
"\ninput:form-input input-checkbox" .
"`";
$f->collapsed = Inputfield::collapsedBlank;
$f->renderFlags = self::renderLast;
$f->val($this->getSetting('addClass'));
$inputfields->add($f);
}
return $inputfields;
}
/**
* Alternative method for configuration that allows for array definition
*
* - This method is typically used instead of the `Inputfield::getConfigInputfields` method
* for module authors that prefer to use the array definition.
*
* - If both `getConfigInputfields()` and `getConfigArray()` are implemented, both will be used.
*
* - See comments for `InputfieldWrapper::importArray()` for example of array definition.
*
* ~~~~~
* // Example implementation
* public function ___getConfigArray() {
* return [
* 'test' => [
* 'type' => 'text',
* 'label' => 'This is a test',
* 'value' => 'Test'
* ]
* ];
* );
* ~~~~~
*
* #pw-group-module
*
* @return array
*
*/
public function ___getConfigArray() {
return array();
}
/**
* Return a list of config property names allowed for fieldgroup/template context
*
* These should be the names of Inputfields returned by `Inputfield::getConfigInputfields()` or
* `Inputfield::getConfigArray()` that are allowed in fieldgroup/template context.
*
* The config property names specified here are allowed to be configured within the context
* of a given `Fieldgroup`, enabling the user to configure them independently per template
* in the admin.
*
* This is the equivalent to the `Fieldtype::getConfigAllowContext()` method, but for the "Input"
* tab rather than the "Details" tab.
*
* #pw-group-module
*
* @param Field $field
* @return array of Inputfield names
* @see Fieldtype::getConfigAllowContext()
*
*/
public function ___getConfigAllowContext($field) {
if($field) {}
return array(
'visibility',
'collapsed',
'columnWidth',
'required',
'requiredIf',
'requiredAttr',
'showIf'
);
}
/**
* Export configuration values for external consumption
*
* Use this method to externalize any config values when necessary.
* For example, internal IDs should be converted to GUIDs where possible.
*
* Most Inputfields do not need to implement this.
*
* #pw-internal
*
* @param array $data
* @return array
*
*/
public function ___exportConfigData(array $data) {
$inputfields = $this->getConfigInputfields();
if(!$inputfields || !count($inputfields)) return $data;
foreach($inputfields->getAll() as $inputfield) {
/** @var Inputfield $inputfield */
$value = $inputfield->isEmpty() ? '' : $inputfield->value;
if(is_object($value)) $value = (string) $value;
$data[$inputfield->name] = $value;
}
return $data;
}
/**
* Convert an array of exported data to a format that will be understood internally (opposite of exportConfigData)
*
* Most Inputfields do not need to implement this.
*
* #pw-internal
*
* @param array $data
* @return array Data as given and modified as needed. Also included is $data[errors], an associative array
* indexed by property name containing errors that occurred during import of config data.
*
*/
public function ___importConfigData(array $data) {
return $data;
}
/**
* Returns a unique key variable used to store errors in the session
*
* #pw-internal
*
*/
public function getErrorSessionKey() {
$name = $this->attr('name');
if(!$name) $name = $this->attr('id');
$key = "_errors_" . $this->className() . "_$name";
return $key;
}
/**
* Record an error for this Inputfield
*
* The error message will be placed in the context of this Inputfield.
* See the `Wire::error()` method for full details on arguments and options.
*
* #pw-group-states
*
* @param string $text Text of error message
* @param int $flags Optional flags
* @return $this
*
*/
public function error($text, $flags = 0) {
// Override Wire's error method and place errors in the context of their inputfield
$session = $this->wire()->session;
$key = $this->getErrorSessionKey();
$errors = $session->$key;
if(!is_array($errors)) $errors = array();
if(!in_array($text, $errors)) {
$errors[] = $text;
$session->set($key, $errors);
}
$label = $this->getSetting('label');
if(empty($label)) $label = $this->attr('name');
if(strlen($label)) $text .= " - $label";
return parent::error($text, $flags);
}
/**
* Return array of strings containing errors that occurred during input processing
*
* Note that this is different from `Wire::errors()` in that it retrieves errors from the session
* rather than just the current run.
*
* #pw-group-states
*
* @param bool $clear Optionally clear the errors after getting them (Default=false).
* @return array
*
*/
public function getErrors($clear = false) {
$session = $this->wire()->session;
$key = $this->getErrorSessionKey();
$errors = $session->get($key);
if(!is_array($errors)) $errors = array();
if($clear) {
$session->remove($key);
parent::errors("clear");
}
return $errors;
}
/**
* Clear errors from this Inputfield
*
* This is the same as `$inputfield->getErrors(true);` but has no return value.
*
* #pw-group-states
*
* @since 3.0.205
*
*/
public function clearErrors() {
$this->getErrors(true);
}
/**
* Does this Inputfield have the requested property or attribute?
*
* #pw-group-attribute-methods
* #pw-group-settings
*
* @param string $key Requested property or attribute.
* @return bool True if it has it, false if it doesn't
*
*/
public function has($key) {
$has = parent::has($key);
if(!$has) $has = isset($this->attributes[$key]);
return $has;
}
/**
* Does this Inputfield have the given setting?
*
* Return value does not indicate that it is non-empty, only that the setting is available.
*
* #pw-internal
*
* @param string $key Setting name
* @return bool
*
*/
public function hasSetting($key) {
return isset($this->data[$key]);
}
/**
* Does this Inputfield have the given attribute?
*
* Return value does not indicate that it is non-empty, only that the setting is available.
*
* #pw-internal
*
* @param string $key Attribute name
* @return bool
*
*/
public function hasAttribute($key) {
return isset($this->attributes[$key]);
}
/**
* Track the change, but only if it was to the 'value' attribute.
*
* We don't track changes to any other properties of Inputfields.
*
* #pw-internal
*
* @param string $what Name of property that changed
* @param mixed $old Previous value before change
* @param mixed $new New value
* @return Inputfield|WireData $this
*
*/
public function trackChange($what, $old = null, $new = null) {
if($what != 'value') return $this;
return parent::trackChange($what, $old, $new);
}
/**
* Entity encode a string with optional Markdown support.
*
* - Markdown support provided with second argument.
* - If string is already entity-encoded it will first be decoded.
*
* #pw-group-output
*
* @param string $str String to encode
* @param bool|int $markdown Optionally specify one of the following:
* - `true` (boolean): To allow Markdown using default "textFormat" setting (which is basic Markdown by default).
* - `false` (boolean): To disallow Markdown support (this is the default when $markdown argument omitted).
* - `Inputfield::textFormatNone` (constant): Disallow Markdown support (default).
* - `Inputfield::textFormatBasic` (constant): To support basic/inline Markdown.
* - `Inputfield::textFormatMarkdown` (constant): To support full Markdown and HTML.
* @return string Entity encoded string or HTML string
*
*/
public function entityEncode($str, $markdown = false) {
$sanitizer = $this->wire()->sanitizer;
$str = (string) $str;
// if already encoded, then un-encode it
if(strpos($str, '&') !== false && preg_match('/&(#\d+|[a-zA-Z]+);/', $str)) {
$str = $sanitizer->unentities($str);
}
if($markdown && $markdown !== self::textFormatNone) {
if(is_int($markdown)) {
$textFormat = $markdown;
} else {
$textFormat = $this->getSetting('textFormat');
}
if(!$textFormat) $textFormat = self::textFormatBasic;
if($textFormat & self::textFormatBasic) {
// only basic markdown allowed (default behavior)
$str = $sanitizer->entitiesMarkdown($str, array('allowBrackets' => true));
} else if($textFormat & self::textFormatMarkdown) {
// full markdown, plus HTML is also allowed
$str = $sanitizer->entitiesMarkdown($str, array('fullMarkdown' => true));
} else {
// nothing allowed, text fully entity encoded regardless of $markdown request
$str = $sanitizer->entities($str);
}
} else {
$str = $sanitizer->entities($str);
}
return $str;
}
/**
* Get or set editable state for this Inputfield
*
* When set to false, the `Inputfield::processInput()` method won't be called by parent InputfieldWrapper,
* effectively skipping over input processing entirely for this Inputfield.
*
* #pw-group-states
*
* @param bool|null $setEditable Specify true or false to set the editable state, or omit just to get the editable state.
* @return bool Returns the current editable state.
*
*/
public function editable($setEditable = null) {
if(!is_null($setEditable)) $this->editable = (bool) $setEditable;
return $this->editable;
}
/**
* debugInfo PHP 5.6+ magic method
*
* @return array
*
*/
public function __debugInfo() {
$info = array(
'className' => $this->className(),
'attributes' => $this->attributes,
'wrapAttributes' => $this->wrapAttributes,
);
if(is_object($this->parent)) {
$info['parent'] = array(
'className' => $this->parent->className(),
'name' => $this->parent->attr('name'),
'id' => $this->parent->attr('id'),
);
}
$info = array_merge($info, parent::__debugInfo());
return $info;
}
/**
* Set custom html render, see $this->html at top for reference.
*
* @param string $html
*
public function setHTML($html) {
$this->html = $html;
}
*/
/**
* Get default or custom HTML for render
*
* If $this->html is populated, it gets returned.
* If not, then this should return the default HTML for the Inputfield,
* where supported.
*
* If this returns blank, then it means custom HTML is not supported.
*
* @param array $attr When populated with key=value, tags will be replaced.
* @return array
*
public function getHTML($attr = array()) {
if(!strlen($this->html) || empty($attr) || strpos($this->html, '{') === false) return $this->html;
$html = $this->html;
if(strpos($html, '{attr}')) {
$html = str_replace('{attr}', $this->getAttributesString($attr), $html);
// @todo remove any other {tags} that might be present
} else {
// a version of html where the {tags} get replaced with blanks
// used for testing if more attributes present without possibility
// of those attributes being injected
// $_html = $html;
// extract value so that a substitution can't result in input-injected tags
if(isset($attr['value'])) {
$value = $attr['value'];
unset($attr['value']);
} else {
$value = null;
}
// populate attributes
foreach($attr as $name => $v) {
$tag = '{' . $name . '}';
if(strpos($html, $tag) === false) continue;
$v = htmlspecialchars($v, ENT_QUOTES, 'UTF-8');
$html = str_replace($tag, $v, $html);
//$_html = str_replace($tag, '', $html);
}
// see if any non-value attributes are left
$pos = strpos($html, '{');
if($pos !== false && $pos != strpos($html, '{value}')) {
// there are unpopulated tags that need to be removed
preg_match_all('/\{[-_a-zA-Z0-9]+\}/', $html, $matches);
}
// once all other attributes populated, we can populate {value}
if($value !== null) {
$value = htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
$html = str_replace('{value}', $value, $html);
$_html = str_replace('{value}', '', $html);
}
// if ther
}
return $html;
}
*/
}