Permalink
Browse files

Collision free approach to resolve the DOM ID issue in a clean way. F…

…ix to generation of ids for multiple checkboxes. Resolves ticket 4064.
  • Loading branch information...
dereuromark committed Dec 4, 2013
1 parent a57c46f commit aae0f762dd19c5bc2d171d7c8b06d63bebebe381
@@ -3222,8 +3222,8 @@ public function testSelectAsCheckbox() {
$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' => '1/2', 'id' => 'ModelMultiField12')),
- array('label' => array('for' => 'ModelMultiField12')),
+ array('input' => array('type' => 'checkbox', 'name' => 'data[Model][multi_field][]', 'value' => '1/2', 'id' => 'ModelMultiField1/2')),
+ array('label' => array('for' => 'ModelMultiField1/2')),
'half',
'/label',
'/div',
@@ -4150,6 +4150,50 @@ public function testRadioWithCreate() {
);
}
+/**
+ * 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
*
@@ -4998,6 +5042,84 @@ public function testSelectMultipleCheckboxes() {
'/div'
);
$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);
}
/**
@@ -851,6 +851,16 @@ public function testClean() {
$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
*
View
@@ -16,6 +16,7 @@
App::uses('Router', 'Routing');
App::uses('Hash', 'Utility');
+App::uses('Inflector', 'Utility');
/**
* Abstract base class for all other Helpers in CakePHP.
@@ -17,6 +17,7 @@
App::uses('ClassRegistry', 'Utility');
App::uses('AppHelper', 'View/Helper');
App::uses('Hash', 'Utility');
+App::uses('Inflector', 'Utility');
/**
* Form helper library.
@@ -109,6 +110,13 @@ class FormHelper extends AppHelper {
*/
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
*
@@ -2065,6 +2073,34 @@ public function select($fieldName, $options = array(), $attributes = array()) {
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.
*
@@ -2609,6 +2645,7 @@ protected function _selectOptions($elements = array(), $parents = array(), $show
$selectedIsEmpty = ($attributes['value'] === '' || $attributes['value'] === null);
$selectedIsArray = is_array($attributes['value']);
+ $this->_domIdSuffixes = array();
foreach ($elements as $name => $title) {
$htmlOptions = array();
if (is_array($title) && (!isset($title['name']) || !isset($title['value']))) {
@@ -2677,7 +2714,7 @@ protected function _selectOptions($elements = array(), $parents = array(), $show
if ($attributes['style'] === 'checkbox') {
$htmlOptions['value'] = $name;
- $tagName = $attributes['id'] . Inflector::camelize(Inflector::slug($name));
+ $tagName = $attributes['id'] . $this->domIdSuffix($name);
$htmlOptions['id'] = $tagName;
$label = array('for' => $tagName);

0 comments on commit aae0f76

Please sign in to comment.