silverstripe-framework/forms/FormField.php

629 lines
17 KiB
PHP
Raw Normal View History

<?php
/**
* Represents a field in a form.
* A FieldSet contains a number of FormField objects which make up the whole of a form.
* In addition to single fields, FormField objects can be "composite", for example, the {@link TabSet}
* field. Composite fields let us define complex forms without having to resort to custom HTML.
* @package forms
* @subpackage core
*/
class FormField extends RequestHandler {
protected $form;
protected $name, $title, $value ,$message, $messageType, $extraClass;
/**
* @var $description string Adds a "title"-attribute to the markup.
* @todo Implement in all subclasses
*/
protected $description;
/**
* @var $extraClasses array Extra CSS-classes for the formfield-container
*/
protected $extraClasses;
public $dontEscape;
/**
* @var $rightTitle string Used in SmallFieldHolder() to force a right-aligned label.
*/
protected $rightTitle;
/**
* @var $leftTitle string Used in SmallFieldHolder() to force a left-aligned label with correct spacing.
* Please use $title for FormFields rendered with FieldHolder().
*/
protected $leftTitle;
/**
* Set the "tabindex" HTML attribute on the field.
*
* @var int
*/
protected $tabIndex;
/**
* Stores a reference to the FieldSet that contains this object.
* @var FieldSet
*/
protected $containerFieldSet;
/**
* @var $readonly boolean
*/
protected $readonly = false;
/**
* @var $disabled boolean
*/
protected $disabled = false;
/**
* @var Custom Validation Message for the Field
*/
protected $customValidationMessage = "";
/**
* Create a new field.
* @param name The internal field name, passed to forms.
* @param title The field label.
* @param value The value of the field.
* @param form Reference to the container form
* @param maxLength The Maximum length of the attribute
*/
function __construct($name, $title = null, $value = null, $form = null, $rightTitle = null) {
$this->name = $name;
$this->title = ($title === null) ? $name : $title;
FEATURE New DatetimeField class (form field wrapper composed of DateField andTimeField) FEATURE New DateField and TimeField form classes with more consistent API and easier localization API CHANGE Date/time parsing in DateField, TimeField and DatetimeField defaults to i18n::get_locale() ('en_US') instead of using en_NZ/en_GB specific parsing. Use i18n::set_locale('en_NZ') in mysite/_config.php to revert to old behaviour. API CHANGE $timeformat constructor parameter in TimeField needs to be in ISO date notation (not PHP's date()) API CHANGE TimeField, DateField and related subclasses use Zend_Date for date parsing, meaning they're stricer than the previously used strtotime() API CHANGE Removed DMYCalendarDateField and CalendarDateField, use DateField with setConfig('showcalendar') API CHANGE Removed CompositeDateField, DMYDateField, use DateField with setConfig('dmyfields') API CHANGE Removed DropdownTimeField, use TimeField with setConfig('showdropdown') API CHANGE Removed PopupDateTimeField, use DatetimeField API CHANGE Changed 'date', 'month' and 'year' HTML field names to lowercase in DMYDateField API CHANGE Removed support for ambiguous date formats in DateField, e.g. '06/03/03'. Use DateField->setConfig('dateformat', '<format>') to revert to this behaviour. API CHANGE Removed $futureOnly flag from DateField, CalendarDateField etc., use DateField->setConfig('min') and DateField->setConfig('max') ENHANCEMENT Using Zend_Date for DateField and TimeField, with more robust date handling, starting localization support. Set globally via i18n::set_locale(), or for a field instance through setLocale(). Note: Javascript validation is not localized yet. git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/branches/2.4@99360 467b73ca-7a2a-4603-9d3b-597d59a354a9
2010-02-17 00:10:09 +01:00
if($value) $this->setValue($value);
if($form) $this->setForm($form);
parent::__construct();
}
/**
* Return a Link to this field
*/
function Link($action = null) {
return Controller::join_links($this->form->FormAction(), 'field/' . $this->name, $action);
}
/**
* Returns the HTML ID of the field - used in the template by label tags.
* The ID is generated as FormName_FieldName. All Field functions should ensure
* that this ID is included in the field.
*/
function id() {
$name = ereg_replace('(^-)|(-$)','',ereg_replace('[^A-Za-z0-9_-]+','-',$this->name));
if($this->form) return $this->form->FormName() . '_' . $name;
else return $name;
}
/**
* Returns the field name - used by templates.
*
* @return string
*/
function Name() {
return $this->name;
}
function attrName() {
return $this->name;
}
/**
* Returns the field message, used by form validation.
* Use {@link setError()} to set this property.
*
* @return string
*/
function Message() {
return $this->message;
}
/**
* Returns the field message type, used by form validation.
* Arbitrary value which is mostly used for CSS classes
* in the rendered HTML, e.g. "required".
* Use {@link setError()} to set this property.
*
* @return string
*/
function MessageType() {
return $this->messageType;
}
/**
* Returns the field value - used by templates.
*/
function Value() {
return $this->value;
}
/**
* Method to save this form field into the given data object.
* By default, makes use of $this->dataValue()
*/
function saveInto(DataObjectInterface $record) {
if($this->name) {
$record->setCastedField($this->name, $this->dataValue());
}
}
/**
* Returns the field value suitable for insertion into the data object
*/
function dataValue() {
return $this->value;
}
/**
* Returns the field label - used by templates.
*/
function Title() {
return $this->title;
}
function setTitle($val) {
$this->title = $val;
}
function RightTitle() {
return $this->rightTitle;
}
function setRightTitle($val) {
$this->rightTitle = $val;
}
function LeftTitle() {
return $this->leftTitle;
}
function setLeftTitle($val) {
$this->leftTitle = $val;
}
/**
* Set tabindex HTML attribute
* (defaults to none).
*
* @param int $index
*/
public function setTabIndex($index) {
$this->tabIndex = $index;
}
/**
* Get tabindex (if previously set)
*
* @return int
*/
public function getTabIndex() {
return $this->tabIndex;
}
/**
* Get tabindex HTML string
*
* @param int $increment Increase current tabindex by this value
* @return string
*/
protected function getTabIndexHTML($increment = 0) {
$tabIndex = (int)$this->getTabIndex() + (int)$increment;
return (is_numeric($tabIndex)) ? ' tabindex = "' . $tabIndex . '"' : '';
}
/**
* Compiles all CSS-classes. Optionally includes a "nolabel"-class
* if no title was set on the formfield.
* Uses {@link Message()} and {@link MessageType()} to add validatoin
* error classes which can be used to style the contained tags.
*
* @return String CSS-classnames
*/
function extraClass() {
$output = "";
if(is_array($this->extraClasses)) {
$output = " " . implode($this->extraClasses, " ");
}
// Allow customization of label and field tag positioning
if(!$this->Title()) $output .= " nolabel";
// Allow custom styling of any element in the container based
// on validation errors, e.g. red borders on input tags.
// CSS-Class needs to be different from the one rendered
// through {@link FieldHolder()}
if($this->Message()) $output .= " holder-" . $this->MessageType();
return $output;
}
/**
* Add a CSS-class to the formfield-container.
*
* @param $class String
*/
function addExtraClass($class) {
$this->extraClasses[$class] = $class;
}
/**
* Remove a CSS-class from the formfield-container.
*
* @param $class String
*/
function removeExtraClass($class) {
if(isset($this->extraClasses) && array_key_exists($class, $this->extraClasses)) unset($this->extraClasses[$class]);
}
/**
* Returns a version of a title suitable for insertion into an HTML attribute
*/
function attrTitle() {
return Convert::raw2att($this->title);
}
/**
* Returns a version of a title suitable for insertion into an HTML attribute
*/
function attrValue() {
return Convert::raw2att($this->value);
}
/**
* Set the field value.
* Returns $this.
*/
function setValue($value) {
$this->value = $value; return $this;
}
/**
* Set the field name
*/
function setName($name) {
$this->name = $name;
}
/**
* Set the container form.
* This is called whenever you create a new form and put fields inside it, so that you don't
* have to worry about linking the two.
*/
function setForm($form) {
$this->form = $form;
}
/**
* Get the currently used form.
*
* @return Form
*/
function getForm() {
return $this->form;
}
/**
* Return TRUE if security token protection is enabled on the parent {@link Form}.
*
* @return bool
*/
public function securityTokenEnabled() {
return $this->getForm() && $this->getForm()->securityTokenEnabled();
}
/**
* Sets the error message to be displayed on the form field
* Set by php validation of the form
*/
function setError($message,$messageType){
$this->message = $message;
$this->messageType = $messageType;
}
/**
* Set the custom error message to show instead of the default
* format of Please Fill In XXX. Different from setError() as
* that appends it to the standard error messaging
*
* @param String Message for the error
*/
public function setCustomValidationMessage($msg) {
$this->customValidationMessage = $msg;
}
/**
* Get the custom error message for this form field. If a custom
* message has not been defined then just return blank. The default
* error is defined on {@link Validator}.
*
* @todo Should the default error message be stored here instead
* @return String
*/
public function getCustomValidationMessage() {
return $this->customValidationMessage;
}
/**
* Returns the form field - used by templates.
* Although FieldHolder is generally what is inserted into templates, all of the field holder
* templates make use of $Field. It's expected that FieldHolder will give you the "complete"
* representation of the field on the form, whereas Field will give you the core editing widget,
* such as an input tag.
*
* Our base FormField class just returns a span containing the value. This should be overridden!
*/
function Field() {
if($this->value) $value = $this->dontEscape ? ($this->reserveNL ? Convert::raw2xml($this->value) : $this->value) : Convert::raw2xml($this->value);
else $value = '<i>(' . _t('FormField.NONE', 'none') . ')</i>';
$attributes = array(
'id' => $this->id(),
'class' => 'readonly' . ($this->extraClass() ? $this->extraClass() : '')
);
$hiddenAttributes = array(
'type' => 'hidden',
'name' => $this->name,
'value' => $this->value,
'tabindex' => $this->getTabIndex()
);
$containerSpan = $this->createTag('span', $attributes, $value);
$hiddenInput = $this->createTag('input', $hiddenAttributes);
return $containerSpan . "\n" . $hiddenInput;
}
/**
* Returns a "Field Holder" for this field - used by templates.
* Forms are constructed from by concatenating a number of these field holders. The default
* field holder is a label and form field inside a paragraph tag.
*
* Composite fields can override FieldHolder to create whatever visual effects you like. It's
* a good idea to put the actual HTML for field holders into templates. The default field holder
* is the DefaultFieldHolder template. This lets you override the HTML for specific sites, if it's
* necessary.
switch externals to trunk. Inform-merge: from the changeset: r32477: Merge 2.0-inform from trunk previously r32478: Merge 2.0-inform from trunk previously r32481: merge 2.0infom with lastes chunk r32483: merge 2.0infom with lastes chunk r33526: Final styling of all forms and combined communication form add/alter javascript for height adjustment of First / Second block r33580: styling for combined form communication in myinfom pages r33706: styling of combined form (communication) in Email r33881: made compatible to $extraClass r33885: added defaultVal r33887: fixed typo r34728: modified SmallFieldHolder?() r34729: added "validationError"-class r34914: WIP3866: Factfinder: Hide "self emplyed" block r34964: Change current plan upto TraumaInsurance? r35038: disabled friggin field focus r35230: #1032 Fixed hash-link insertion r35887: conditionally setting parameters in sourceID() - to avoid empty overrides r35892: Saving value in SQL-compatible format (YYYY-MM-DD instead of DD/MM/YYYY), with fallback for non-sql values (just passed through without conversion) r35928: Removed "create a" from PageType?-dropdown, sorting alphabetically, falling back to $singular_name r35990: branched off for membertablefield r35994: fix for membertablefield r36024: added array-condition needed for DMYDateField r36083: fix bug for compositeField -> dropDatalessField r36394: removed debug code r36826: change wrong indent format r36828: WIP 4262: Logging out of My Inform goes to blank page r36858: Fixed error caused in r12472 while merging to Session-class r37132: Merged partial changesets from branches/2.0-nzct, only adding childID to detailform when not in add-mode r40815: add an unsubscribe record when a member subscribe a newslettertype r41113: fix the bug described in http://support.silverstripe.com/info/ticket/31: CRM not showing search results r43226: fixed search (partial merge from trunk) r43268: merged createNewPassword() from trunk, was referencing a non-existinent global function randomString() git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@45473 467b73ca-7a2a-4603-9d3b-597d59a354a9
2007-11-23 02:10:19 +01:00
*
* @todo Add "validationError" if needed.
*/
function FieldHolder() {
$Title = $this->XML_val('Title');
$Message = $this->XML_val('Message');
$MessageType = $this->XML_val('MessageType');
$RightTitle = $this->XML_val('RightTitle');
$Type = $this->XML_val('Type');
$extraClass = $this->XML_val('extraClass');
$Name = $this->XML_val('Name');
$Field = $this->XML_val('Field');
// Only of the the following titles should apply
$titleBlock = (!empty($Title)) ? "<label class=\"left\" for=\"{$this->id()}\">$Title</label>" : "";
$rightTitleBlock = (!empty($RightTitle)) ? "<label class=\"right\" for=\"{$this->id()}\">$RightTitle</label>" : "";
// $MessageType is also used in {@link extraClass()} with a "holder-" prefix
$messageBlock = (!empty($Message)) ? "<span class=\"message $MessageType\">$Message</span>" : "";
return <<<HTML
<div id="$Name" class="field $Type $extraClass">$titleBlock<div class="middleColumn">$Field</div>$rightTitleBlock$messageBlock</div>
HTML;
}
/**
* Returns a restricted field holder used within things like FieldGroups.
*/
function SmallFieldHolder() {
$result = '';
// set label
if($title = $this->RightTitle()){
$result .= "<label class=\"right\" for=\"" . $this->id() . "\">{$title}</label>\n";
} elseif($title = $this->LeftTitle()) {
$result .= "<label class=\"left\" for=\"" . $this->id() . "\">{$title}</label>\n";
} elseif($title = $this->Title()) {
$result .= "<label for=\"" . $this->id() . "\">{$title}</label>\n";
}
$result .= $this->Field();
return $result;
}
/**
* Returns true if this field is a composite field.
* To create composite field types, you should subclass {@link CompositeField}.
*/
function isComposite() { return false; }
/**
* Returns true if this field has its own data.
* Some fields, such as titles and composite fields, don't actually have any data. It doesn't
* make sense for data-focused methods to look at them. By overloading hasData() to return false,
* you can prevent any data-focused methods from looking at it.
*
* @see FieldSet::collateDataFields()
*/
function hasData() { return true; }
/**
* @return boolean
*/
function isReadonly() {
return $this->readonly;
}
/**
* Sets readonly-flag on form-field. Please use performReadonlyTransformation()
* to actually transform this instance.
* @param $bool boolean Setting "false" has no effect on the field-state.
*/
function setReadonly($bool) {
$this->readonly = $bool;
}
/**
* @return boolean
*/
function isDisabled() {
return $this->disabled;
}
/**
* Sets disabed-flag on form-field. Please use performDisabledTransformation()
* to actually transform this instance.
* @param $bool boolean Setting "false" has no effect on the field-state.
*/
function setDisabled($bool) {
$this->disabled = $bool;
}
/**
* Returns a readonly version of this field
*/
function performReadonlyTransformation() {
$field = new ReadonlyField($this->name, $this->title, $this->value);
$field->addExtraClass($this->extraClass());
$field->setForm($this->form);
return $field;
}
/**
* Return a disabled version of this field
*/
function performDisabledTransformation() {
$clone = clone $this;
$disabledClassName = $clone->class . '_Disabled';
if( ClassInfo::exists( $disabledClassName ) )
return new $disabledClassName( $this->name, $this->title, $this->value );
elseif($clone->hasMethod('setDisabled')){
$clone->setDisabled(true);
return $clone;
}else{
return $this->performReadonlyTransformation();
}
}
function transform(FormTransformation $trans) {
return $trans->transform($this);
}
switch externals to trunk. Inform-merge: from the changeset: r32477: Merge 2.0-inform from trunk previously r32478: Merge 2.0-inform from trunk previously r32481: merge 2.0infom with lastes chunk r32483: merge 2.0infom with lastes chunk r33526: Final styling of all forms and combined communication form add/alter javascript for height adjustment of First / Second block r33580: styling for combined form communication in myinfom pages r33706: styling of combined form (communication) in Email r33881: made compatible to $extraClass r33885: added defaultVal r33887: fixed typo r34728: modified SmallFieldHolder?() r34729: added "validationError"-class r34914: WIP3866: Factfinder: Hide "self emplyed" block r34964: Change current plan upto TraumaInsurance? r35038: disabled friggin field focus r35230: #1032 Fixed hash-link insertion r35887: conditionally setting parameters in sourceID() - to avoid empty overrides r35892: Saving value in SQL-compatible format (YYYY-MM-DD instead of DD/MM/YYYY), with fallback for non-sql values (just passed through without conversion) r35928: Removed "create a" from PageType?-dropdown, sorting alphabetically, falling back to $singular_name r35990: branched off for membertablefield r35994: fix for membertablefield r36024: added array-condition needed for DMYDateField r36083: fix bug for compositeField -> dropDatalessField r36394: removed debug code r36826: change wrong indent format r36828: WIP 4262: Logging out of My Inform goes to blank page r36858: Fixed error caused in r12472 while merging to Session-class r37132: Merged partial changesets from branches/2.0-nzct, only adding childID to detailform when not in add-mode r40815: add an unsubscribe record when a member subscribe a newslettertype r41113: fix the bug described in http://support.silverstripe.com/info/ticket/31: CRM not showing search results r43226: fixed search (partial merge from trunk) r43268: merged createNewPassword() from trunk, was referencing a non-existinent global function randomString() git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@45473 467b73ca-7a2a-4603-9d3b-597d59a354a9
2007-11-23 02:10:19 +01:00
function hasClass($class){
$patten = '/'.strtolower($class).'/i';
$subject = strtolower($this->class." ".$this->extraClass());
switch externals to trunk. Inform-merge: from the changeset: r32477: Merge 2.0-inform from trunk previously r32478: Merge 2.0-inform from trunk previously r32481: merge 2.0infom with lastes chunk r32483: merge 2.0infom with lastes chunk r33526: Final styling of all forms and combined communication form add/alter javascript for height adjustment of First / Second block r33580: styling for combined form communication in myinfom pages r33706: styling of combined form (communication) in Email r33881: made compatible to $extraClass r33885: added defaultVal r33887: fixed typo r34728: modified SmallFieldHolder?() r34729: added "validationError"-class r34914: WIP3866: Factfinder: Hide "self emplyed" block r34964: Change current plan upto TraumaInsurance? r35038: disabled friggin field focus r35230: #1032 Fixed hash-link insertion r35887: conditionally setting parameters in sourceID() - to avoid empty overrides r35892: Saving value in SQL-compatible format (YYYY-MM-DD instead of DD/MM/YYYY), with fallback for non-sql values (just passed through without conversion) r35928: Removed "create a" from PageType?-dropdown, sorting alphabetically, falling back to $singular_name r35990: branched off for membertablefield r35994: fix for membertablefield r36024: added array-condition needed for DMYDateField r36083: fix bug for compositeField -> dropDatalessField r36394: removed debug code r36826: change wrong indent format r36828: WIP 4262: Logging out of My Inform goes to blank page r36858: Fixed error caused in r12472 while merging to Session-class r37132: Merged partial changesets from branches/2.0-nzct, only adding childID to detailform when not in add-mode r40815: add an unsubscribe record when a member subscribe a newslettertype r41113: fix the bug described in http://support.silverstripe.com/info/ticket/31: CRM not showing search results r43226: fixed search (partial merge from trunk) r43268: merged createNewPassword() from trunk, was referencing a non-existinent global function randomString() git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@45473 467b73ca-7a2a-4603-9d3b-597d59a354a9
2007-11-23 02:10:19 +01:00
return preg_match($patten, $subject);
}
/**
* Returns the field type - used by templates.
* The field type is the class name with the word Field dropped off the end, all lowercase.
* It's handy for assigning HTML classes.
*/
function Type() {return strtolower(ereg_replace('Field$','',$this->class)); }
/**
* Construct and return HTML tag.
*
* @todo Transform to static helper method.
*/
function createTag($tag, $attributes, $content = null) {
$preparedAttributes = '';
foreach($attributes as $k => $v) {
// Note: as indicated by the $k == value item here; the decisions over what to include in the attributes can sometimes get finicky
if(!empty($v) || $v === '0' || $k == 'value') $preparedAttributes .= " $k=\"" . Convert::raw2att($v) . "\"";
}
if($content || $tag != 'input') return "<$tag$preparedAttributes>$content</$tag>";
else return "<$tag$preparedAttributes />";
}
/**
* javascript handler Functions for each field type by default
* formfield doesnt have a validation function
*
* @todo shouldn't this be an abstract method?
*/
function jsValidation() {
}
/**
* Validation Functions for each field type by default
* formfield doesnt have a validation function
*
* @todo shouldn't this be an abstract method?
*/
function validate() {
return true;
}
/**
* Describe this field, provide help text for it.
* The function returns this so it can be used like this:
* $action = FormAction::create('submit', 'Submit')->describe("Send your changes to be approved")
*/
function describe($description) {
$this->description = $description;
return $this;
}
function debug() {
return "$this->class ($this->name: $this->title : <font style='color:red;'>$this->message</font>) = $this->value";
}
/**
* This function is used by the template processor. If you refer to a field as a $ variable, it
* will return the $Field value.
*/
function forTemplate() {
return $this->Field();
}
/**
* @uses Validator->fieldIsRequired()
* @return boolean
*/
function Required() {
if($this->form && ($validator = $this->form->Validator)) {
return $validator->fieldIsRequired($this->name);
}
}
/**
* Takes a fieldname and converts camelcase to spaced
* words. Also resolves combined fieldnames with dot syntax
* to spaced words.
*
* @example 'TotalAmount' will return 'Total Amount'
* @example 'Organisation.ZipCode' will return 'Organisation Zip Code'
*
* @param string $fieldName
* @return string
*/
public function name_to_label($fieldName) {
if(strpos($fieldName, '.') !== false) {
$parts = explode('.', $fieldName);
$label = $parts[count($parts)-2] . ' ' . $parts[count($parts)-1];
} else {
$label = $fieldName;
}
$label = preg_replace("/([a-z]+)([A-Z])/","$1 $2", $label);
return $label;
}
/**
* Set the fieldset that contains this field.
*
* @param FieldSet $containerFieldSet
*/
function setContainerFieldSet($containerFieldSet) {
$this->containerFieldSet = $containerFieldSet;
}
function rootFieldSet() {
if(is_object($this->containerFieldSet)) return $this->containerFieldSet->rootFieldSet();
else user_error("rootFieldSet() called on $this->class object without a containerFieldSet", E_USER_ERROR);
}
}
?>