Merge pull request #1635 from dereuromark/master-dom-ids

Fix duplicate ID generation of for multiple checkboxes.
This commit is contained in:
Mark Story 2013-12-24 07:02:38 -08:00
commit 1cb7e4f0ff
4 changed files with 240 additions and 8 deletions

View file

@ -3287,8 +3287,8 @@ class FormHelperTest extends CakeTestCase {
$expected = array( $expected = array(
'input' => array('type' => 'hidden', 'name' => 'data[Model][multi_field]', 'value' => '', 'id' => 'ModelMultiField'), 'input' => array('type' => 'hidden', 'name' => 'data[Model][multi_field]', 'value' => '', 'id' => 'ModelMultiField'),
array('div' => array('class' => 'checkbox')), array('div' => array('class' => 'checkbox')),
array('input' => array('type' => 'checkbox', 'name' => 'data[Model][multi_field][]', 'value' => '1/2', 'id' => 'ModelMultiField12')), array('input' => array('type' => 'checkbox', 'name' => 'data[Model][multi_field][]', 'value' => '1/2', 'id' => 'ModelMultiField1/2')),
array('label' => array('for' => 'ModelMultiField12')), array('label' => array('for' => 'ModelMultiField1/2')),
'half', 'half',
'/label', '/label',
'/div', '/div',
@ -3568,8 +3568,8 @@ class FormHelperTest extends CakeTestCase {
$result = $this->Form->radio('Model.field', array('1/2' => 'half')); $result = $this->Form->radio('Model.field', array('1/2' => 'half'));
$expected = array( $expected = array(
'input' => array('type' => 'hidden', 'name' => 'data[Model][field]', 'value' => '', 'id' => 'ModelField_'), 'input' => array('type' => 'hidden', 'name' => 'data[Model][field]', 'value' => '', 'id' => 'ModelField_'),
array('input' => array('type' => 'radio', 'name' => 'data[Model][field]', 'value' => '1/2', 'id' => 'ModelField12')), array('input' => array('type' => 'radio', 'name' => 'data[Model][field]', 'value' => '1/2', 'id' => 'ModelField1/2')),
'label' => array('for' => 'ModelField12'), 'label' => array('for' => 'ModelField1/2'),
'half', 'half',
'/label' '/label'
); );
@ -3696,6 +3696,38 @@ class FormHelperTest extends CakeTestCase {
'/fieldset' '/fieldset'
); );
$this->assertTags($result, $expected); $this->assertTags($result, $expected);
$result = $this->Form->radio(
'Model.field',
array('a>b' => 'first', 'a<b' => 'second', 'a"b' => 'third')
);
$expected = array(
'fieldset' => array(),
'legend' => array(),
'Field',
'/legend',
'input' => array(
'type' => 'hidden', 'name' => 'data[Model][field]',
'id' => 'ModelField_', 'value' => '',
),
array('input' => array('type' => 'radio', 'name' => 'data[Model][field]',
'id' => 'ModelFieldAB', 'value' => 'a&gt;b')),
array('label' => array('for' => 'ModelFieldAB')),
'first',
'/label',
array('input' => array('type' => 'radio', 'name' => 'data[Model][field]',
'id' => 'ModelFieldAB1', 'value' => 'a&lt;b')),
array('label' => array('for' => 'ModelFieldAB1')),
'second',
'/label',
array('input' => array('type' => 'radio', 'name' => 'data[Model][field]',
'id' => 'ModelFieldAB2', 'value' => 'a&quot;b')),
array('label' => array('for' => 'ModelFieldAB2')),
'third',
'/label',
'/fieldset'
);
$this->assertTags($result, $expected);
} }
/** /**
@ -4215,6 +4247,50 @@ class FormHelperTest extends CakeTestCase {
); );
} }
/**
* testDomIdSuffix method
*
* @return void
*/
public function testDomIdSuffix() {
$result = $this->Form->domIdSuffix('1 string with 1$-dollar signs');
$this->assertEquals('1StringWith1$-dollarSigns', $result);
$result = $this->Form->domIdSuffix('<abc x="foo" y=\'bar\'>');
$this->assertEquals('AbcX=FooY=Bar', $result);
$result = $this->Form->domIdSuffix('1 string with 1$-dollar signs', 'xhtml');
$this->assertEquals('1StringWith1-dollarSigns', $result);
$result = $this->Form->domIdSuffix('<abc x="foo" y=\'bar\'>', 'xhtml');
$this->assertEquals('AbcXFooYBar', $result);
}
/**
* testDomIdSuffixCollisionResolvement()
*
* @return void
*/
public function testDomIdSuffixCollisionResolvement() {
$result = $this->Form->domIdSuffix('a>b');
$this->assertEquals('AB', $result);
$result = $this->Form->domIdSuffix('a<b');
$this->assertEquals('AB1', $result);
$result = $this->Form->domIdSuffix('a\'b');
$this->assertEquals('AB2', $result);
$result = $this->Form->domIdSuffix('1 string with 1$-dollar', 'xhtml');
$this->assertEquals('1StringWith1-dollar', $result);
$result = $this->Form->domIdSuffix('1 string with 1€-dollar', 'xhtml');
$this->assertEquals('1StringWith1-dollar1', $result);
$result = $this->Form->domIdSuffix('1 string with 1$-dollar', 'xhtml');
$this->assertEquals('1StringWith1-dollar2', $result);
}
/** /**
* testSelect method * testSelect method
* *
@ -4655,6 +4731,34 @@ class FormHelperTest extends CakeTestCase {
'/select' '/select'
); );
$this->assertTags($result, $expected); $this->assertTags($result, $expected);
$result = $this->Form->select(
'Model.multi_field',
array('a>b' => 'first', 'a<b' => 'second', 'a"b' => 'third'),
array('multiple' => true)
);
$expected = array(
'input' => array(
'type' => 'hidden', 'name' => 'data[Model][multi_field]', 'value' => '',
'id' => 'ModelMultiField_'
),
array('select' => array('name' => 'data[Model][multi_field][]',
'multiple' => 'multiple', 'id' => 'ModelMultiField'
)),
array('option' => array('value' => 'a&gt;b')),
'first',
'/option',
array('option' => array('value' => 'a&lt;b')),
'second',
'/option',
array('option' => array(
'value' => 'a&quot;b'
)),
'third',
'/option',
'/select'
);
$this->assertTags($result, $expected);
} }
/** /**
@ -5063,6 +5167,84 @@ class FormHelperTest extends CakeTestCase {
'/div' '/div'
); );
$this->assertTags($result, $expected); $this->assertTags($result, $expected);
$result = $this->Form->select(
'Model.multi_field',
array('a+' => 'first', 'a++' => 'second', 'a+++' => 'third'),
array('multiple' => 'checkbox')
);
$expected = array(
'input' => array(
'type' => 'hidden', 'name' => 'data[Model][multi_field]', 'value' => '', 'id' => 'ModelMultiField'
),
array('div' => array('class' => 'checkbox')),
array('input' => array(
'type' => 'checkbox', 'name' => 'data[Model][multi_field][]',
'value' => 'a+', 'id' => 'ModelMultiFieldA+'
)),
array('label' => array('for' => 'ModelMultiFieldA+')),
'first',
'/label',
'/div',
array('div' => array('class' => 'checkbox')),
array('input' => array(
'type' => 'checkbox', 'name' => 'data[Model][multi_field][]',
'value' => 'a++', 'id' => 'ModelMultiFieldA++'
)),
array('label' => array('for' => 'ModelMultiFieldA++')),
'second',
'/label',
'/div',
array('div' => array('class' => 'checkbox')),
array('input' => array(
'type' => 'checkbox', 'name' => 'data[Model][multi_field][]',
'value' => 'a+++', 'id' => 'ModelMultiFieldA+++'
)),
array('label' => array('for' => 'ModelMultiFieldA+++')),
'third',
'/label',
'/div'
);
$this->assertTags($result, $expected);
$result = $this->Form->select(
'Model.multi_field',
array('a>b' => 'first', 'a<b' => 'second', 'a"b' => 'third'),
array('multiple' => 'checkbox')
);
$expected = array(
'input' => array(
'type' => 'hidden', 'name' => 'data[Model][multi_field]', 'value' => '', 'id' => 'ModelMultiField'
),
array('div' => array('class' => 'checkbox')),
array('input' => array(
'type' => 'checkbox', 'name' => 'data[Model][multi_field][]',
'value' => 'a&gt;b', 'id' => 'ModelMultiFieldAB2'
)),
array('label' => array('for' => 'ModelMultiFieldAB2')),
'first',
'/label',
'/div',
array('div' => array('class' => 'checkbox')),
array('input' => array(
'type' => 'checkbox', 'name' => 'data[Model][multi_field][]',
'value' => 'a&lt;b', 'id' => 'ModelMultiFieldAB1'
)),
array('label' => array('for' => 'ModelMultiFieldAB1')),
'second',
'/label',
'/div',
array('div' => array('class' => 'checkbox')),
array('input' => array(
'type' => 'checkbox', 'name' => 'data[Model][multi_field][]',
'value' => 'a&quot;b', 'id' => 'ModelMultiFieldAB'
)),
array('label' => array('for' => 'ModelMultiFieldAB')),
'third',
'/label',
'/div'
);
$this->assertTags($result, $expected);
} }
/** /**

View file

@ -851,6 +851,16 @@ class HelperTest extends CakeTestCase {
$this->assertEquals('&amp;lt;script&amp;gt;alert(document.cookie)&amp;lt;/script&amp;gt;', $result); $this->assertEquals('&amp;lt;script&amp;gt;alert(document.cookie)&amp;lt;/script&amp;gt;', $result);
} }
/**
* testDomId method
*
* @return void
*/
public function testDomId() {
$result = $this->Helper->domId('Foo.bar');
$this->assertEquals('FooBar', $result);
}
/** /**
* testMultiDimensionalField method * testMultiDimensionalField method
* *

View file

@ -16,6 +16,7 @@
App::uses('Router', 'Routing'); App::uses('Router', 'Routing');
App::uses('Hash', 'Utility'); App::uses('Hash', 'Utility');
App::uses('Inflector', 'Utility');
/** /**
* Abstract base class for all other Helpers in CakePHP. * Abstract base class for all other Helpers in CakePHP.

View file

@ -17,6 +17,7 @@
App::uses('ClassRegistry', 'Utility'); App::uses('ClassRegistry', 'Utility');
App::uses('AppHelper', 'View/Helper'); App::uses('AppHelper', 'View/Helper');
App::uses('Hash', 'Utility'); App::uses('Hash', 'Utility');
App::uses('Inflector', 'Utility');
/** /**
* Form helper library. * Form helper library.
@ -109,6 +110,13 @@ class FormHelper extends AppHelper {
*/ */
public $validationErrors = array(); public $validationErrors = array();
/**
* Holds already used DOM ID suffixes to avoid collisions with multiple form field elements.
*
* @var array
*/
protected $_domIdSuffixes = array();
/** /**
* Copies the validationErrors variable from the View object into this instance * Copies the validationErrors variable from the View object into this instance
* *
@ -1506,6 +1514,7 @@ class FormHelper extends AppHelper {
$value = $value ? 1 : 0; $value = $value ? 1 : 0;
} }
$this->_domIdSuffixes = array();
foreach ($options as $optValue => $optTitle) { foreach ($options as $optValue => $optTitle) {
$optionsHere = array('value' => $optValue, 'disabled' => false); $optionsHere = array('value' => $optValue, 'disabled' => false);
@ -1516,9 +1525,7 @@ class FormHelper extends AppHelper {
if ($disabled && (!is_array($disabled) || in_array((string)$optValue, $disabled, !$isNumeric))) { if ($disabled && (!is_array($disabled) || in_array((string)$optValue, $disabled, !$isNumeric))) {
$optionsHere['disabled'] = true; $optionsHere['disabled'] = true;
} }
$tagName = Inflector::camelize( $tagName = $attributes['id'] . $this->domIdSuffix($optValue);
$attributes['id'] . '_' . Inflector::slug($optValue)
);
if ($label) { if ($label) {
$labelOpts = is_array($label) ? $label : array(); $labelOpts = is_array($label) ? $label : array();
@ -2065,6 +2072,34 @@ class FormHelper extends AppHelper {
return implode("\n", $select); return implode("\n", $select);
} }
/**
* Generates a valid DOM ID suffix from a string.
* Also avoids collisions when multiple values are coverted to the same suffix by
* appending a numeric value.
*
* For pre-HTML5 IDs only characters like a-z 0-9 - _ are valid. HTML5 doesn't have that
* limitation, but to avoid layout issues it still filters out some sensitive chars.
*
* @param string $value The value that should be transferred into a DOM ID suffix.
* @param string $type Doctype to use. Defaults to html5. Anything else will use limited chars.
* @return string DOM ID
*/
public function domIdSuffix($value, $type = 'html5') {
if ($type === 'html5') {
$value = str_replace(array('<', '>', ' ', '"', '\''), '_', $value);
} else {
$value = preg_replace('~[^\\pL\d-_]+~u', '_', $value);
}
$value = Inflector::camelize($value);
$count = 1;
$suffix = $value;
while (in_array($suffix, $this->_domIdSuffixes)) {
$suffix = $value . $count++;
}
$this->_domIdSuffixes[] = $suffix;
return $suffix;
}
/** /**
* Returns a SELECT element for days. * Returns a SELECT element for days.
* *
@ -2605,6 +2640,7 @@ class FormHelper extends AppHelper {
$selectedIsEmpty = ($attributes['value'] === '' || $attributes['value'] === null); $selectedIsEmpty = ($attributes['value'] === '' || $attributes['value'] === null);
$selectedIsArray = is_array($attributes['value']); $selectedIsArray = is_array($attributes['value']);
$this->_domIdSuffixes = array();
foreach ($elements as $name => $title) { foreach ($elements as $name => $title) {
$htmlOptions = array(); $htmlOptions = array();
if (is_array($title) && (!isset($title['name']) || !isset($title['value']))) { if (is_array($title) && (!isset($title['name']) || !isset($title['value']))) {
@ -2673,7 +2709,7 @@ class FormHelper extends AppHelper {
if ($attributes['style'] === 'checkbox') { if ($attributes['style'] === 'checkbox') {
$htmlOptions['value'] = $name; $htmlOptions['value'] = $name;
$tagName = $attributes['id'] . Inflector::camelize(Inflector::slug($name)); $tagName = $attributes['id'] . $this->domIdSuffix($name);
$htmlOptions['id'] = $tagName; $htmlOptions['id'] = $tagName;
$label = array('for' => $tagName); $label = array('for' => $tagName);
@ -2692,6 +2728,9 @@ class FormHelper extends AppHelper {
$item = $this->Html->useTag('checkboxmultiple', $name, $htmlOptions); $item = $this->Html->useTag('checkboxmultiple', $name, $htmlOptions);
$select[] = $this->Html->div($attributes['class'], $item . $label); $select[] = $this->Html->div($attributes['class'], $item . $label);
} else { } else {
if ($attributes['escape']) {
$name = h($name);
}
$select[] = $this->Html->useTag('selectoption', $name, $htmlOptions, $title); $select[] = $this->Html->useTag('selectoption', $name, $htmlOptions, $title);
} }
} }