diff --git a/src/Html/FormBuilder.php b/src/Html/FormBuilder.php index e4864da9..b0406cb4 100644 --- a/src/Html/FormBuilder.php +++ b/src/Html/FormBuilder.php @@ -407,6 +407,15 @@ protected function setQuickTextAreaSize(array $options): array /** * Create a select box field with empty option support. + * + * Supports several formats for the $list parameter: + * - Simple format: ['value' => 'Label'] + * - With icon/image: ['value' => ['Label', 'icon-name']] or ['value' => ['Label', 'image.png']] + * - With optgroups: ['Group Name' => ['value' => 'Label', ...]] + * - Mixed format combining all of the above + * + * Icons are detected when the second array element doesn't contain a dot (.). + * Images are detected when the second array element contains a dot (.). */ public function select(string $name, array $list = [], string|array|null $selected = null, array $options = []): string { @@ -482,10 +491,15 @@ public function selectMonth(string $name, string|array|null $selected = null, ar /** * Get the select option for the given value. + * + * Determines whether to create a single option or an optgroup based on the $display parameter: + * - If $display is an array with string keys, creates an optgroup + * - If $display is an array with numeric keys (e.g., ['Label', 'icon']), creates a single option with icon/image + * - If $display is a string, creates a simple option */ public function getSelectOption(string|array $display, string $value, string|array|null $selected = null): string { - if (is_array($display)) { + if (is_array($display) && array_keys($display) !== [0,1]) { return $this->optionGroup($display, $value, $selected); } @@ -508,16 +522,33 @@ protected function optionGroup(array $list, string $label, string|array|null $se /** * Create a select element option. + * + * If $display is an array in the format ['Label', 'icon-or-image'], adds data attributes: + * - data-icon: added if the second element doesn't contain a dot (e.g., 'icon-refresh') + * - data-image: added if the second element contains a dot (e.g., 'image.png') */ - protected function option(string $display, string $value, string|array|null $selected = null): string + protected function option(string|array $display, string $value, string|array|null $selected = null): string { $selectedAttr = $this->getSelectedValue($value, $selected); $options = [ - 'value' => e($value), + 'value' => $value, 'selected' => $selectedAttr ]; + if (is_array($display)) { + $label = array_get($display, 0); + $data = array_get($display, 1); + + if (is_string($data) && $data !== '') { + if (strpos($data, '.') !== false) { + $options['data-image'] = $data; + } else { + $options['data-icon'] = $data; + } + } + $display = $label; + } return 'html->attributes($options) . '>' . e($display) . ''; } diff --git a/tests/Html/FormBuilderTest.php b/tests/Html/FormBuilderTest.php index d5bcd740..dc601fa1 100644 --- a/tests/Html/FormBuilderTest.php +++ b/tests/Html/FormBuilderTest.php @@ -340,4 +340,255 @@ public function testSelectWithEmptyOption() $this->assertStringContainsString('', $result); $this->assertStringContainsString('', $result); } + + /** + * @testdox can create a select element with icon data attributes. + */ + public function testSelectWithIcon() + { + $result = $this->formBuilder->select( + name: 'my-select', + list: [ + '1' => 'Regular Option', + '2' => ['Option With Icon', 'icon-refresh'], + ], + selected: null, + options: [] + ); + + $this->assertElementIs('select', $result); + $this->assertElementAttributeEquals('name', 'my-select', $result); + $this->assertStringContainsString('', $result); + $this->assertStringContainsString('', $result); + } + + /** + * @testdox can create a select element with image data attributes. + */ + public function testSelectWithImage() + { + $result = $this->formBuilder->select( + name: 'my-select', + list: [ + '1' => 'Regular Option', + '2' => ['Option With Image', 'myImage.jpeg'], + ], + selected: null, + options: [] + ); + + $this->assertElementIs('select', $result); + $this->assertElementAttributeEquals('name', 'my-select', $result); + $this->assertStringContainsString('', $result); + $this->assertStringContainsString('', $result); + } + + /** + * @testdox can create a select element with image data attributes. + */ + public function testSelectWithSelectedImage() + { + $result = $this->formBuilder->select( + name: 'my-select', + list: [ + '1' => 'Regular Option', + '2' => ['Option With Image', 'myImage.jpeg'], + ], + selected: '2', + options: [] + ); + + $this->assertElementIs('select', $result); + $this->assertElementAttributeEquals('name', 'my-select', $result); + $this->assertStringContainsString('', $result); + $this->assertStringContainsString('', $result); + } + + /** + * @testdox can create a select element with optgroups. + */ + public function testSelectWithOptgroups() + { + $result = $this->formBuilder->select( + name: 'my-select', + list: [ + 'Group 1' => [ + 'g1-opt1' => 'Group 1 Option 1', + 'g1-opt2' => 'Group 1 Option 2', + ], + 'Group 2' => [ + 'g2-opt1' => 'Group 2 Option 1', + 'g2-opt2' => 'Group 2 Option 2', + ], + ], + selected: null, + options: [] + ); + + $this->assertElementIs('select', $result); + $this->assertElementAttributeEquals('name', 'my-select', $result); + $this->assertStringContainsString('', $result); + $this->assertStringContainsString('', $result); + $this->assertStringContainsString('', $result); + $this->assertStringContainsString('', $result); + $this->assertStringContainsString('', $result); + $this->assertStringContainsString('', $result); + $this->assertStringContainsString('', $result); + } + + /** + * @testdox can create a select element with optgroups containing icons and images. + */ + public function testSelectWithOptgroupsAndIconsImages() + { + $result = $this->formBuilder->select( + name: 'my-select', + list: [ + 'option1' => 'Regular option', + 'option2' => ['Option With Image', 'myImage.jpeg'], + 'Group1' => [ + 'group1-opt1' => 'OptGroup Option1 regular option', + 'group1-opt2' => ['OptGroup Option2 with icon', 'icon-refresh'], + 'group1-opt3' => ['OptGroup Option3 with image', 'otherImage.png'], + ], + 'Group2' => [ + 'group2-opt1' => 'OptGroup2 Option1', + 'group2-opt2' => 'OptGroup2 Option2', + ], + ], + selected: null, + options: [] + ); + + $this->assertElementIs('select', $result); + $this->assertElementAttributeEquals('name', 'my-select', $result); + + // Regular options + $this->assertStringContainsString('', $result); + $this->assertStringContainsString('', $result); + + // Optgroups + $this->assertStringContainsString('', $result); + $this->assertStringContainsString('', $result); + + // Options inside optgroups + $this->assertStringContainsString('', $result); + $this->assertStringContainsString('', $result); + $this->assertStringContainsString('', $result); + $this->assertStringContainsString('', $result); + $this->assertStringContainsString('', $result); + } + + /** + * @testdox can create a select element with backward compatibility for simple string options. + */ + public function testSelectBackwardCompatibility() + { + $result = $this->formBuilder->select( + name: 'my-select', + list: [ + '1' => 'Option 1', + '2' => 'Option 2', + '3' => 'Option 3', + ], + selected: '2', + options: [] + ); + + $this->assertElementIs('select', $result); + $this->assertElementAttributeEquals('name', 'my-select', $result); + $this->assertStringContainsString('', $result); + $this->assertStringContainsString('', $result); + $this->assertStringContainsString('', $result); + $this->assertStringNotContainsString('data-icon', $result); + $this->assertStringNotContainsString('data-image', $result); + } + + /** + * @testdox can create a select element with backward compatibility for optgroup integer keys + */ + public function testSelectBackwardCompatibilityOptgroupIdItemsKeys() + { + // this simulates grouped options base on a model with ids as keys + $result = $this->formBuilder->select( + name: 'my-select', + list: [ + 'Group1' => [ + 1 => 'Option 1', + 2 => 'Option 2', + ], + 'Group2' => [ + 3 => 'Option 3', + 4 => 'Option 4', + ], + ], + selected: 2, + options: [] + ); + + $this->assertElementIs('select', $result); + $this->assertElementAttributeEquals('name', 'my-select', $result); + + // Optgroups + $this->assertStringContainsString('', $result); + $this->assertStringContainsString('', $result); + + // Options inside optgroups + $this->assertStringContainsString('', $result); + $this->assertStringContainsString('', $result); + $this->assertStringContainsString('', $result); + $this->assertStringContainsString('', $result); + $this->assertStringNotContainsString('data-icon', $result); + $this->assertStringNotContainsString('data-image', $result); + } + + /** + * @testdox show case where backward compatibility is broken (expected) + */ + public function testSelectBackwardCompatibilityBrokenOptGroup() + { + // optgroup syntax with two items with integer keys starting at zero are seen as a regular option with an icon + $result = $this->formBuilder->select( + name: 'my-select', + list: [ + 'Group1' => [ + 0 => 'Option 1', + 1 => 'Option 2', + ], + ], + options: [] + ); + + $this->assertElementIs('select', $result); + $this->assertElementAttributeEquals('name', 'my-select', $result); + + // Options inside optgroups + $this->assertStringContainsString('', $result); + $this->assertStringContainsString('data-icon', $result); + } + + /** + * @testdox properly escapes HTML in option labels and values. + */ + public function testSelectHtmlEscaping() + { + $result = $this->formBuilder->select( + name: 'my-select', + list: [ + '