From 06a2b10b4730d3bf2c79eeb883c958537ca76b05 Mon Sep 17 00:00:00 2001 From: Thomas Portelange Date: Mon, 26 Jun 2023 09:58:50 +0200 Subject: [PATCH 1/9] provide method to create html options --- src/Forms/SelectField.php | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/Forms/SelectField.php b/src/Forms/SelectField.php index edc3f4f0401..6ea7531081e 100644 --- a/src/Forms/SelectField.php +++ b/src/Forms/SelectField.php @@ -5,12 +5,16 @@ use SilverStripe\ORM\SS_List; use SilverStripe\ORM\Map; use ArrayAccess; +use SilverStripe\Core\Convert; /** * Represents a field that allows users to select one or more items from a list */ abstract class SelectField extends FormField { + private static $casting = [ + 'OptionsHTML' => 'HTMLFragment', + ]; /** * Associative or numeric array of all dropdown items, @@ -271,4 +275,33 @@ public function castedCopy($classOrCopy) } return $field; } + + /** + * Provides '; + $options[] = $item; + } + + return implode("\n", $options); + } } From 162cadf91976b3ddd771c7c96609a435420e3aa1 Mon Sep 17 00:00:00 2001 From: Thomas Portelange Date: Mon, 26 Jun 2023 10:00:02 +0200 Subject: [PATCH 2/9] use OptionsHTML --- templates/SilverStripe/Forms/DropdownField.ss | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/templates/SilverStripe/Forms/DropdownField.ss b/templates/SilverStripe/Forms/DropdownField.ss index da566e5b5b8..29d8f8ef54c 100644 --- a/templates/SilverStripe/Forms/DropdownField.ss +++ b/templates/SilverStripe/Forms/DropdownField.ss @@ -1,9 +1,3 @@ From 33990f569cf62d745e375d2f7141874e3ea28029 Mon Sep 17 00:00:00 2001 From: Thomas Portelange Date: Mon, 26 Jun 2023 11:33:05 +0200 Subject: [PATCH 3/9] use full attributes --- src/Forms/SelectField.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Forms/SelectField.php b/src/Forms/SelectField.php index 6ea7531081e..5fa4fcef35d 100644 --- a/src/Forms/SelectField.php +++ b/src/Forms/SelectField.php @@ -292,16 +292,16 @@ public function OptionsHTML(): string foreach ($source as $value => $title) { $selected = ''; if ($this->isSelectedValue($value, $currentValue)) { - $selected = ' selected'; + $selected = ' selected="selected"'; } $disabled = ''; if ($this->isDisabledValue($value) && $title != $emptyString) { - $disabled = ' disabled'; + $disabled = ' disabled="disabled"'; } $item = ''; $options[] = $item; } - + return implode("\n", $options); } } From 1454aab96e0d401de8d4717fa137183da796b5a5 Mon Sep 17 00:00:00 2001 From: Thomas Portelange Date: Mon, 26 Jun 2023 14:25:04 +0200 Subject: [PATCH 4/9] prevent template caching --- src/Forms/DropdownField.php | 393 ++++++++++++++++++++++++++---------- 1 file changed, 282 insertions(+), 111 deletions(-) diff --git a/src/Forms/DropdownField.php b/src/Forms/DropdownField.php index 765b8a9df31..10db5f65820 100644 --- a/src/Forms/DropdownField.php +++ b/src/Forms/DropdownField.php @@ -2,145 +2,316 @@ namespace SilverStripe\Forms; -use SilverStripe\ORM\ArrayList; -use SilverStripe\View\ArrayData; +use ArrayAccess; +use SilverStripe\ORM\Map; +use SilverStripe\ORM\SS_List; +use SilverStripe\Core\Convert; +use SilverStripe\ORM\FieldType\DBHTMLText; /** - * Dropdown field, created from a select tag. - * - * Setting a $has_one relation - * - * Using here an example of an art gallery, with Exhibition pages, - * each of which has a Gallery they belong to. The Gallery class is also user-defined. - * - * static $has_one = array( - * 'Gallery' => 'Gallery', - * ); - * - * public function getCMSFields() { - * $fields = parent::getCMSFields(); - * $field = DropdownField::create('GalleryID', 'Gallery', Gallery::get()->map('ID', 'Title')) - * ->setEmptyString('(Select one)'); - * $fields->addFieldToTab('Root.Content', $field, 'Content'); - * - * - * As you see, you need to put "GalleryID", rather than "Gallery" here. - * - * Populate with Array - * - * Example model definition: - * - * class MyObject extends DataObject { - * static $db = array( - * 'Country' => "Varchar(100)" - * ); - * } - * - * - * Example instantiation: - * - * DropdownField::create( - * 'Country', - * 'Country', - * array( - * 'NZ' => 'New Zealand', - * 'US' => 'United States', - * 'GEM'=> 'Germany' - * ) - * ); - * - * - * Populate with Enum-Values - * - * You can automatically create a map of possible values from an {@link Enum} database column. - * - * Example model definition: - * - * class MyObject extends DataObject { - * static $db = array( - * 'Country' => "Enum('New Zealand,United States,Germany','New Zealand')" - * ); - * } - * - * - * Field construction: - * - * DropdownField::create( - * 'Country', - * 'Country', - * singleton('MyObject')->dbObject('Country')->enumValues() - * ); - * - * - * Disabling individual items - * - * Individual items can be disabled by feeding their array keys to setDisabledItems. - * - * - * $DrDownField->setDisabledItems( array( 'US', 'GEM' ) ); - * - * - * @see CheckboxSetField for multiple selections through checkboxes instead. - * @see ListboxField for a single box (with single or multiple selections). + * @see TreeDropdownField for a rich and customizeable UI that can visualize a tree of selectable elements */ -abstract class SelectField extends FormField +class DropdownField extends SingleSelectField { - /** - * 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 = []; - - /** - * @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 = [], $value = null) - { - $this->setSource($source); - if (!isset($title)) { - $title = $name; - } - parent::__construct($name, $title, $value); - } - - public function getSchemaStateDefaults() - { - $data = parent::getSchemaStateDefaults(); - $disabled = $this->getDisabledItems(); - - // Add options to 'data' - $source = $this->getSource(); - $data['source'] = (is_array($source)) - ? array_map(function ($value, $title) use ($disabled) { - return [ - 'value' => $value, - 'title' => $title, - 'disabled' => in_array($value, $disabled), - ]; - }, array_keys($source), $source) - : []; - - return $data; - } - - /** - * 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 - * @return $this - */ - 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(), - ['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 + * Build a field option for template rendering * - * @param mixed $source - * @return $this + * @param mixed $value Value of the option + * @param string $title Title of the option + * @return ArrayData Field option */ - public function setSource($source) + protected function getFieldOption($value, $title) { - $this->source = $this->getListMap($source); - return $this; - } + // Check selection + $selected = $this->isSelectedValue($value, $this->Value()); - /** - * 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 Map) { - $source = $source->toArray(); - } - if (!is_array($source) && !($source instanceof ArrayAccess)) { - throw new \InvalidArgumentException('$source passed in as invalid type'); + // Check disabled + $disabled = false; + if ($this->isDisabledValue($value) && $title != $this->getEmptyString()) { + $disabled = 'disabled'; } - return $source; + return new ArrayData([ + 'Title' => (string)$title, + 'Value' => $value, + 'Selected' => $selected, + 'Disabled' => $disabled, + ]); } /** - * 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 + * A required DropdownField must have a user selected attribute, + * so require an empty default for a required field * - * @param mixed $values - * @return array Non-associative list of values - */ - protected function getListValues($values) - { - // Empty values - if (empty($values)) { - return []; - } - - // Direct array - if (is_array($values)) { - return array_values($values ?? []); - } - - // Extract lists - if ($values instanceof SS_List) { - return $values->column('ID'); - } - - return [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; - } - - // Safety check against casting arrays as strings in PHP>5.4 - if (is_array($dataValue) || is_array($userValue)) { - return false; - } - - // 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() - { - /** @var LookupField $field */ - $field = $this->castedCopy('SilverStripe\\Forms\\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 + * @return bool */ - public function castedCopy($classOrCopy) + public function getHasEmptyDefault() { - $field = parent::castedCopy($classOrCopy); - if ($field instanceof SelectField) { - $field->setSource($this->getSource()); - } - return $field; + return parent::getHasEmptyDefault() || $this->Required(); } /** - * Provides '; - $options[] = $item; + // Add all options + foreach ($this->getSourceEmpty() as $value => $title) { + $options[] = $this->getFieldOption($value, $title); } - return implode("\n", $options); - } + $properties = array_merge($properties, [ + 'Options' => new ArrayList($options) + ]); - /** - * @param array $properties - * @return string - */ - public function Field($properties = []) - { - // Without this, changing the source will render the previous list due to cache - $OptionsHTML = new DBHTMLText('Options'); - $OptionsHTML->setValue($this->renderOptionsHTML()); - $properties['OptionsHTML'] = $OptionsHTML; return parent::Field($properties); } } From 328f94553edeadfd006e836775111c40fdf4267c Mon Sep 17 00:00:00 2001 From: Thomas Portelange Date: Mon, 26 Jun 2023 14:46:32 +0200 Subject: [PATCH 7/9] move methods to dropdown field --- src/Forms/DropdownField.php | 40 +++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/Forms/DropdownField.php b/src/Forms/DropdownField.php index 765b8a9df31..90574d3b690 100644 --- a/src/Forms/DropdownField.php +++ b/src/Forms/DropdownField.php @@ -2,8 +2,10 @@ namespace SilverStripe\Forms; +use SilverStripe\Core\Convert; use SilverStripe\ORM\ArrayList; use SilverStripe\View\ArrayData; +use SilverStripe\ORM\FieldType\DBHTMLText; /** * Dropdown field, created from a select tag. @@ -124,6 +126,39 @@ public function getHasEmptyDefault() return parent::getHasEmptyDefault() || $this->Required(); } + /** + * Provides '; + $options[] = $item; + } + + return implode("\n", $options); + } + /** * @param array $properties * @return string @@ -141,6 +176,11 @@ public function Field($properties = []) 'Options' => new ArrayList($options) ]); + // Without this, changing the source will render the previous list due to cache + $OptionsHTML = new DBHTMLText('Options'); + $OptionsHTML->setValue($this->renderOptionsHTML()); + $properties['OptionsHTML'] = $OptionsHTML; + return parent::Field($properties); } } From b4d85320885d19b619050aba7fdaa95d6ae5f823 Mon Sep 17 00:00:00 2001 From: Thomas Portelange Date: Mon, 26 Jun 2023 14:47:19 +0200 Subject: [PATCH 8/9] prevent child classes side effects --- src/Forms/SelectField.php | 49 +++------------------------------------ 1 file changed, 3 insertions(+), 46 deletions(-) diff --git a/src/Forms/SelectField.php b/src/Forms/SelectField.php index 10db5f65820..edc3f4f0401 100644 --- a/src/Forms/SelectField.php +++ b/src/Forms/SelectField.php @@ -2,17 +2,16 @@ namespace SilverStripe\Forms; -use ArrayAccess; -use SilverStripe\ORM\Map; use SilverStripe\ORM\SS_List; -use SilverStripe\Core\Convert; -use SilverStripe\ORM\FieldType\DBHTMLText; +use SilverStripe\ORM\Map; +use ArrayAccess; /** * 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 @@ -272,46 +271,4 @@ public function castedCopy($classOrCopy) } return $field; } - - /** - * Provides '; - $options[] = $item; - } - - return implode("\n", $options); - } - - /** - * @param array $properties - * @return string - */ - public function Field($properties = []) - { - // Without this, changing the source will render the previous list due to cache - $OptionsHTML = new DBHTMLText('Options'); - $OptionsHTML->setValue($this->renderOptionsHTML()); - $properties['OptionsHTML'] = $OptionsHTML; - return parent::Field($properties); - } } From caa42c304f405cb27bb62e473d45351b838a36dd Mon Sep 17 00:00:00 2001 From: Thomas Portelange Date: Mon, 26 Jun 2023 16:55:30 +0200 Subject: [PATCH 9/9] Update src/Forms/DropdownField.php Co-authored-by: Michal Kleiner --- src/Forms/DropdownField.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Forms/DropdownField.php b/src/Forms/DropdownField.php index 90574d3b690..2793bb34fe2 100644 --- a/src/Forms/DropdownField.php +++ b/src/Forms/DropdownField.php @@ -132,7 +132,7 @@ public function getHasEmptyDefault() * * @return string */ - public function renderOptionsHTML(): string + private function renderOptionsHTML(): string { // Some methods only exists for single selects $source = $this->hasMethod('getSourceEmpty') ? $this->getSourceEmpty() : $this->getSource();