Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for optgroup-tags for select-fields #1778

Merged
merged 10 commits into from
Dec 22, 2016
75 changes: 75 additions & 0 deletions _test/tests/inc/form/dropdownelement.test.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,81 @@ function test_extended_options() {
$this->assertTrue($option->hasClass('classes'));
}

public function test_optgroups() {
$form = new Form\Form();

$options1 = array(
'first' => 'the label',
'second'
);

$options2 = array(
'third' => array (
'label' => 'label of third option',
'attribute' => 'attribute-value'
),
'fourth'
);

$dropdown = $form->addDropdown('foo', null, 'label text');
$dropdown->addOptGroup('opt1', $options1);
$dropdown->addOptGroup('opt2', $options2);

$dropdown->val('third');
$this->assertEquals('third', $dropdown->val());

/** @var Form\OptGroup[] $optGroups */
$optGroups = $dropdown->optGroups();
$this->assertEquals(array(
'first' => array('label' => 'the label'),
'second' => array('label' => 'second')
), $optGroups['opt1']->options());

// HTML
$html = $form->toHTML();
$pq = phpQuery::newDocumentXHTML($html);

$optGroupsHTML = $pq->find('optgroup');
$this->assertEquals(2, $optGroupsHTML->length);

$options = $pq->find('option');
$this->assertEquals(4, $options->length);

$selected = $pq->find('option[selected=selected]');
$this->assertEquals('third', $selected->val());
$this->assertEquals('label of third option', $selected->text());
}

/**
* Ensure that there is always only a single one selected option
*/
public function test_optgroups_doubleselect() {
$form = new Form\Form();
$options1 = array(
'double' => 'the label'
);

$options2 = array(
'double' => array (
'label' => 'label of third option',
'attribute' => 'attribute-value'
)
);

$dropdown = $form->addDropdown('foo', null, 'label text');
$dropdown->addOptGroup('opt1', $options1);
$dropdown->addOptGroup('opt2', $options2);
$dropdown->val('double');

// HTML
$html = $form->toHTML();
$pq = phpQuery::newDocumentXHTML($html);
$selected = $pq->find('option[selected=selected]');
$this->assertEquals(1, $selected->length);
$this->assertEquals('the label', $selected->text());
}


/**
* check that posted values overwrite preset default
*/
Expand Down
122 changes: 91 additions & 31 deletions inc/Form/DropdownElement.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,59 @@
*/
class DropdownElement extends InputElement {

protected $options = array();

protected $value = '';
/** @var array OptGroup[] */
protected $optGroups = array();

/**
* @param string $name The name of this form element
* @param string $options The available options
* @param array $options The available options
* @param string $label The label text for this element (will be autoescaped)
*/
public function __construct($name, $options, $label = '') {
parent::__construct('dropdown', $name, $label);
$this->options($options);
$this->rmattr('type');
$this->optGroups[''] = new OptGroup(null, $options);
$this->val('');
}

/**
* Add an `<optgroup>` and respective options
*
* @param string $label
* @param array $options
* @return OptGroup a reference to the added optgroup
* @throws \Exception
*/
public function addOptGroup($label, $options) {
if (empty($label)) {
throw new \InvalidArgumentException(hsc('<optgroup> must have a label!'));
}
$this->optGroups[$label] = new OptGroup($label, $options);
return end($this->optGroups);
}

/**
* Set or get the optgroups of an Dropdown-Element.
*
* optgroups have to be given as associative array
* * the key being the label of the group
* * the value being an array of options as defined in @see OptGroup::options()
*
* @param null|array $optGroups
* @return OptGroup[]|DropdownElement
*/
public function optGroups($optGroups = null) {
if($optGroups === null) {
return $this->optGroups;
}
if (!is_array($optGroups)) {
throw new \InvalidArgumentException(hsc('Argument must be an associative array of label => [options]!'));
}
$this->optGroups = array();
foreach ($optGroups as $label => $options) {
$this->addOptGroup($label, $options);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should it return $this here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2bd1d2c

return $this;
}

/**
Expand All @@ -41,21 +82,10 @@ public function __construct($name, $options, $label = '') {
* @return $this|array
*/
public function options($options = null) {
if($options === null) return $this->options;
if(!is_array($options)) throw new \InvalidArgumentException('Options have to be an array');
$this->options = array();

foreach($options as $key => $val) {
if(is_int($key)) {
$this->options[$val] = array('label' => (string) $val);
} elseif (!is_array($val)) {
$this->options[$key] = array('label' => (string) $val);
} else {
if (!key_exists('label', $val)) throw new \InvalidArgumentException('If option is given as array, it has to have a "label"-key!');
$this->options[$key] = $val;
}
if ($options === null) {
return $this->optGroups['']->options();
}
$this->val(''); // set default value (empty or first)
$this->optGroups[''] = new OptGroup(null, $options);
return $this;
}

Expand Down Expand Up @@ -91,17 +121,55 @@ public function attr($name, $value = null) {
public function val($value = null) {
if($value === null) return $this->value;

if(isset($this->options[$value])) {
$value_exists = $this->setValueInOptGroups($value);

if($value_exists) {
$this->value = $value;
} else {
// unknown value set, select first option instead
$keys = array_keys($this->options);
$this->value = (string) array_shift($keys);
$this->value = $this->getFirstOption();
$this->setValueInOptGroups($this->value);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how are we handling multi selects? it seems this only works with a single value?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are not, we are throwing an exception when setting the multiple-attribute. I removed my erroneous handling of multi selects in 45082b9.
We should consider a new PR for adding multi-select-functionality.

}

return $this;
}

/**
* Returns the first options as it will be rendered in HTML
*
* @return string
*/
protected function getFirstOption() {
$options = $this->options();
if (!empty($options)) {
return (string) array_shift(array_keys($options));
}
foreach ($this->optGroups as $optGroup) {
$options = $optGroup->options();
if (!empty($options)) {
return (string) array_shift(array_keys($options));
}
}
}

/**
* Set the value in the OptGroups, including the optgroup for the options without optgroup.
*
* @param string $value
* @return bool
*/
protected function setValueInOptGroups($value) {
$value_exists = false;
/** @var OptGroup $optGroup */
foreach ($this->optGroups as $optGroup) {
$value_exists = $optGroup->storeValue($value) || $value_exists;
if ($value_exists) {
$value = null;
}
}
return $value_exists;
}

/**
* Create the HTML for the select it self
*
Expand All @@ -111,15 +179,7 @@ protected function mainElementHTML() {
if($this->useInput) $this->prefillInput();

$html = '<select ' . buildAttributes($this->attrs()) . '>';
foreach($this->options as $key => $val) {
$selected = ($key == $this->value) ? ' selected="selected"' : '';
$attrs = '';
if (is_array($val['attrs'])) {
array_walk($val['attrs'],function (&$aval, $akey){$aval = hsc($akey).'="'.hsc($aval).'"';});
$attrs = join(' ', $val['attrs']);
}
$html .= '<option' . $selected . ' value="' . hsc($key) . '" '.$attrs.'>' . hsc($val['label']) . '</option>';
}
$html = array_reduce($this->optGroups, function($html, OptGroup $optGroup) {return $html . $optGroup->toHTML();}, $html);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's a bit weird that not all options are handled through the optgroups in $this->optGroups but that there are two places. I would expect the "unlabeled" optgroup to be in that array as well.

$html .= '</select>';

return $html;
Expand Down
97 changes: 97 additions & 0 deletions inc/Form/OptGroup.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php

namespace dokuwiki\Form;


class OptGroup extends Element {
protected $options = array();
protected $value;

/**
* @param string $label The label text for this element (will be autoescaped)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wrong doc block

* @param array $options The available options
*/
public function __construct($label, $options) {
parent::__construct('optGroup', array('label' => $label));
$this->options($options);
}

/**
* Store the given value so it can be used during rendering
*
* This is intended to be only called from within @see DropdownElement::val()
*
* @param string $value
* @return bool true if an option with the given value exists, false otherwise
*/
public function storeValue($value) {
$this->value = $value;
return isset($this->options[$value]);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bit unclear what this function does. It seems to store the currently selected value? What if this optgroup is part of a multiselect? What exactly is the return value?


/**
* Get or set the options of the optgroup
*
* Options can be given as associative array (value => label) or as an
* indexd array (label = value) or as an array of arrays. In the latter
* case an element has to look as follows:
* option-value => array (
* 'label' => option-label,
* 'attrs' => array (
* attr-key => attr-value, ...
* )
* )
*
* @param null|array $options
* @return $this|array
*/
public function options($options = null) {
if($options === null) return $this->options;
if(!is_array($options)) throw new \InvalidArgumentException('Options have to be an array');
$this->options = array();
foreach($options as $key => $val) {
if(is_int($key)) {
$this->options[$val] = array('label' => (string) $val);
} elseif (!is_array($val)) {
$this->options[$key] = array('label' => (string) $val);
} else {
if (!key_exists('label', $val)) throw new \InvalidArgumentException('If option is given as array, it has to have a "label"-key!');
$this->options[$key] = $val;
}
}
return $this;
}


/**
* The HTML representation of this element
*
* @return string
*/
public function toHTML() {
if ($this->attributes['label'] === null) {
return $this->renderOptions();
}
$html = '<optgroup '. buildAttributes($this->attrs()) . '>';
$html .= $this->renderOptions();
$html .= '</optgroup>';
return $html;
}


/**
* @return string
*/
protected function renderOptions() {
$html = '';
foreach($this->options as $key => $val) {
$selected = ($key == $this->value) ? ' selected="selected"' : '';
$attrs = '';
if (!empty($val['attrs']) && is_array($val['attrs'])) {
$attrs = buildAttributes($val['attrs']);
}
$html .= '<option' . $selected . ' value="' . hsc($key) . '" '.$attrs.'>' . hsc($val['label']) . '</option>';
}
return $html;
}
}