Merge pull request #4568 from tractorcow/pulls/4.0/dropdown-cleanup

API Cleanup and refactor of select fields and Standardise Relation interface
This commit is contained in:
Daniel Hensby 2016-01-21 12:39:27 +00:00
commit 1bf0605f5e
29 changed files with 1142 additions and 1246 deletions

View File

@ -49,7 +49,8 @@
SelectParser.prototype.add_option = function(option, group_position, group_disabled) { SelectParser.prototype.add_option = function(option, group_position, group_disabled) {
if (option.nodeName.toUpperCase() === "OPTION") { if (option.nodeName.toUpperCase() === "OPTION") {
if (option.text !== "") { // workaround for https://github.com/harvesthq/chosen/issues/2125
if (!option.text.match(/^\s*$/g)) {
if (group_position != null) { if (group_position != null) {
this.parsed[group_position].children += 1; this.parsed[group_position].children += 1;
} }
@ -135,7 +136,12 @@ Copyright (c) 2011 by Harvest
this.results_showing = false; this.results_showing = false;
this.result_highlighted = null; this.result_highlighted = null;
this.result_single_selected = null; this.result_single_selected = null;
this.allow_single_deselect = (this.options.allow_single_deselect != null) && (this.form_field.options[0] != null) && this.form_field.options[0].text === "" ? this.options.allow_single_deselect : false; this.allow_single_deselect = (this.options.allow_single_deselect != null)
&& (this.form_field.options[0] != null)
// workaround for https://github.com/harvesthq/chosen/issues/2125
&& this.form_field.options[0].text.match(/^\s*$/g)
? this.options.allow_single_deselect
: false;
this.disable_search_threshold = this.options.disable_search_threshold || 0; this.disable_search_threshold = this.options.disable_search_threshold || 0;
this.disable_search = this.options.disable_search || false; this.disable_search = this.options.disable_search || false;
this.search_contains = this.options.search_contains || false; this.search_contains = this.options.search_contains || false;

View File

@ -498,3 +498,18 @@ configuration customisation is done via overriding these templates.
If upgrading from an existing installation, make sure to invoke ?flush=all at least once. If upgrading from an existing installation, make sure to invoke ?flush=all at least once.
See [/developer_guides/files/file_security] for more information. See [/developer_guides/files/file_security] for more information.
### `ListboxField` is now multiple-only
Previously, this field would operate as either a single select (default) or multi-select by setting
`setMultiple` to either true or false.
Now this field should only be used for multi-selection. Single-selection should be done using
a regular `DropdownField`.
### `GroupedDropdownField::setDisabled` now only accepts a list of values.
Where previously you could specify a list of grouped values in the same way as `setSource`, this
method now only accepts either a non-associative array of values (not titles) or an `SS_List`
of items to disable.

View File

@ -36,12 +36,7 @@
* @package forms * @package forms
* @subpackage fields-basic * @subpackage fields-basic
*/ */
class CheckboxSetField extends OptionsetField { class CheckboxSetField extends MultiSelectField {
/**
* @var array
*/
protected $defaultItems = array();
/** /**
* @todo Explain different source data that can be used with this field, * @todo Explain different source data that can be used with this field,
@ -60,306 +55,43 @@ class CheckboxSetField extends OptionsetField {
} }
/** /**
* Gets the list of options to render in this formfield
*
* @return ArrayList * @return ArrayList
*/ */
public function getOptions() { public function getOptions() {
$selectedValues = $this->getValueArray();
$defaultItems = $this->getDefaultItems();
// Generate list of options to display
$odd = 0; $odd = 0;
$formID = $this->ID();
$source = $this->source; $options = new ArrayList();
$values = $this->value; foreach($this->getSource() as $itemValue => $title) {
$items = array(); $itemID = Convert::raw2htmlid("{$formID}_{$itemValue}");
// Get values from the join, if available
if(is_object($this->form)) {
$record = $this->form->getRecord();
if(!$values && $record && $record->hasMethod($this->name)) {
$funcName = $this->name;
$join = $record->$funcName();
if($join) {
foreach($join as $joinItem) {
$values[] = $joinItem->ID;
}
}
}
}
// Source is not an array
if(!is_array($source) && !is_a($source, 'SQLMap')) {
if(is_array($values)) {
$items = $values;
} else {
// Source and values are DataObject sets.
if($values && is_a($values, 'SS_List')) {
foreach($values as $object) {
if(is_a($object, 'DataObject')) {
$items[] = $object->ID;
}
}
} elseif($values && is_string($values)) {
if(!empty($values)) {
$items = explode(',', $values);
$items = str_replace('{comma}', ',', $items);
} else {
$items = array();
}
}
}
} else {
// Sometimes we pass a singluar default value thats ! an array && !SS_List
if($values instanceof SS_List || is_array($values)) {
$items = $values;
} else {
if($values === null) {
$items = array();
}
else {
if(!empty($values)) {
$items = explode(',', $values);
$items = str_replace('{comma}', ',', $items);
} else {
$items = array();
}
}
}
}
if(is_array($source)) {
unset($source['']);
}
$options = array();
if ($source == null) {
$source = array();
}
foreach($source as $value => $item) {
if($item instanceof DataObject) {
$value = $item->ID;
$title = $item->Title;
} else {
$title = $item;
}
$itemID = $this->ID() . '_' . preg_replace('/[^a-zA-Z0-9]/', '', $value);
$odd = ($odd + 1) % 2; $odd = ($odd + 1) % 2;
$extraClass = $odd ? 'odd' : 'even'; $extraClass = $odd ? 'odd' : 'even';
$extraClass .= ' val' . preg_replace('/[^a-zA-Z0-9\-\_]/', '_', $value); $extraClass .= ' val' . preg_replace('/[^a-zA-Z0-9\-\_]/', '_', $itemValue);
$options[] = new ArrayData(array( $itemChecked = in_array($itemValue, $selectedValues) || in_array($itemValue, $defaultItems);
$itemDisabled = $this->isDisabled() || in_array($itemValue, $defaultItems);
$options->push(new ArrayData(array(
'ID' => $itemID, 'ID' => $itemID,
'Class' => $extraClass, 'Class' => $extraClass,
'Name' => "{$this->name}[{$value}]", 'Name' => "{$this->name}[{$itemValue}]",
'Value' => $value, 'Value' => $itemValue,
'Title' => $title, 'Title' => $title,
'isChecked' => in_array($value, $items) || in_array($value, $this->defaultItems), 'isChecked' => $itemChecked,
'isDisabled' => $this->disabled || in_array($value, $this->disabledItems) 'isDisabled' => $itemDisabled,
)); )));
} }
$options = new ArrayList($options);
$this->extend('updateGetOptions', $options); $this->extend('updateGetOptions', $options);
return $options; return $options;
} }
/**
* Default selections, regardless of the {@link setValue()} settings.
* Note: Items marked as disabled through {@link setDisabledItems()} can still be
* selected by default through this method.
*
* @param Array $items Collection of array keys, as defined in the $source array
*/
public function setDefaultItems($items) {
$this->defaultItems = $items;
return $this;
}
/**
* @return Array
*/
public function getDefaultItems() {
return $this->defaultItems;
}
/**
* Load a value into this CheckboxSetField
*/
public function setValue($value, $obj = null) {
// If we're not passed a value directly, we can look for it in a relation method on the object passed as a
// second arg
if(!$value && $obj && $obj instanceof DataObject && $obj->hasMethod($this->name)) {
$funcName = $this->name;
$value = $obj->$funcName()->getIDList();
}
parent::setValue($value, $obj);
return $this;
}
/**
* Save the current value of this CheckboxSetField into a DataObject.
* If the field it is saving to is a has_many or many_many relationship,
* it is saved by setByIDList(), otherwise it creates a comma separated
* list for a standard DB text/varchar field.
*
* @param DataObject $record The record to save into
*/
public function saveInto(DataObjectInterface $record) {
$fieldname = $this->name;
$relation = ($fieldname && $record && $record->hasMethod($fieldname)) ? $record->$fieldname() : null;
if($fieldname && $record && $relation &&
($relation instanceof RelationList || $relation instanceof UnsavedRelationList)) {
$idList = array();
if($this->value) foreach($this->value as $id => $bool) {
if($bool) {
$idList[] = $id;
}
}
$relation->setByIDList($idList);
} elseif($fieldname && $record) {
if($this->value) {
$this->value = str_replace(',', '{comma}', $this->value);
$record->$fieldname = implode(',', (array) $this->value);
} else {
$record->$fieldname = '';
}
}
}
/**
* Return the CheckboxSetField value as a string
* selected item keys.
*
* @return string
*/
public function dataValue() {
if($this->value && is_array($this->value)) {
$filtered = array();
foreach($this->value as $item) {
if($item) {
$filtered[] = str_replace(",", "{comma}", $item);
}
}
return implode(',', $filtered);
}
return '';
}
public function performDisabledTransformation() {
$clone = clone $this;
$clone->setDisabled(true);
return $clone;
}
/**
* Transforms the source data for this CheckboxSetField
* into a comma separated list of values.
*
* @return ReadonlyField
*/
public function performReadonlyTransformation() {
$values = '';
$data = array();
$items = $this->value;
if($this->source) {
foreach($this->source as $source) {
if(is_object($source)) {
$sourceTitles[$source->ID] = $source->Title;
}
}
}
if($items) {
// Items is a DO Set
if($items instanceof SS_List) {
foreach($items as $item) {
$data[] = $item->Title;
}
if($data) $values = implode(', ', $data);
// Items is an array or single piece of string (including comma seperated string)
} else {
if(!is_array($items)) {
$items = preg_split('/ *, */', trim($items));
}
foreach($items as $item) {
if(is_array($item)) {
$data[] = $item['Title'];
} elseif(is_array($this->source) && !empty($this->source[$item])) {
$data[] = $this->source[$item];
} elseif(is_a($this->source, 'SS_List')) {
$data[] = $sourceTitles[$item];
} else {
$data[] = $item;
}
}
$values = implode(', ', $data);
}
}
$field = $this->castedCopy('ReadonlyField');
$field->setValue($values);
return $field;
}
public function Type() { public function Type() {
return 'optionset checkboxset'; return 'optionset checkboxset';
} }
public function ExtraOptions() {
return FormField::ExtraOptions();
}
/**
* Validate this field
*
* @param Validator $validator
* @return bool
*/
public function validate($validator) {
$values = $this->value;
if (!$values) {
return true;
}
$sourceArray = $this->getSourceAsArray();
if (is_array($values)) {
if (!array_intersect_key($sourceArray, $values)) {
$validator->validationError(
$this->name,
_t(
'CheckboxSetField.SOURCE_VALIDATION',
"Please select a value within the list provided. '{value}' is not a valid option",
array('value' => implode(' and ', array_diff($sourceArray, $values)))
),
"validation"
);
return false;
}
} else {
if (!in_array($this->value, $sourceArray)) {
$validator->validationError(
$this->name,
_t(
'CheckboxSetField.SOURCE_VALIDATION',
"Please select a value within the list provided. '{value}' is not a valid option",
array('value' => $this->value)
),
"validation"
);
return false;
}
}
return true;
}
} }

View File

@ -11,13 +11,18 @@ class CountryDropdownField extends DropdownField {
/** /**
* Should we default the dropdown to the region determined from the user's locale? * Should we default the dropdown to the region determined from the user's locale?
*
* @config
* @var bool * @var bool
*/ */
private static $default_to_locale = true; private static $default_to_locale = true;
/** /**
* The region code to default to if default_to_locale is set to false, or we can't determine a region from a locale * The region code to default to if default_to_locale is set to false, or we can't
* @var string * determine a region from a locale.
*
* @config
* @var string
*/ */
private static $default_country = 'NZ'; private static $default_country = 'NZ';
@ -28,48 +33,57 @@ class CountryDropdownField extends DropdownField {
* @return string * @return string
*/ */
protected function locale() { protected function locale() {
if (($member = Member::currentUser()) && $member->Locale) return $member->Locale; if (($member = Member::currentUser()) && $member->Locale) {
return $member->Locale;
}
return i18n::get_locale(); return i18n::get_locale();
} }
public function __construct($name, $title = null, $source = null, $value = "", $form=null) { public function setSource($source) {
if(!is_array($source)) { if($source) {
// Get a list of countries from Zend return parent::setSource($source);
$source = Zend_Locale::getTranslationList('territory', $this->locale(), 2);
// We want them ordered by display name, not country code
// PHP 5.3 has an extension that sorts UTF-8 strings correctly
if (class_exists('Collator') && ($collator = Collator::create($this->locale()))) {
$collator->asort($source);
}
// Otherwise just put up with them being weirdly ordered for now
else {
asort($source);
}
// We don't want "unknown country" as an option
unset($source['ZZ']);
} }
parent::__construct($name, ($title===null) ? $name : $title, $source, $value, $form); // map empty source to country list
// Get a list of countries from Zend
$source = Zend_Locale::getTranslationList('territory', $this->locale(), 2);
// We want them ordered by display name, not country code
// PHP 5.3 has an extension that sorts UTF-8 strings correctly
if (class_exists('Collator') && ($collator = Collator::create($this->locale()))) {
$collator->asort($source);
} else {
// Otherwise just put up with them being weirdly ordered for now
asort($source);
}
// We don't want "unknown country" as an option
unset($source['ZZ']);
return parent::setSource($source);
} }
public function Field($properties = array()) { public function Field($properties = array()) {
$source = $this->getSource(); $source = $this->getSource();
if (!$this->value || !isset($source[$this->value])) { // Default value to best availabel locale
if ($this->config()->default_to_locale && $this->locale()) { $value = $this->Value();
$locale = new Zend_Locale(); if ($this->config()->default_to_locale
$locale->setLocale($this->locale()); && (!$value || !isset($source[$value]))
$this->value = $locale->getRegion(); && $this->locale()
} ) {
$locale = new Zend_Locale();
$locale->setLocale($this->locale());
$value = $locale->getRegion();
$this->setValue($value);
} }
if (!$this->value || !isset($source[$this->value])) { // Default to default country otherwise
$this->value = $this->config()->default_country; if (!$value || !isset($source[$value])) {
$this->setValue($this->config()->default_country);
} }
return parent::Field(); return parent::Field($properties);
} }
} }

View File

@ -81,51 +81,31 @@
* @package forms * @package forms
* @subpackage fields-basic * @subpackage fields-basic
*/ */
class DropdownField extends FormField { class DropdownField extends SingleSelectField {
/** /**
* @var array|ArrayAccess $source Associative or numeric array of all dropdown items, * Build a field option for template rendering
* with array key as the submitted field value, and the array value as a *
* natural language description shown in the interface element. * @param mixed $value Value of the option
* @param string $title Title of the option
* @return ArrayData Field option
*/ */
protected $source; protected function getFieldOption($value, $title) {
// Check selection
$selected = $this->isSelectedValue($value, $this->Value());
/** // Check disabled
* @var boolean $isSelected Determines if the field was selected $disabled = false;
* at the time it was rendered, so if {@link $value} matches on of the array if($this->isDisabledValue($value) && $title != $this->getEmptyString()){
* values specified in {@link $source} $disabled = 'disabled';
*/ }
protected $isSelected;
/** return new ArrayData(array(
* @var boolean $hasEmptyDefault Show the first <option> element as 'Title' => $title,
* empty (not having a value), with an optional label defined through 'Value' => $value,
* {@link $emptyString}. By default, the <select> element will be 'Selected' => $selected,
* rendered with the first option from {@link $source} selected. 'Disabled' => $disabled,
*/ ));
protected $hasEmptyDefault = false;
/**
* @var string $emptyString The title shown for an empty default selection,
* e.g. "Select...".
*/
protected $emptyString = '';
/**
* @var array $disabledItems The keys for items that should be disabled (greyed out) in the dropdown
*/
protected $disabledItems = array();
/**
* @param string $name The field name
* @param string $title The field title
* @param array|ArrayAccess $source A map of the dropdown items
* @param string $value The current value
* @param Form $form The parent form
*/
public function __construct($name, $title=null, $source=array(), $value='', $form=null) {
$this->setSource($source);
parent::__construct($name, ($title===null) ? $name : $title, $value, $form);
} }
/** /**
@ -133,49 +113,11 @@ class DropdownField extends FormField {
* @return HTMLText * @return HTMLText
*/ */
public function Field($properties = array()) { public function Field($properties = array()) {
$source = $this->getSource();
$options = array(); $options = array();
if ($this->getHasEmptyDefault()) { // Add all options
$selected = ($this->value === '' || $this->value === null); foreach($this->getSourceEmpty() as $value => $title) {
$disabled = (in_array('', $this->disabledItems, true)) ? 'disabled' : false; $options[] = $this->getFieldOption($value, $title);
$options[] = new ArrayData(array(
'Value' => '',
'Title' => $this->getEmptyString(),
'Selected' => $selected,
'Disabled' => $disabled
));
}
if ($source) {
foreach($source as $value => $title) {
$selected = false;
if($value === '' && ($this->value === '' || $this->value === null)) {
$selected = true;
} else {
// check against value, fallback to a type check comparison when !value
if($value) {
$selected = ($value == $this->value);
} else {
$selected = ($value === $this->value) || (((string) $value) === ((string) $this->value));
}
$this->isSelected = $selected;
}
$disabled = false;
if(in_array($value, $this->disabledItems) && $title != $this->emptyString ){
$disabled = 'disabled';
}
$options[] = new ArrayData(array(
'Title' => $title,
'Value' => $value,
'Selected' => $selected,
'Disabled' => $disabled,
));
}
} }
$properties = array_merge($properties, array( $properties = array_merge($properties, array(
@ -184,173 +126,4 @@ class DropdownField extends FormField {
return parent::Field($properties); return parent::Field($properties);
} }
/**
* Mark certain elements as disabled, regardless of the
* {@link setDisabled()} settings.
*
* @param array $items Collection of array keys, as defined in the $source array
*/
public function setDisabledItems($items) {
$this->disabledItems = $items;
return $this;
}
/**
* @return array
*/
public function getDisabledItems() {
return $this->disabledItems;
}
/**
* @return array
*/
public function getAttributes() {
return array_merge(
parent::getAttributes(),
array(
'type' => null,
'value' => null
)
);
}
/**
* @return boolean
*/
public function isSelected() {
return $this->isSelected;
}
/**
* Gets the source array including any empty default values.
*
* @return array|ArrayAccess
*/
public function getSource() {
return $this->source;
}
/**
* @param array|ArrayAccess $source
*/
public function setSource($source) {
$this->source = $source;
return $this;
}
/**
* @param boolean $bool
*/
public function setHasEmptyDefault($bool) {
$this->hasEmptyDefault = $bool;
return $this;
}
/**
* @return boolean
*/
public function getHasEmptyDefault() {
return $this->hasEmptyDefault;
}
/**
* Set the default selection label, e.g. "select...".
*
* Defaults to an empty string. Automatically sets {@link $hasEmptyDefault}
* to true.
*
* @param string $str
*/
public function setEmptyString($str) {
$this->setHasEmptyDefault(true);
$this->emptyString = $str;
return $this;
}
/**
* @return string
*/
public function getEmptyString() {
return $this->emptyString;
}
/**
* @return LookupField
*/
public function performReadonlyTransformation() {
$field = $this->castedCopy('LookupField');
$field->setSource($this->getSource());
$field->setReadonly(true);
return $field;
}
/**
* Get the source of this field as an array
*
* @return array
*/
public function getSourceAsArray()
{
$source = $this->getSource();
if (is_array($source)) {
return $source;
} else {
$sourceArray = array();
foreach ($source as $key => $value) {
$sourceArray[$key] = $value;
}
}
return $sourceArray;
}
/**
* Validate this field
*
* @param Validator $validator
* @return bool
*/
public function validate($validator) {
$source = $this->getSourceAsArray();
$disabled = $this->getDisabledItems();
if (!array_key_exists($this->value, $source) || in_array($this->value, $disabled)) {
if ($this->getHasEmptyDefault() && !$this->value) {
return true;
}
$validator->validationError(
$this->name,
_t(
'DropdownField.SOURCE_VALIDATION',
"Please select a value within the list provided. {value} is not a valid option",
array('value' => $this->value)
),
"validation"
);
return false;
}
return true;
}
/**
* Returns another instance of this field, but "cast" to a different class.
*
* @see FormField::castedCopy()
*
* @param String $classOrCopy
* @return FormField
*/
public function castedCopy($classOrCopy) {
$field = parent::castedCopy($classOrCopy);
if($field->hasMethod('setHasEmptyDefault')) {
$field->setHasEmptyDefault($this->getHasEmptyDefault());
}
return $field;
}
} }

View File

@ -649,7 +649,6 @@ class FormField extends RequestHandler {
*/ */
public function setValue($value) { public function setValue($value) {
$this->value = $value; $this->value = $value;
return $this; return $this;
} }

View File

@ -36,18 +36,13 @@
* *
* <b>Disabling individual items</b> * <b>Disabling individual items</b>
* *
* Unlike the source, disabled items are specified in the same way as
* normal DropdownFields, using a single value list. Don't pass in grouped
* values here.
*
* <code> * <code>
* $groupedDrDownField->setDisabledItems( * // Disables first and third option in each group
* array( * $groupedDrDownField->setDisabledItems(array("1", "3"))
* "numbers" => array(
* "1" => "1",
* "3" => "3"
* ),
* "letters" => array(
* "3" => "C"
* )
* )
* )
* </code> * </code>
* *
* @package forms * @package forms
@ -55,80 +50,47 @@
*/ */
class GroupedDropdownField extends DropdownField { class GroupedDropdownField extends DropdownField {
public function Field($properties = array()) { /**
$options = ''; * Build a potentially nested fieldgroup
foreach($this->getSource() as $value => $title) { *
if(is_array($title)) { * @param mixed $valueOrGroup Value of item, or title of group
$options .= "<optgroup label=\"$value\">"; * @param string|array $titleOrOptions Title of item, or options in grouip
foreach($title as $value2 => $title2) { * @return ArrayData Data for this item
$disabled = ''; */
if( array_key_exists($value, $this->disabledItems) protected function getFieldOption($valueOrGroup, $titleOrOptions) {
&& is_array($this->disabledItems[$value]) // Return flat option
&& in_array($value2, $this->disabledItems[$value]) ){ if(!is_array($titleOrOptions)) {
$disabled = 'disabled="disabled"'; return parent::getFieldOption($valueOrGroup, $titleOrOptions);
}
$selected = $value2 == $this->value ? " selected=\"selected\"" : "";
$options .= "<option$selected value=\"$value2\" $disabled>$title2</option>";
}
$options .= "</optgroup>";
} else { // Fall back to the standard dropdown field
$disabled = '';
if( in_array($value, $this->disabledItems) ){
$disabled = 'disabled="disabled"';
}
$selected = $value == $this->value ? " selected=\"selected\"" : "";
$options .= "<option$selected value=\"$value\" $disabled>$title</option>";
}
} }
return FormField::create_tag('select', $this->getAttributes(), $options); // Build children from options list
$options = new ArrayList();
foreach($titleOrOptions as $childValue => $childTitle) {
$options->push($this->getFieldOption($childValue, $childTitle));
}
return new ArrayData(array(
'Title' => $valueOrGroup,
'Options' => $options
));
} }
public function Type() { public function Type() {
return 'groupeddropdown dropdown'; return 'groupeddropdown dropdown';
} }
/** public function getSourceValues() {
* Validate this field // Flatten values
* $values = array();
* @param Validator $validator $source = $this->getSource();
* @return bool array_walk_recursive(
*/ $source,
public function validate($validator) { // Function to extract value from array key
$valid = false; function($title, $value) use (&$values) {
$source = $this->getSourceAsArray(); $values[] = $value;
$disabled = $this->getDisabledItems();
if ($this->value) {
foreach ($source as $value => $title) {
if (is_array($title) && array_key_exists($this->value, $title)) {
// Check that the set value is not in the list of disabled items
if (!isset($disabled[$value]) || !in_array($this->value, $disabled[$value])) {
$valid = true;
}
// Check that the value matches and is not disabled
} elseif($this->value == $value && !in_array($this->value, $disabled)) {
$valid = true;
}
} }
} elseif ($this->getHasEmptyDefault()) { );
$valid = true; return $values;
}
if (!$valid) {
$validator->validationError(
$this->name,
_t(
'DropdownField.SOURCE_VALIDATION',
"Please select a value within the list provided. {value} is not a valid option",
array('value' => $this->value)
),
"validation"
);
return false;
}
return true;
} }
} }

View File

@ -25,7 +25,7 @@
* @package forms * @package forms
* @subpackage fields-basic * @subpackage fields-basic
*/ */
class ListboxField extends DropdownField { class ListboxField extends MultiSelectField {
/** /**
* The size of the field in rows. * The size of the field in rows.
@ -33,223 +33,107 @@ class ListboxField extends DropdownField {
*/ */
protected $size; protected $size;
/**
* Should the user be able to select multiple
* items on this dropdown field?
*
* @var boolean
*/
protected $multiple = false;
/** /**
* @var Array * @var Array
*/ */
protected $disabledItems = array(); protected $disabledItems = array();
/**
* @var Array
*/
protected $defaultItems = array();
/** /**
* Creates a new dropdown field. * Creates a new dropdown field.
* *
* @param string $name The field name * @param string $name The field name
* @param string $title The field title * @param string $title The field title
* @param array $source An map of the dropdown items * @param array $source An map of the dropdown items
* @param string|array $value You can pass an array of values or a single value like a drop down to be selected * @param string|array|null $value You can pass an array of values or a single value like a drop down to be selected
* @param int $size Optional size of the select element * @param int $size Optional size of the select element
* @param form The parent form
*/ */
public function __construct($name, $title = '', $source = array(), $value = '', $size = null, $multiple = false) { public function __construct($name, $title = '', $source = array(), $value = null, $size = null) {
if($size) $this->size = $size; if($size) {
if($multiple) $this->multiple = $multiple; $this->setSize($size);
}
parent::__construct($name, $title, $source, $value); parent::__construct($name, $title, $source, $value);
} }
/** /**
* Returns a <select> tag containing all the appropriate <option> tags * Returns a <select> tag containing all the appropriate <option> tags
*
* @param array $properties
* @return string
*/ */
public function Field($properties = array()) { public function Field($properties = array()) {
if($this->multiple) $this->name .= '[]'; $properties = array_merge($properties, array(
$options = array(); 'Options' => $this->getOptions()
));
return $this
->customise($properties)
->renderWith($this->getTemplates());
}
// We have an array of values /**
if(is_array($this->value)){ * Gets the list of options to render in this formfield
// Loop through and figure out which values were selected. *
foreach($this->getSource() as $value => $title) { * @return ArrayList
$options[] = new ArrayData(array( */
'Title' => $title, public function getOptions() {
'Value' => $value, // Loop through and figure out which values were selected.
'Selected' => (in_array($value, $this->value) || in_array($value, $this->defaultItems)), $options = array();
'Disabled' => $this->disabled || in_array($value, $this->disabledItems), $selectedValue = $this->getValueArray();
)); foreach($this->getSource() as $itemValue => $title) {
} $itemSelected = in_array($itemValue, $selectedValue)
} else { || in_array($itemValue, $this->getDefaultItems());
// Listbox was based a singlular value, so treat it like a dropdown. $itemDisabled = $this->isDisabled()
foreach($this->getSource() as $value => $title) { || in_array($itemValue, $this->getDisabledItems());
$options[] = new ArrayData(array( $options[] = new ArrayData(array(
'Title' => $title, 'Title' => $title,
'Value' => $value, 'Value' => $itemValue,
'Selected' => ($value == $this->value || in_array($value, $this->defaultItems)), 'Selected' => $itemSelected,
'Disabled' => $this->disabled || in_array($value, $this->disabledItems), 'Disabled' => $itemDisabled,
)); ));
}
} }
$properties = array_merge($properties, array( $options = new ArrayList($options);
'Options' => new ArrayList($options) $this->extend('updateGetOptions', $options);
)); return $options;
return $this->customise($properties)->renderWith($this->getTemplates());
} }
public function getAttributes() { public function getAttributes() {
return array_merge( return array_merge(
parent::getAttributes(), parent::getAttributes(),
array( array(
'multiple' => $this->multiple, 'multiple' => 'true',
'size' => $this->size 'size' => $this->getSize(),
'name' => $this->getName() . '[]'
) )
); );
} }
/**
* Get the size of this dropdown in rows.
*
* @return integer
*/
public function getSize() {
return $this->size;
}
/** /**
* Sets the size of this dropdown in rows. * Sets the size of this dropdown in rows.
*
* @param int $size The height in rows (e.g. 3) * @param int $size The height in rows (e.g. 3)
* @return $this Self reference
*/ */
public function setSize($size) { public function setSize($size) {
$this->size = $size; $this->size = $size;
return $this; return $this;
} }
/**
* Sets this field to have a muliple select attribute
* @param boolean $bool
*/
public function setMultiple($bool) {
$this->multiple = $bool;
return $this;
}
public function setSource($source) {
if($source) {
$hasCommas = array_filter(array_keys($source),
create_function('$key', 'return strpos($key, ",") !== FALSE;'));
if($hasCommas) {
throw new InvalidArgumentException('No commas allowed in $source keys');
}
}
parent::setSource($source);
return $this;
}
/**
* Return the CheckboxSetField value as a string
* selected item keys.
*
* @return string
*/
public function dataValue() {
if($this->value && is_array($this->value) && $this->multiple) {
$filtered = array();
foreach($this->value as $item) {
if($item) {
$filtered[] = str_replace(",", "{comma}", $item);
}
}
return implode(',', $filtered);
} else {
return parent::dataValue();
}
}
/**
* Save the current value of this field into a DataObject.
* If the field it is saving to is a has_many or many_many relationship,
* it is saved by setByIDList(), otherwise it creates a comma separated
* list for a standard DB text/varchar field.
*
* @param DataObject $record The record to save into
*/
public function saveInto(DataObjectInterface $record) {
if($this->multiple) {
$fieldname = $this->name;
$relation = ($fieldname && $record && $record->hasMethod($fieldname)) ? $record->$fieldname() : null;
if($fieldname && $record && $relation &&
($relation instanceof RelationList || $relation instanceof UnsavedRelationList)) {
$idList = (is_array($this->value)) ? array_values($this->value) : array();
if(!$record->ID) {
$record->write(); // record needs to have an ID in order to set relationships
$relation = ($fieldname && $record && $record->hasMethod($fieldname))
? $record->$fieldname()
: null;
}
$relation->setByIDList($idList);
} elseif($fieldname && $record) {
if($this->value) {
$this->value = str_replace(',', '{comma}', $this->value);
$record->$fieldname = implode(",", $this->value);
} else {
$record->$fieldname = null;
}
}
} else {
parent::saveInto($record);
}
}
/**
* Load a value into this ListboxField
*/
public function setValue($val, $obj = null) {
// If we're not passed a value directly,
// we can look for it in a relation method on the object passed as a second arg
if(!$val && $obj && $obj instanceof DataObject && $obj->hasMethod($this->name)) {
$funcName = $this->name;
$val = array_values($obj->$funcName()->getIDList());
}
if($val) {
if(!$this->multiple && is_array($val)) {
throw new InvalidArgumentException('Array values are not allowed (when multiple=false).');
}
if($this->multiple) {
$parts = (is_array($val)) ? $val : preg_split("/ *, */", trim($val));
if(ArrayLib::is_associative($parts)) {
// This is due to the possibility of accidentally passing an array of values (as keys) and titles (as values) when only the keys were intended to be saved.
throw new InvalidArgumentException('Associative arrays are not allowed as values (when multiple=true), only indexed arrays.');
}
// Doesn't check against unknown values in order to allow for less rigid data handling.
// They're silently ignored and overwritten the next time the field is saved.
parent::setValue($parts);
} else {
if(!in_array($val, array_keys($this->getSource()))) {
throw new InvalidArgumentException(sprintf(
'Invalid value "%s" for multiple=false',
Convert::raw2xml($val)
));
}
parent::setValue($val);
}
} else {
parent::setValue($val);
}
return $this;
}
/** /**
* Mark certain elements as disabled, * Mark certain elements as disabled,
* regardless of the {@link setDisabled()} settings. * regardless of the {@link setDisabled()} settings.
* *
* @param array $items Collection of array keys, as defined in the $source array * @param array $items Collection of array keys, as defined in the $source array
* @return $this Self reference
*/ */
public function setDisabledItems($items) { public function setDisabledItems($items) {
$this->disabledItems = $items; $this->disabledItems = $items;
@ -257,70 +141,10 @@ class ListboxField extends DropdownField {
} }
/** /**
* @return Array * @return array
*/ */
public function getDisabledItems() { public function getDisabledItems() {
return $this->disabledItems; return $this->disabledItems;
} }
/**
* Default selections, regardless of the {@link setValue()} settings.
* Note: Items marked as disabled through {@link setDisabledItems()} can still be
* selected by default through this method.
*
* @param Array $items Collection of array keys, as defined in the $source array
*/
public function setDefaultItems($items) {
$this->defaultItems = $items;
return $this;
}
/**
* @return Array
*/
public function getDefaultItems() {
return $this->defaultItems;
}
/**
* Validate this field
*
* @param Validator $validator
* @return bool
*/
public function validate($validator) {
$values = $this->value;
if (!$values) {
return true;
}
$source = $this->getSourceAsArray();
if (is_array($values)) {
if (!array_intersect_key($source,array_flip($values))) {
$validator->validationError(
$this->name,
_t(
"Please select a value within the list provided. {value} is not a valid option",
array('value' => $this->value)
),
"validation"
);
return false;
}
} else {
if (!array_key_exists($this->value, $source)) {
$validator->validationError(
$this->name,
_t(
'ListboxField.SOURCE_VALIDATION',
"Please select a value within the list provided. %s is not a valid option",
array('value' => $this->value)
),
"validation"
);
return false;
}
}
return true;
}
} }

View File

@ -9,7 +9,7 @@
* @package forms * @package forms
* @subpackage fields-basic * @subpackage fields-basic
*/ */
class LookupField extends DropdownField { class LookupField extends MultiSelectField {
/** /**
* @var boolean $readonly * @var boolean $readonly
@ -24,36 +24,20 @@ class LookupField extends DropdownField {
* @return string * @return string
*/ */
public function Field($properties = array()) { public function Field($properties = array()) {
$source = $this->getSource(); $source = ArrayLib::flatten($this->getSource());
$values = $this->getValueArray();
// Normalize value to array to simplify further processing
if(is_array($this->value) || is_object($this->value)) {
$values = $this->value;
} else {
$values = array(trim($this->value));
}
// Get selected values
$mapped = array(); $mapped = array();
foreach($values as $value) {
if($source instanceof SQLMap) { if(isset($source[$value])) {
foreach($values as $value) { $mapped[] = $source[$value];
$mapped[] = $source->getItem($value);
} }
} else if($source instanceof ArrayAccess || is_array($source)) {
$source = ArrayLib::flatten($source);
foreach($values as $value) {
if(isset($source[$value])) {
$mapped[] = $source[$value];
}
}
} else {
$mapped = array();
} }
// Don't check if string arguments are matching against the source, // Don't check if string arguments are matching against the source,
// as they might be generated HTML diff views instead of the actual values // as they might be generated HTML diff views instead of the actual values
if($this->value && !is_array($this->value) && !$mapped) { if($this->value && is_string($this->value) && empty($mapped)) {
$mapped = array(trim($this->value)); $mapped = array(trim($this->value));
$values = array(); $values = array();
} }
@ -102,10 +86,13 @@ class LookupField extends DropdownField {
*/ */
public function performReadonlyTransformation() { public function performReadonlyTransformation() {
$clone = clone $this; $clone = clone $this;
return $clone; return $clone;
} }
public function getHasEmptyDefault() {
return false;
}
/** /**
* @return string * @return string
*/ */

View File

@ -5,63 +5,87 @@
*/ */
class MemberDatetimeOptionsetField extends OptionsetField { class MemberDatetimeOptionsetField extends OptionsetField {
const CUSTOM_OPTION = '__custom__';
/**
* Non-ambiguous date to use for the preview.
* Must be in 'y-MM-dd HH:mm:ss' format
*
* @var string
*/
private static $preview_date = '25-12-2011 17:30:00';
public function Field($properties = array()) { public function Field($properties = array()) {
Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/javascript/MemberDatetimeOptionsetField.js'); Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/javascript/MemberDatetimeOptionsetField.js');
$options = array();
$odd = false;
$options = ''; // Add all options striped
$odd = 0; $anySelected = false;
$source = $this->getSource(); foreach($this->getSourceEmpty() as $value => $title) {
$odd = !$odd;
foreach($source as $key => $value) { if(!$anySelected) {
// convert the ID to an HTML safe value (dots are not replaced, as they are valid in an ID attribute) $anySelected = $this->isSelectedValue($value, $this->Value());
$itemID = $this->id() . '_' . preg_replace('/[^\.a-zA-Z0-9\-\_]/', '_', $key);
if($key == $this->value) {
$useValue = false;
$checked = " checked=\"checked\"";
} else {
$checked = "";
} }
$options[] = $this->getFieldOption($value, $title, $odd);
$odd = ($odd + 1) % 2;
$extraClass = $odd ? "odd" : "even";
$extraClass .= " val" . preg_replace('/[^a-zA-Z0-9\-\_]/', '_', $key);
$disabled = ($this->disabled || in_array($key, $this->disabledItems)) ? "disabled=\"disabled\"" : "";
$ATT_key = Convert::raw2att($key);
$options .= "<li class=\"".$extraClass."\">"
. "<input id=\"$itemID\" name=\"$this->name\" type=\"radio\" value=\"$key\"$checked $disabled"
. " class=\"radio\" /> <label title=\"$ATT_key\" for=\"$itemID\">$value</label></li>\n";
} }
// Add "custom" input field // Add "custom" input field option
$value = ($this->value && !array_key_exists($this->value, $this->source)) ? $this->value : null; $options[] = $this->getCustomFieldOption(!$anySelected, !$odd);
$checked = ($value) ? " checked=\"checked\"" : '';
$options .= "<li class=\"valCustom\">"
. sprintf(
"<input id=\"%s_custom\" name=\"%s\" type=\"radio\" value=\"__custom__\" class=\"radio\" %s />",
$itemID, $this->name,
$checked
)
. sprintf(
'<label for="%s_custom">%s:</label>',
$itemID, _t('MemberDatetimeOptionsetField.Custom', 'Custom')
)
. sprintf(
"<input class=\"customFormat cms-help cms-help-tooltip\" name=\"%s_custom\" value=\"%s\" />\n",
$this->name, Convert::raw2xml($value)
)
. sprintf(
"<input type=\"hidden\" class=\"formatValidationURL\" value=\"%s\" />",
$this->Link() . '/validate'
);
$options .= ($value) ? sprintf(
'<span class="preview">(%s: "%s")</span>',
_t('MemberDatetimeOptionsetField.Preview', 'Preview'),
Convert::raw2xml(Zend_Date::now()->toString($value))
) : '';
$id = $this->id(); // Build fieldset
return "<ul id=\"$id\" class=\"optionset {$this->extraClass()}\">\n$options</ul>\n"; $properties = array_merge($properties, array(
'Options' => new ArrayList($options)
));
return $this->customise($properties)->renderWith(
$this->getTemplates()
);
}
/**
* Create the "custom" selection field option
*
* @param bool $isChecked True if this is checked
* @param bool $odd Is odd striped
* @return ArrayData
*/
protected function getCustomFieldOption($isChecked, $odd) {
// Add "custom" input field
$option = $this->getFieldOption(
self::CUSTOM_OPTION,
_t('MemberDatetimeOptionsetField.Custom', 'Custom'),
$odd
);
$option->setField('isChecked', $isChecked);
$option->setField('CustomName', $this->getName().'[Custom]');
$option->setField('CustomValue', $this->Value());
if($this->Value()) {
$preview = Convert::raw2xml($this->previewFormat($this->Value()));
$option->setField('CustomPreview', $preview);
$option->setField('CustomPreviewLabel', _t('MemberDatetimeOptionsetField.Preview', 'Preview'));
}
return $option;
}
/**
* For a given format, generate a preview for the date
*
* @param string $format Date format
* @return string
*/
protected function previewFormat($format) {
$date = $this->config()->preview_date;
$zendDate = new Zend_Date($date, 'y-MM-dd HH:mm:ss');
return $zendDate->toString($format);
}
public function getOptionName() {
return parent::getOptionName() . '[Options]';
}
public function Type() {
return 'optionset memberdatetimeoptionset';
} }
/** /**
@ -110,13 +134,20 @@ class MemberDatetimeOptionsetField extends OptionsetField {
return $output; return $output;
} }
public function setValue($value) { public function setValue($value) {
if($value == '__custom__') { // Extract custom option from postback
$value = isset($_REQUEST[$this->name . '_custom']) ? $_REQUEST[$this->name . '_custom'] : null; if(is_array($value)) {
} if(empty($value['Options'])) {
if($value) { $value = '';
parent::setValue($value); } elseif($value['Options'] === self::CUSTOM_OPTION) {
$value = $value['Custom'];
} else {
$value = $value['Options'];
}
} }
return parent::setValue($value);
} }
/** /**
@ -126,8 +157,10 @@ class MemberDatetimeOptionsetField extends OptionsetField {
* @return bool * @return bool
*/ */
public function validate($validator) { public function validate($validator) {
$value = isset($_POST[$this->name . '_custom']) ? $_POST[$this->name . '_custom'] : null; $value = $this->Value();
if(!$value) return true; // no custom value, don't validate if(!$value) {
return true; // no custom value, don't validate
}
// Check that the current date with the date format is valid or not // Check that the current date with the date format is valid or not
require_once 'Zend/Date.php'; require_once 'Zend/Date.php';
@ -135,12 +168,17 @@ class MemberDatetimeOptionsetField extends OptionsetField {
$valid = Zend_Date::isDate($date, $value); $valid = Zend_Date::isDate($date, $value);
if($valid) { if($valid) {
return true; return true;
} else {
if($validator) {
$validator->validationError($this->name,
_t('MemberDatetimeOptionsetField.DATEFORMATBAD',"Date format is invalid"), "validation", false);
}
return false;
} }
// Fail
$validator->validationError(
$this->getName(),
_t(
'MemberDatetimeOptionsetField.DATEFORMATBAD',
"Date format is invalid"
),
"validation"
);
return false;
} }
} }

219
forms/MultiSelectField.php Normal file
View File

@ -0,0 +1,219 @@
<?php
use SilverStripe\Model\Relation;
/**
* Represents a SelectField that may potentially have multiple selections, and may have
* a {@link ManyManyList} as a data source.
*/
abstract class MultiSelectField extends SelectField {
/**
* List of items to mark as checked, and may not be unchecked
*
* @var array
*/
protected $defaultItems = array();
/**
* Extracts the value of this field, normalised as an array.
* Scalar values will return a single length array, even if empty
*
* @return array List of values as an array
*/
public function getValueArray() {
return $this->getListValues($this->Value());
}
/**
* Default selections, regardless of the {@link setValue()} settings.
* Note: Items marked as disabled through {@link setDisabledItems()} can still be
* selected by default through this method.
*
* @param array $items Collection of array keys, as defined in the $source array
* @return $this Self reference
*/
public function setDefaultItems($items) {
$this->defaultItems = $this->getListValues($items);
return $this;
}
/**
* Default selections, regardless of the {@link setValue()} settings.
*
* @return array
*/
public function getDefaultItems() {
return $this->defaultItems;
}
/**
* Load a value into this MultiSelectField
*/
public function setValue($val, $obj = null) {
// If we're not passed a value directly,
// we can look for it in a relation method on the object passed as a second arg
if($obj instanceof DataObject) {
$this->loadFrom($obj);
} else {
return parent::setValue($val);
}
return $this;
}
/**
* Load the value from the dataobject into this field
*
* @param DataObjectInterface $record
*/
public function loadFrom(DataObjectInterface $record) {
$fieldName = $this->getName();
if(empty($fieldName) || empty($record)) {
return;
}
$relation = $record->hasMethod($fieldName)
? $record->$fieldName()
: null;
// Detect DB relation or field
if($relation instanceof Relation) {
// Load ids from relation
$value = array_values($relation->getIDList());
parent::setValue($value);
} elseif($record->hasField($fieldName)) {
$value = $this->stringDecode($record->$fieldName);
parent::setValue($value);
}
}
/**
* Save the current value of this MultiSelectField into a DataObject.
* If the field it is saving to is a has_many or many_many relationship,
* it is saved by setByIDList(), otherwise it creates a comma separated
* list for a standard DB text/varchar field.
*
* @param DataObject $record The record to save into
*/
public function saveInto(DataObjectInterface $record) {
$fieldName = $this->getName();
if(empty($fieldName) || empty($record)) {
return;
}
$relation = $record->hasMethod($fieldName)
? $record->$fieldName()
: null;
// Detect DB relation or field
$items = $this->getValueArray();
if($relation instanceof Relation) {
// Save ids into relation
$relation->setByIDList($items);
} elseif($record->hasField($fieldName)) {
// Save dataValue into field
$record->$fieldName = $this->stringEncode($items);
}
}
/**
* Encode a list of values into a string, or null if empty (to simplify empty checks)
*
* @param array $value
* @return string|null
*/
public function stringEncode($value) {
return $value
? json_encode(array_values($value))
: null;
}
/**
* Extract a string value into an array of values
*
* @param string $value
* @return array
*/
protected function stringDecode($value) {
// Handle empty case
if(empty($value)) {
return array();
}
// If json deserialisation fails, then fallover to legacy format
$result = json_decode($value, true);
if($result !== false) {
return $result;
}
throw new \InvalidArgumentException("Invalid string encoded value for multi select field");
}
/**
* Validate this field
*
* @param Validator $validator
* @return bool
*/
public function validate($validator) {
$values = $this->getValueArray();
$validValues = $this->getValidValues();
// Filter out selected values not in the data source
$self = $this;
$invalidValues = array_filter(
$values,
function($userValue) use ($self, $validValues) {
foreach($validValues as $formValue) {
if($self->isSelectedValue($formValue, $userValue)) {
return false;
}
}
return true;
}
);
if(empty($invalidValues)) {
return true;
}
// List invalid items
$validator->validationError(
$this->getName(),
_t(
'MultiSelectField.SOURCE_VALIDATION',
"Please select values within the list provided. Invalid option(s) {value} given",
array('value' => implode(',', $invalidValues))
),
"validation"
);
return false;
}
/**
* Transforms the source data for this CheckboxSetField
* into a comma separated list of values.
*
* @return ReadonlyField
*/
public function performReadonlyTransformation() {
$source = $this->getSource();
// Map selected values to titles
$data = array();
foreach($this->getValueArray() as $value) {
if(array_key_exists($value, $source)) {
$data[] = $source[$value];
} else {
$data[] = $value;
}
}
$values = implode(', ', $data);
$field = $this->castedCopy('ReadonlyField');
$field->setValue($values);
return $field;
}
}

View File

@ -50,33 +50,69 @@
* @package forms * @package forms
* @subpackage fields-basic * @subpackage fields-basic
*/ */
class OptionsetField extends DropdownField { class OptionsetField extends SingleSelectField {
/** /**
* {@inheritdoc} * Build a field option for template rendering
*
* @param mixed $value Value of the option
* @param string $title Title of the option
* @param boolean $odd True if this should be striped odd. Otherwise it should be striped even
* @return ArrayData Field option
*/ */
protected function getFieldOption($value, $title, $odd) {
return new ArrayData(array(
'ID' => $this->getOptionID($value),
'Class' => $this->getOptionClass($value, $odd),
'Name' => $this->getOptionName(),
'Value' => $value,
'Title' => $title,
'isChecked' => $this->isSelectedValue($value, $this->Value()),
'isDisabled' => $this->isDisabledValue($value)
));
}
/**
* Generate an ID property for a single option
*
* @param string $value
* @return string
*/
protected function getOptionID($value) {
return $this->ID() . '_' . Convert::raw2htmlid($value);
}
/**
* Get the "name" property for each item in the list
*
* @return string
*/
protected function getOptionName() {
return $this->getName();
}
/**
* Get extra classes for each item in the list
*
* @param string $value Value of this item
* @param bool $odd If this item is odd numbered in the list
* @return string
*/
protected function getOptionClass($value, $odd) {
$oddClass = $odd ? 'odd' : 'even';
$valueClass = ' val' . Convert::raw2htmlid($value);
return $oddClass . $valueClass;
}
public function Field($properties = array()) { public function Field($properties = array()) {
$source = $this->getSource();
$odd = 0;
$options = array(); $options = array();
$odd = false;
if($source) { // Add all options striped
foreach($source as $value => $title) { foreach($this->getSourceEmpty() as $value => $title) {
$itemID = $this->ID() . '_' . preg_replace('/[^a-zA-Z0-9]/', '', $value); $odd = !$odd;
$odd = ($odd + 1) % 2; $options[] = $this->getFieldOption($value, $title, $odd);
$extraClass = $odd ? 'odd' : 'even';
$extraClass .= ' val' . preg_replace('/[^a-zA-Z0-9\-\_]/', '_', $value);
$options[] = new ArrayData(array(
'ID' => $itemID,
'Class' => $extraClass,
'Name' => $this->name,
'Value' => $value,
'Title' => $title,
'isChecked' => $value == $this->value,
'isDisabled' => $this->disabled || in_array($value, $this->disabledItems),
));
}
} }
$properties = array_merge($properties, array( $properties = array_merge($properties, array(
@ -92,17 +128,13 @@ class OptionsetField extends DropdownField {
* {@inheritdoc} * {@inheritdoc}
*/ */
public function validate($validator) { public function validate($validator) {
if (!$this->value) { if (!$this->Value()) {
return true; return true;
} }
return parent::validate($validator); return parent::validate($validator);
} }
public function ExtraOptions() {
return new ArrayList();
}
public function getAttributes() { public function getAttributes() {
$attributes = parent::getAttributes(); $attributes = parent::getAttributes();
unset($attributes['name']); unset($attributes['name']);

225
forms/SelectField.php Normal file
View File

@ -0,0 +1,225 @@
<?php
/**
* Represents a field that allows users to select one or more items from a list
*/
abstract class SelectField extends FormField {
/**
* Associative or numeric array of all dropdown items,
* with array key as the submitted field value, and the array value as a
* natural language description shown in the interface element.
*
* @var array|ArrayAccess
*/
protected $source;
/**
* The values for items that should be disabled (greyed out) in the dropdown.
* This is a non-associative array
*
* @var array
*/
protected $disabledItems = array();
/**
* @param string $name The field name
* @param string $title The field title
* @param array|ArrayAccess $source A map of the dropdown items
* @param mixed $value The current value
*/
public function __construct($name, $title = null, $source = array(), $value = null) {
$this->setSource($source);
if(!isset($title)) {
$title = $name;
}
parent::__construct($name, $title, $value);
}
/**
* Mark certain elements as disabled,
* regardless of the {@link setDisabled()} settings.
*
* These should be items that appear in the source list, not in addition to them.
*
* @param array|SS_List $items Collection of values or items
*/
public function setDisabledItems($items){
$this->disabledItems = $this->getListValues($items);
return $this;
}
/**
* Non-associative list of disabled item values
*
* @return array
*/
public function getDisabledItems(){
return $this->disabledItems;
}
/**
* Check if the given value is disabled
*
* @param string $value
* @return bool
*/
protected function isDisabledValue($value) {
if($this->isDisabled()) {
return true;
}
return in_array($value, $this->getDisabledItems());
}
public function getAttributes() {
return array_merge(
parent::getAttributes(),
array('type' => null, 'value' => null)
);
}
/**
* Retrieve all values in the source array
*
* @return array
*/
protected function getSourceValues() {
return array_keys($this->getSource());
}
/**
* Gets all valid values for this field.
*
* Does not include "empty" value if specified
*
* @return array
*/
public function getValidValues() {
$valid = array_diff($this->getSourceValues(), $this->getDisabledItems());
// Renumber indexes from 0
return array_values($valid);
}
/**
* Gets the source array not including any empty default values.
*
* @return array|ArrayAccess
*/
public function getSource() {
return $this->source;
}
/**
* Set the source for this list
*
* @param mixed $source
* @return $this
*/
public function setSource($source) {
$this->source = $this->getListMap($source);
return $this;
}
/**
* Given a list of values, extract the associative map of id => title
*
* @param mixed $source
* @return array Associative array of ids and titles
*/
protected function getListMap($source) {
// Extract source as an array
if($source instanceof SS_List) {
$source = $source->map();
}
if($source instanceof SS_Map) {
$source = $source->toArray();
}
if(!is_array($source) && !($source instanceof ArrayAccess)) {
user_error('$source passed in as invalid type', E_USER_ERROR);
}
return $source;
}
/**
* Given a non-array collection, extract the non-associative list of ids
* If passing as array, treat the array values (not the keys) as the ids
*
* @param mixed $values
* @return array Non-associative list of values
*/
protected function getListValues($values) {
// Empty values
if(empty($values)) {
return array();
}
// Direct array
if(is_array($values)) {
return array_values($values);
}
// Extract lists
if($values instanceof SS_List) {
return $values->column('ID');
}
return array(trim($values));
}
/**
* Determine if the current value of this field matches the given option value
*
* @param mixed $dataValue The value as extracted from the source of this field (or empty value if available)
* @param mixed $userValue The value as submitted by the user
* @return boolean True if the selected value matches the given option value
*/
public function isSelectedValue($dataValue, $userValue) {
if($dataValue === $userValue) {
return true;
}
// Allow null to match empty strings
if($dataValue === '' && $userValue === null) {
return true;
}
// For non-falsey values do loose comparison
if($dataValue) {
return $dataValue == $userValue;
}
// For empty values, use string comparison to perform visible value match
return ((string) $dataValue) === ((string) $userValue);
}
public function performReadonlyTransformation() {
$field = $this->castedCopy('LookupField');
$field->setSource($this->getSource());
$field->setReadonly(true);
return $field;
}
public function performDisabledTransformation() {
$clone = clone $this;
$clone->setDisabled(true);
return $clone;
}
/**
* Returns another instance of this field, but "cast" to a different class.
*
* @see FormField::castedCopy()
*
* @param String $classOrCopy
* @return FormField
*/
public function castedCopy($classOrCopy) {
$field = parent::castedCopy($classOrCopy);
if($field instanceof SelectField) {
$field->setSource($this->getSource());
}
return $field;
}
}

121
forms/SingleSelectField.php Normal file
View File

@ -0,0 +1,121 @@
<?php
/**
* Represents the base class for a single-select field
*/
abstract class SingleSelectField extends SelectField {
/**
* Show the first <option> element as empty (not having a value),
* with an optional label defined through {@link $emptyString}.
* By default, the <select> element will be rendered with the
* first option from {@link $source} selected.
*
* @var bool
*/
protected $hasEmptyDefault = false;
/**
* The title shown for an empty default selection,
* e.g. "Select...".
*
* @var string
*/
protected $emptyString = '';
/**
* @param boolean $bool
* @return self Self reference
*/
public function setHasEmptyDefault($bool) {
$this->hasEmptyDefault = $bool;
return $this;
}
/**
* @return bool
*/
public function getHasEmptyDefault() {
return $this->hasEmptyDefault;
}
/**
* Set the default selection label, e.g. "select...".
* Defaults to an empty string. Automatically sets
* {@link $hasEmptyDefault} to true.
*
* @param string $string
*/
public function setEmptyString($string) {
$this->setHasEmptyDefault(true);
$this->emptyString = $string;
return $this;
}
/**
* @return string
*/
public function getEmptyString() {
return $this->emptyString;
}
/**
* Gets the source array, including the empty string, if present
*
* @return array|ArrayAccess
*/
public function getSourceEmpty() {
// Inject default option
if($this->getHasEmptyDefault()) {
return array('' => $this->getEmptyString()) + $this->getSource();
} else {
return $this->getSource();
}
}
/**
* Validate this field
*
* @param Validator $validator
* @return bool
*/
public function validate($validator) {
// Check if valid value is given
$selected = $this->Value();
if(strlen($selected)) {
// Use selection rules to check which are valid
foreach($this->getValidValues() as $formValue) {
if($this->isSelectedValue($formValue, $selected)) {
return true;
}
}
} else {
if ($this->getHasEmptyDefault()) {
// Check empty value
return true;
}
$selected = '(none)';
}
// Fail
$validator->validationError(
$this->name,
_t(
'DropdownField.SOURCE_VALIDATION',
"Please select a value within the list provided. {value} is not a valid option",
array('value' => $selected)
),
"validation"
);
return false;
}
public function castedCopy($classOrCopy) {
$field = parent::castedCopy($classOrCopy);
if($field instanceof SingleSelectField && $this->getHasEmptyDefault()) {
$field->setEmptyString($this->getEmptyString());
}
return $field;
}
}

43
model/Relation.php Normal file
View File

@ -0,0 +1,43 @@
<?php
namespace SilverStripe\Model;
use DBField;
use SS_Filterable;
use SS_Limitable;
use SS_List;
use SS_Sortable;
/**
* Abstract representation of a DB relation field, either saved or in memory
*
* @package framework
* @subpackage model
*/
interface Relation extends SS_List, SS_Filterable, SS_Sortable, SS_Limitable {
/**
* Sets the ComponentSet to be the given ID list.
* Records will be added and deleted as appropriate.
*
* @param array $idList List of IDs.
*/
public function setByIDList($idList);
/**
* Returns an array with both the keys and values set to the IDs of the records in this list.
*
* Does not return the IDs for unsaved DataObjects
*
* @return array
*/
public function getIDList();
/**
* Return the DBField object that represents the given field on the related class.
*
* @param string $fieldName Name of the field
* @return DBField The field as a DBField object
*/
public function dbObject($fieldName);
}

View File

@ -1,5 +1,7 @@
<?php <?php
use SilverStripe\Model\Relation;
/** /**
* A DataList that represents a relation. * A DataList that represents a relation.
* *
@ -8,7 +10,7 @@
* @package framework * @package framework
* @subpackage model * @subpackage model
*/ */
abstract class RelationList extends DataList { abstract class RelationList extends DataList implements Relation {
public function getForeignID() { public function getForeignID() {
return $this->dataQuery->getQueryParam('Foreign.ID'); return $this->dataQuery->getQueryParam('Foreign.ID');

View File

@ -1,5 +1,7 @@
<?php <?php
use SilverStripe\Model\Relation;
/** /**
* An {@link ArrayList} that represents an unsaved relation. * An {@link ArrayList} that represents an unsaved relation.
* *
@ -16,7 +18,7 @@
* @package framework * @package framework
* @subpackage model * @subpackage model
*/ */
class UnsavedRelationList extends ArrayList { class UnsavedRelationList extends ArrayList implements Relation {
/** /**
* The DataObject class name that this relation is on * The DataObject class name that this relation is on
@ -151,21 +153,6 @@ class UnsavedRelationList extends ArrayList {
return $this; return $this;
} }
/**
* Returns true if the given column can be used to filter the records.
*/
public function canFilterBy($by) {
return false;
}
/**
* Returns true if the given column can be used to sort the records.
*/
public function canSortBy($by) {
return false;
}
/** /**
* Remove all items from this relation. * Remove all items from this relation.
*/ */
@ -177,7 +164,7 @@ class UnsavedRelationList extends ArrayList {
/** /**
* Remove the items from this list with the given IDs * Remove the items from this list with the given IDs
* *
* @param array $idList * @param array $items
*/ */
public function removeMany($items) { public function removeMany($items) {
$this->items = array_diff($this->items, $items); $this->items = array_diff($this->items, $items);
@ -283,156 +270,4 @@ class UnsavedRelationList extends ArrayList {
public function dbObject($fieldName) { public function dbObject($fieldName) {
return singleton($this->dataClass)->dbObject($fieldName); return singleton($this->dataClass)->dbObject($fieldName);
} }
/**#@+
* Prevents calling DataList methods that rely on the objects being saved
*/
public function addFilter() {
throw new LogicException(__FUNCTION__ . " can't be called on an UnsavedRelationList.");
}
public function alterDataQuery() {
throw new LogicException(__FUNCTION__ . " can't be called on an UnsavedRelationList.");
}
public function avg() {
throw new LogicException(__FUNCTION__ . " can't be called on an UnsavedRelationList.");
}
public function byIDs() {
throw new LogicException(__FUNCTION__ . " can't be called on an UnsavedRelationList.");
}
public function byID($id) {
throw new LogicException(__FUNCTION__ . " can't be called on an UnsavedRelationList.");
}
public function dataQuery() {
throw new LogicException(__FUNCTION__ . " can't be called on an UnsavedRelationList.");
}
public function exclude() {
throw new LogicException(__FUNCTION__ . " can't be called on an UnsavedRelationList.");
}
public function filter() {
throw new LogicException(__FUNCTION__ . " can't be called on an UnsavedRelationList.");
}
public function getRange($offset, $length) {
throw new LogicException(__FUNCTION__ . " can't be called on an UnsavedRelationList.");
}
public function innerJoin() {
throw new LogicException(__FUNCTION__ . " can't be called on an UnsavedRelationList.");
}
public function insertFirst() {
throw new LogicException(__FUNCTION__ . " can't be called on an UnsavedRelationList.");
}
public function join() {
throw new LogicException(__FUNCTION__ . " can't be called on an UnsavedRelationList.");
}
public function leftJoin() {
throw new LogicException(__FUNCTION__ . " can't be called on an UnsavedRelationList.");
}
public function limit($length, $offset = 0) {
throw new LogicException(__FUNCTION__ . " can't be called on an UnsavedRelationList.");
}
public function map($keyField = 'ID', $titleField = 'Title') {
throw new LogicException(__FUNCTION__ . " can't be called on an UnsavedRelationList.");
}
public function max() {
throw new LogicException(__FUNCTION__ . " can't be called on an UnsavedRelationList.");
}
public function merge($with) {
throw new LogicException(__FUNCTION__ . " can't be called on an UnsavedRelationList.");
}
public function min() {
throw new LogicException(__FUNCTION__ . " can't be called on an UnsavedRelationList.");
}
public function newObject() {
throw new LogicException(__FUNCTION__ . " can't be called on an UnsavedRelationList.");
}
public function offsetExists($offset) {
throw new LogicException(__FUNCTION__ . " can't be called on an UnsavedRelationList.");
}
public function offsetGet($offset) {
throw new LogicException(__FUNCTION__ . " can't be called on an UnsavedRelationList.");
}
public function offsetSet($offset, $value) {
throw new LogicException(__FUNCTION__ . " can't be called on an UnsavedRelationList.");
}
public function offsetUnset($offset) {
throw new LogicException(__FUNCTION__ . " can't be called on an UnsavedRelationList.");
}
public function pop() {
throw new LogicException(__FUNCTION__ . " can't be called on an UnsavedRelationList.");
}
public function relation() {
throw new LogicException(__FUNCTION__ . " can't be called on an UnsavedRelationList.");
}
public function removeByFilter() {
throw new LogicException(__FUNCTION__ . " can't be called on an UnsavedRelationList.");
}
public function removeByID() {
throw new LogicException(__FUNCTION__ . " can't be called on an UnsavedRelationList.");
}
public function reverse() {
throw new LogicException(__FUNCTION__ . " can't be called on an UnsavedRelationList.");
}
public function setDataModel() {
throw new LogicException(__FUNCTION__ . " can't be called on an UnsavedRelationList.");
}
public function setDataQuery() {
throw new LogicException(__FUNCTION__ . " can't be called on an UnsavedRelationList.");
}
public function setQueriedColumns() {
throw new LogicException(__FUNCTION__ . " can't be called on an UnsavedRelationList.");
}
public function shift() {
throw new LogicException(__FUNCTION__ . " can't be called on an UnsavedRelationList.");
}
public function sql() {
throw new LogicException(__FUNCTION__ . " can't be called on an UnsavedRelationList.");
}
public function subtract() {
throw new LogicException(__FUNCTION__ . " can't be called on an UnsavedRelationList.");
}
public function sum() {
throw new LogicException(__FUNCTION__ . " can't be called on an UnsavedRelationList.");
}
public function unshift($item) {
throw new LogicException(__FUNCTION__ . " can't be called on an UnsavedRelationList.");
}
public function where() {
throw new LogicException(__FUNCTION__ . " can't be called on an UnsavedRelationList.");
}
/**#@-*/
} }

View File

@ -202,7 +202,6 @@ class Group extends DataObject {
} }
$rolesField = ListboxField::create('Roles', false, $allRoles->map()->toArray()) $rolesField = ListboxField::create('Roles', false, $allRoles->map()->toArray())
->setMultiple(true)
->setDefaultItems($groupRoleIDs) ->setDefaultItems($groupRoleIDs)
->setAttribute('data-placeholder', _t('Group.AddRole', 'Add a role for this group')) ->setAttribute('data-placeholder', _t('Group.AddRole', 'Add a role for this group'))
->setDisabledItems($inheritedRoleIDs); ->setDisabledItems($inheritedRoleIDs);

View File

@ -1342,7 +1342,6 @@ class Member extends DataObject implements TemplateGlobalProvider {
asort($groupsMap); asort($groupsMap);
$fields->addFieldToTab('Root.Main', $fields->addFieldToTab('Root.Main',
ListboxField::create('DirectGroups', singleton('Group')->i18n_plural_name()) ListboxField::create('DirectGroups', singleton('Group')->i18n_plural_name())
->setMultiple(true)
->setSource($groupsMap) ->setSource($groupsMap)
->setAttribute( ->setAttribute(
'data-placeholder', 'data-placeholder',

View File

@ -0,0 +1,12 @@
<% if $Options %>
<optgroup label="$Title.ATT">
<% loop $Options %>
<% include GroupedDropdownFieldOption %>
<% end_loop %>
</optgroup>
<% else %>
<option value="$Value.ATT"
<% if $Selected %> selected="selected"<% end_if %>
<% if $Disabled %> disabled="disabled"<% end_if %>
><% if $Title %>$Title.XML<% else %>&nbsp;<% end_if %></option>
<% end_if %>

View File

@ -1,5 +1,9 @@
<select $AttributesHTML> <select $AttributesHTML>
<% loop $Options %> <% loop $Options %>
<option value="$Value.XML"<% if $Selected %> selected="selected"<% end_if %><% if $Disabled %> disabled="disabled"<% end_if %>><% if Title %>$Title.XML<% else %>&nbsp;<% end_if %></option> <option value="$Value.XML"
<% if $Selected %> selected="selected"<% end_if %>
<% if $Disabled %> disabled="disabled"<% end_if %>
><% if $Title %>$Title.XML<% else %>&nbsp;<% end_if %>
</option>
<% end_loop %> <% end_loop %>
</select> </select>

View File

@ -0,0 +1,5 @@
<select $AttributesHTML>
<% loop $Options %>
<% include GroupedDropdownFieldOption %>
<% end_loop %>
</select>

View File

@ -0,0 +1,5 @@
<select $AttributesHTML>
<% loop $Options %>
<option value="$Value.XML"<% if $Selected %> selected="selected"<% end_if %><% if $Disabled %> disabled="disabled"<% end_if %>>$Title.XML</option>
<% end_loop %>
</select>

View File

@ -0,0 +1,14 @@
<ul $AttributesHTML>
<% loop $Options %>
<li class="$Class">
<input id="$ID" class="radio" name="$Name" type="radio" value="$Value.ATT" <% if $isChecked %> checked<% end_if %><% if $isDisabled %> disabled<% end_if %> />
<label for="$ID">$Title.XML</label>
<% if $CustomName %>
<input class="customFormat cms-help cms-help-tooltip" name="$CustomName" value="$CustomValue.ATT">
<% if $CustomPreview %>
<span class="preview">({$CustomPreviewLabel.XML}: "{$CustomPreview.XML}")</span>
<% end_if %>
<% end_if %>
</li>
<% end_loop %>
</ul>

View File

@ -49,6 +49,39 @@ class CheckboxSetFieldTest extends SapphireTest {
); );
} }
/**
* Test different data sources
*/
public function testSources() {
// Array
$items = array('a' => 'Apple', 'b' => 'Banana', 'c' => 'Cranberry');
$field = new CheckboxSetField('Field', null, $items);
$this->assertEquals($items, $field->getSource());
// SS_List
$list = new ArrayList(array(
new ArrayData(array(
'ID' => 'a',
'Title' => 'Apple'
)),
new ArrayData(array(
'ID' => 'b',
'Title' => 'Banana'
)),
new ArrayData(array(
'ID' => 'c',
'Title' => 'Cranberry'
))
));
$field2 = new CheckboxSetField('Field', null, $list);
$this->assertEquals($items, $field2->getSource());
$field3 = new CheckboxSetField('Field', null, $list->map());
$this->assertEquals($items, $field3->getSource());
}
public function testSaveWithNothingSelected() { public function testSaveWithNothingSelected() {
$article = $this->objFromFixture('CheckboxSetFieldTest_Article', 'articlewithouttags'); $article = $this->objFromFixture('CheckboxSetFieldTest_Article', 'articlewithouttags');
@ -73,11 +106,11 @@ class CheckboxSetFieldTest extends SapphireTest {
$tag1 = $this->objFromFixture('CheckboxSetFieldTest_Tag', 'tag1'); $tag1 = $this->objFromFixture('CheckboxSetFieldTest_Tag', 'tag1');
$tag2 = $this->objFromFixture('CheckboxSetFieldTest_Tag', 'tag2'); $tag2 = $this->objFromFixture('CheckboxSetFieldTest_Tag', 'tag2');
/* Create a CheckboxSetField with 2 items selected. Note that the array is in the format (key) => (selected) */ /* Create a CheckboxSetField with 2 items selected. Note that the array is a list of values */
$field = new CheckboxSetField("Tags", "Test field", DataObject::get("CheckboxSetFieldTest_Tag")->map()); $field = new CheckboxSetField("Tags", "Test field", DataObject::get("CheckboxSetFieldTest_Tag")->map());
$field->setValue(array( $field->setValue(array(
$tag1->ID => true, $tag1->ID,
$tag2->ID => true $tag2->ID
)); ));
/* Saving should work */ /* Saving should work */
@ -115,12 +148,14 @@ class CheckboxSetFieldTest extends SapphireTest {
new FieldList() new FieldList()
); );
$form->loadDataFrom($articleWithTags); $form->loadDataFrom($articleWithTags);
$value = $field->Value();
sort($value);
$this->assertEquals( $this->assertEquals(
array( array(
$tag1->ID => $tag1->ID, $tag1->ID,
$tag2->ID => $tag2->ID $tag2->ID
), ),
$field->Value(), $value,
'CheckboxSetField loads data from a manymany relationship in an object through Form->loadDataFrom()' 'CheckboxSetField loads data from a manymany relationship in an object through Form->loadDataFrom()'
); );
} }
@ -141,33 +176,36 @@ class CheckboxSetFieldTest extends SapphireTest {
$article->ID $article->ID
))->value(); ))->value();
$this->assertEquals('Test,Another', $dbValue); // JSON encoded values
$this->assertEquals('["Test","Another"]', $dbValue);
} }
public function testValidationWithArray() { public function testValidationWithArray() {
//test with array input // Test with array input
$field = CheckboxSetField::create('Test', 'Testing', array( $field = CheckboxSetField::create('Test', 'Testing', array(
"One" => "One", "One" => "One",
"Two" => "Two", "Two" => "Two",
"Three" => "Three" "Three" => "Three"
)); ));
$validator = new RequiredFields(); $validator = new RequiredFields();
$field->setValue(array("One" => "One", "Two" => "Two")); $field->setValue(array("One", "Two"));
$this->assertTrue( $this->assertTrue(
$field->validate($validator), $field->validate($validator),
'Field validates values within source array' 'Field validates values within source array'
); );
//non valid value should fail
// Non valid value should fail
$field->setValue(array("Four" => "Four")); $field->setValue(array("Four" => "Four"));
$this->assertFalse( $this->assertFalse(
$field->validate($validator), $field->validate($validator),
'Field does not validate values outside of source array' 'Field does not validate values outside of source array'
); );
//non valid value included with valid options should succeed
$field->setValue(array("One" => "One", "Two" => "Two", "Four" => "Four")); // Non valid value, even if included with valid options, should fail
$this->assertTrue( $field->setValue(array("One", "Two", "Four"));
$this->assertFalse(
$field->validate($validator), $field->validate($validator),
'Field validates when presented with mixed valid and invalid values' 'Field does not validate when presented with mixed valid and invalid values'
); );
} }
@ -200,9 +238,9 @@ class CheckboxSetFieldTest extends SapphireTest {
$tag2->ID => $tag2->ID, $tag2->ID => $tag2->ID,
$tag3->ID => $tag3->ID $tag3->ID => $tag3->ID
)); ));
$this->assertTrue( $this->assertFalse(
$field->validate($validator), $field->validate($validator),
'Validates when presented with mixed valid and invalid values' 'Field does not validate when presented with mixed valid and invalid values'
); );
} }

View File

@ -16,6 +16,37 @@ class DropdownFieldTest extends SapphireTest {
); );
} }
/**
* Test different data sources
*/
public function testSources() {
// Array
$items = array('a' => 'Apple', 'b' => 'Banana', 'c' => 'Cranberry');
$field = new DropdownField('Field', null, $items);
$this->assertEquals($items, $field->getSource());
// SS_List
$list = new ArrayList(array(
new ArrayData(array(
'ID' => 'a',
'Title' => 'Apple'
)),
new ArrayData(array(
'ID' => 'b',
'Title' => 'Banana'
)),
new ArrayData(array(
'ID' => 'c',
'Title' => 'Cranberry'
))
));
$field2 = new DropdownField('Field', null, $list);
$this->assertEquals($items, $field2->getSource());
$field3 = new DropdownField('Field', null, $list->map());
$this->assertEquals($items, $field3->getSource());
}
public function testReadonlyField() { public function testReadonlyField() {
$field = new DropdownField('FeelingOk', 'Are you feeling ok?', array(0 => 'No', 1 => 'Yes')); $field = new DropdownField('FeelingOk', 'Are you feeling ok?', array(0 => 'No', 1 => 'Yes'));
$field->setEmptyString('(Select one)'); $field->setEmptyString('(Select one)');

View File

@ -18,6 +18,8 @@ class GroupedDropdownFieldTest extends SapphireTest {
) )
)); ));
$this->assertEquals(array("1", "2", "3", "4"), $field->getValidValues());
$validator = new RequiredFields(); $validator = new RequiredFields();
$field->setValue("1"); $field->setValue("1");
@ -43,11 +45,10 @@ class GroupedDropdownFieldTest extends SapphireTest {
//disabled items shouldn't validate //disabled items shouldn't validate
$field->setDisabledItems(array('1')); $field->setDisabledItems(array('1'));
$field->setValue('1'); $field->setValue('1');
$this->assertFalse($field->validate($validator));
//grouped disabled items shouldn't validate $this->assertEquals(array("2", "3", "4"), $field->getValidValues());
$field->setDisabledItems(array("Group One" => array("2"))); $this->assertEquals(array("1"), $field->getDisabledItems());
$field->setValue('2');
$this->assertFalse($field->validate($validator)); $this->assertFalse($field->validate($validator));
} }

View File

@ -17,7 +17,6 @@ class ListboxFieldTest extends SapphireTest {
$tag2 = $this->objFromFixture('ListboxFieldTest_Tag', 'tag2'); $tag2 = $this->objFromFixture('ListboxFieldTest_Tag', 'tag2');
$tag3 = $this->objFromFixture('ListboxFieldTest_Tag', 'tag3'); $tag3 = $this->objFromFixture('ListboxFieldTest_Tag', 'tag3');
$field = new ListboxField("Tags", "Test field", DataObject::get("ListboxFieldTest_Tag")->map()->toArray()); $field = new ListboxField("Tags", "Test field", DataObject::get("ListboxFieldTest_Tag")->map()->toArray());
$field->setMultiple(true);
$field->setValue(null, $articleWithTags); $field->setValue(null, $articleWithTags);
$p = new CSSContentParser($field->Field()); $p = new CSSContentParser($field->Field());
@ -35,7 +34,6 @@ class ListboxFieldTest extends SapphireTest {
$tag2 = $this->objFromFixture('ListboxFieldTest_Tag', 'tag2'); $tag2 = $this->objFromFixture('ListboxFieldTest_Tag', 'tag2');
$tag3 = $this->objFromFixture('ListboxFieldTest_Tag', 'tag3'); $tag3 = $this->objFromFixture('ListboxFieldTest_Tag', 'tag3');
$field = new ListboxField("Tags", "Test field", DataObject::get("ListboxFieldTest_Tag")->map()->toArray()); $field = new ListboxField("Tags", "Test field", DataObject::get("ListboxFieldTest_Tag")->map()->toArray());
$field->setMultiple(true);
$field->setValue(null, $articleWithTags); $field->setValue(null, $articleWithTags);
$field->setDisabledItems(array($tag1->ID, $tag3->ID)); $field->setDisabledItems(array($tag1->ID, $tag3->ID));
@ -54,7 +52,6 @@ class ListboxFieldTest extends SapphireTest {
public function testSaveIntoNullValueWithMultipleOff() { public function testSaveIntoNullValueWithMultipleOff() {
$choices = array('a' => 'a value', 'b' => 'b value','c' => 'c value'); $choices = array('a' => 'a value', 'b' => 'b value','c' => 'c value');
$field = new ListboxField('Choices', 'Choices', $choices); $field = new ListboxField('Choices', 'Choices', $choices);
$field->multiple = true;
$obj = new ListboxFieldTest_DataObject(); $obj = new ListboxFieldTest_DataObject();
$field->setValue('a'); $field->setValue('a');
@ -67,10 +64,9 @@ class ListboxFieldTest extends SapphireTest {
public function testSaveIntoNullValueWithMultipleOn() { public function testSaveIntoNullValueWithMultipleOn() {
$choices = array('a' => 'a value', 'b' => 'b value','c' => 'c value'); $choices = array('a' => 'a value', 'b' => 'b value','c' => 'c value');
$field = new ListboxField('Choices', 'Choices', $choices); $field = new ListboxField('Choices', 'Choices', $choices);
$field->multiple = true;
$obj = new ListboxFieldTest_DataObject(); $obj = new ListboxFieldTest_DataObject();
$field->setValue('a,c'); $field->setValue(array('a', 'c'));
$field->saveInto($obj); $field->saveInto($obj);
$field->setValue(''); $field->setValue('');
$field->saveInto($obj); $field->saveInto($obj);
@ -80,30 +76,30 @@ class ListboxFieldTest extends SapphireTest {
public function testSaveInto() { public function testSaveInto() {
$choices = array('a' => 'a value', 'b' => 'b value','c' => 'c value'); $choices = array('a' => 'a value', 'b' => 'b value','c' => 'c value');
$field = new ListboxField('Choices', 'Choices', $choices); $field = new ListboxField('Choices', 'Choices', $choices);
$field->multiple = false;
$obj = new ListboxFieldTest_DataObject(); $obj = new ListboxFieldTest_DataObject();
$field->setValue('a'); $field->setValue('a');
$field->saveInto($obj); $field->saveInto($obj);
$this->assertEquals('a', $obj->Choices); $this->assertEquals('["a"]', $obj->Choices);
} }
public function testSaveIntoMultiple() { public function testSaveIntoMultiple() {
$choices = array('a' => 'a value', 'b' => 'b value','c' => 'c value'); $choices = array('a' => 'a value', 'b' => 'b value','c' => 'c value');
$field = new ListboxField('Choices', 'Choices', $choices); $field = new ListboxField('Choices', 'Choices', $choices);
$field->multiple = true;
// As array // As array
$obj1 = new ListboxFieldTest_DataObject(); $obj1 = new ListboxFieldTest_DataObject();
$field->setValue(array('a', 'c')); $field->setValue(array('a', 'c'));
$field->saveInto($obj1); $field->saveInto($obj1);
$this->assertEquals('a,c', $obj1->Choices); $this->assertEquals('["a","c"]', $obj1->Choices);
// As string // As string
$obj2 = new ListboxFieldTest_DataObject(); $obj2 = new ListboxFieldTest_DataObject();
$field->setValue('a,c'); $obj2->Choices = '["a","c"]';
$field->setValue(null, $obj2);
$this->assertEquals(array('a', 'c'), $field->Value());
$field->saveInto($obj2); $field->saveInto($obj2);
$this->assertEquals('a,c', $obj2->Choices); $this->assertEquals('["a","c"]', $obj2->Choices);
} }
public function testSaveIntoManyManyRelation() { public function testSaveIntoManyManyRelation() {
@ -112,7 +108,6 @@ class ListboxFieldTest extends SapphireTest {
$tag1 = $this->objFromFixture('ListboxFieldTest_Tag', 'tag1'); $tag1 = $this->objFromFixture('ListboxFieldTest_Tag', 'tag1');
$tag2 = $this->objFromFixture('ListboxFieldTest_Tag', 'tag2'); $tag2 = $this->objFromFixture('ListboxFieldTest_Tag', 'tag2');
$field = new ListboxField("Tags", "Test field", DataObject::get("ListboxFieldTest_Tag")->map()->toArray()); $field = new ListboxField("Tags", "Test field", DataObject::get("ListboxFieldTest_Tag")->map()->toArray());
$field->setMultiple(true);
// Save new relations // Save new relations
$field->setValue(array($tag1->ID,$tag2->ID)); $field->setValue(array($tag1->ID,$tag2->ID));
@ -133,37 +128,9 @@ class ListboxFieldTest extends SapphireTest {
$this->assertEquals(array(), $article->Tags()->sort('ID')->column('ID')); $this->assertEquals(array(), $article->Tags()->sort('ID')->column('ID'));
} }
/**
* @expectedException InvalidArgumentException
*/
public function testSetValueFailsOnArrayIfMultipleIsOff() {
$choices = array('a' => 'a value', 'b' => 'b value','c' => 'c value');
$field = new ListboxField('Choices', 'Choices', $choices);
$field->multiple = false;
// As array (type error)
$failsOnArray = false;
$obj = new ListboxFieldTest_DataObject();
$field->setValue(array('a', 'c'));
}
/**
* @expectedException InvalidArgumentException
*/
public function testSetValueFailsOnStringIfChoiceInvalidAndMultipleIsOff() {
$choices = array('a' => 'a value', 'b' => 'b value','c' => 'c value');
$field = new ListboxField('Choices', 'Choices', $choices);
$field->multiple = false;
// As string (invalid choice as comma is regarded literal)
$obj = new ListboxFieldTest_DataObject();
$field->setValue('invalid');
}
public function testFieldRenderingMultipleOff() { public function testFieldRenderingMultipleOff() {
$choices = array('a' => 'a value', 'b' => 'b value','c' => 'c value'); $choices = array('a' => 'a value', 'b' => 'b value','c' => 'c value');
$field = new ListboxField('Choices', 'Choices', $choices); $field = new ListboxField('Choices', 'Choices', $choices);
$field->multiple = true;
$field->setValue('a'); $field->setValue('a');
$parser = new CSSContentParser($field->Field()); $parser = new CSSContentParser($field->Field());
$optEls = $parser->getBySelector('option'); $optEls = $parser->getBySelector('option');
@ -176,8 +143,7 @@ class ListboxFieldTest extends SapphireTest {
public function testFieldRenderingMultipleOn() { public function testFieldRenderingMultipleOn() {
$choices = array('a' => 'a value', 'b' => 'b value','c' => 'c value'); $choices = array('a' => 'a value', 'b' => 'b value','c' => 'c value');
$field = new ListboxField('Choices', 'Choices', $choices); $field = new ListboxField('Choices', 'Choices', $choices);
$field->multiple = true; $field->setValue(array('a', 'c'));
$field->setValue('a,c');
$parser = new CSSContentParser($field->Field()); $parser = new CSSContentParser($field->Field());
$optEls = $parser->getBySelector('option'); $optEls = $parser->getBySelector('option');
$this->assertEquals(3, count($optEls)); $this->assertEquals(3, count($optEls));
@ -186,14 +152,6 @@ class ListboxFieldTest extends SapphireTest {
$this->assertEquals('selected', (string)$optEls[2]['selected']); $this->assertEquals('selected', (string)$optEls[2]['selected']);
} }
/**
* @expectedException InvalidArgumentException
*/
public function testCommasInSourceKeys() {
$choices = array('a' => 'a value', 'b,with,comma' => 'b value,with,comma',);
$field = new ListboxField('Choices', 'Choices', $choices);
}
public function testValidationWithArray() { public function testValidationWithArray() {
//test with array input //test with array input
$field = ListboxField::create('Test', 'Testing', array( $field = ListboxField::create('Test', 'Testing', array(
@ -208,7 +166,6 @@ class ListboxFieldTest extends SapphireTest {
$field->validate($validator), $field->validate($validator),
'Validates values in source map' 'Validates values in source map'
); );
$field->setMultiple(true);
$field->setValue(array(1)); $field->setValue(array(1));
$this->assertTrue( $this->assertTrue(
$field->validate($validator), $field->validate($validator),
@ -247,7 +204,6 @@ class ListboxFieldTest extends SapphireTest {
// $field->validate($validator), // $field->validate($validator),
// 'Field does not validate values outside of source map' // 'Field does not validate values outside of source map'
// ); // );
$field->setMultiple(true);
$field->setValue(false, new ArrayData(array( $field->setValue(false, new ArrayData(array(
$tag1->ID => $tag1->ID, $tag1->ID => $tag1->ID,
$tag2->ID => $tag2->ID $tag2->ID => $tag2->ID

View File

@ -59,7 +59,7 @@ class MemberDatetimeOptionsetFieldTest extends SapphireTest {
$field->setForm(new Form(new MemberDatetimeOptionsetFieldTest_Controller(), 'Form', new FieldList(), $field->setForm(new Form(new MemberDatetimeOptionsetFieldTest_Controller(), 'Form', new FieldList(),
new FieldList())); // fake form new FieldList())); // fake form
$parser = new CSSContentParser($field->Field()); $parser = new CSSContentParser($field->Field());
$xmlArr = $parser->getBySelector('#Form_Form_TimeFormat_h_mm_ss_a'); $xmlArr = $parser->getBySelector('#Form_Form_TimeFormat_h:mm:ss_a');
$this->assertEquals('checked', (string) $xmlArr[0]['checked']); $this->assertEquals('checked', (string) $xmlArr[0]['checked']);
} }
@ -81,8 +81,7 @@ class MemberDatetimeOptionsetFieldTest extends SapphireTest {
$field->setForm(new Form(new MemberDatetimeOptionsetFieldTest_Controller(), 'Form', new FieldList(), $field->setForm(new Form(new MemberDatetimeOptionsetFieldTest_Controller(), 'Form', new FieldList(),
new FieldList())); // fake form new FieldList())); // fake form
$parser = new CSSContentParser($field->Field()); $parser = new CSSContentParser($field->Field());
$xmlInputArr = $parser->getBySelector('.valCustom input'); $xmlInputArr = $parser->getBySelector('.valcustom input');
$xmlPreview = $parser->getBySelector('.preview');
$this->assertEquals('checked', (string) $xmlInputArr[0]['checked']); $this->assertEquals('checked', (string) $xmlInputArr[0]['checked']);
$this->assertEquals('dd MM yy', (string) $xmlInputArr[1]['value']); $this->assertEquals('dd MM yy', (string) $xmlInputArr[1]['value']);
} }
@ -91,9 +90,15 @@ class MemberDatetimeOptionsetFieldTest extends SapphireTest {
$field = new MemberDatetimeOptionsetField('DateFormat', 'DateFormat'); $field = new MemberDatetimeOptionsetField('DateFormat', 'DateFormat');
$validator = new RequiredFields(); $validator = new RequiredFields();
$this->assertTrue($field->validate($validator)); $this->assertTrue($field->validate($validator));
$_POST['DateFormat_custom'] = 'dd MM yyyy'; $field->setValue(array(
'Options' => '__custom__',
'Custom' => 'dd MM yyyy'
));
$this->assertTrue($field->validate($validator)); $this->assertTrue($field->validate($validator));
$_POST['DateFormat_custom'] = 'sdfdsfdfd1244'; $field->setValue(array(
'Options' => '__custom__',
'Custom' => 'sdfdsfdfd1244'
));
$this->assertFalse($field->validate($validator)); $this->assertFalse($field->validate($validator));
} }