diff --git a/core/lib/Drupal/Core/Template/Attribute.php b/core/lib/Drupal/Core/Template/Attribute.php index 48e2fa1..846174d 100644 --- a/core/lib/Drupal/Core/Template/Attribute.php +++ b/core/lib/Drupal/Core/Template/Attribute.php @@ -80,8 +80,18 @@ public function offsetSet($name, $value) { * An AttributeValueBase representation of the attribute's value. */ protected function createAttributeValue($name, $value) { - if (is_array($value)) { - $value = new AttributeArray($name, $value); + // If the value is already an AttributeValueBase object, return it + // straight away. + if ($value instanceOf AttributeValueBase) { + return $value; + } + // An array value or 'class' attribute name are forced to always be an + // AttributeArray value for consistency. + if (is_array($value) || $name == 'class') { + // Cast the value to an array if the value was passed in as a string. + // @todo Decide to fix all the broken instances of class as a string + // in core or cast them. + $value = new AttributeArray($name, (array) $value); } elseif (is_bool($value)) { $value = new AttributeBoolean($name, $value); @@ -107,6 +117,68 @@ public function offsetExists($name) { } /** + * Adds argument values by merging them on to array of existing CSS classes. + * + * @param string|array ... + * CSS classes to add to the class attribute array. + * + * @return $this + */ + public function addClass() { + $args = func_get_args(); + $classes = array(); + foreach ($args as $arg) { + // Merge the values passed in from the classes array. + // The argument is cast to an array to support comma separated single + // values or one or more array arguments. + $classes = array_merge($classes, (array) $arg); + } + + // Merge if there are values, just add them otherwise. + if (isset($this->storage['class']) && $this->storage['class'] instanceOf AttributeArray) { + // Merge the values passed in from the class value array. + $classes = array_merge($this->storage['class']->value(), $classes); + // Filter out any duplicate values. + $classes = array_unique($classes); + $this->storage['class']->exchangeArray($classes); + } + else { + // Filter out any duplicate values. + $classes = array_unique($classes); + $this->offsetSet('class', $classes); + } + + return $this; + } + + /** + * Removes argument values from array of existing CSS classes. + * + * @param string|array ... + * CSS classes to remove from the class attribute array. + * + * @return $this + */ + public function removeClass() { + // With no class attribute, there is no need to remove. + if (isset($this->storage['class']) && $this->storage['class'] instanceOf AttributeArray) { + $args = func_get_args(); + $classes = array(); + foreach ($args as $arg) { + // Merge the values passed in from the classes array. + // The argument is cast to an array to support comma separated single + // values or one or more array arguments. + $classes = array_merge($classes, (array) $arg); + } + + // Remove the values passed in from the value array. + $classes = array_diff($this->storage['class']->value(), $classes); + $this->storage['class']->exchangeArray($classes); + } + return $this; + } + + /** * Implements the magic __toString() method. */ public function __toString() { diff --git a/core/lib/Drupal/Core/Template/AttributeArray.php b/core/lib/Drupal/Core/Template/AttributeArray.php index 95e0ef3..c432ba0 100644 --- a/core/lib/Drupal/Core/Template/AttributeArray.php +++ b/core/lib/Drupal/Core/Template/AttributeArray.php @@ -83,4 +83,21 @@ public function value() { return $this->value; } + /** + * Exchange the array for another one. + * + * @see ArrayObject::exchangeArray + * + * @param array $input + * The array input to replace the internal value. + * + * @return array + * The old array value. + */ + public function exchangeArray($input) { + $old = $this->value; + $this->value = $input; + return $old; + } + } diff --git a/core/tests/Drupal/Tests/Core/Template/AttributeTest.php b/core/tests/Drupal/Tests/Core/Template/AttributeTest.php index 4181f33..bcd04a2 100644 --- a/core/tests/Drupal/Tests/Core/Template/AttributeTest.php +++ b/core/tests/Drupal/Tests/Core/Template/AttributeTest.php @@ -58,6 +58,130 @@ public function testRemove() { } /** + * Tests adding class attributes with the AttributeArray helper method. + * @covers ::addClass() + */ + public function testAddClasses() { + // Add empty Attribute object with no classes. + $attribute = new Attribute(); + + // Add one class on empty attribute. + $attribute->addClass('banana'); + $this->assertArrayEquals(array('banana'), $attribute['class']->value()); + + // Add one class. + $attribute->addClass('aa'); + $this->assertArrayEquals(array('banana', 'aa'), $attribute['class']->value()); + + // Add multiple classes. + $attribute->addClass('xx', 'yy'); + $this->assertArrayEquals(array('banana', 'aa', 'xx', 'yy'), $attribute['class']->value()); + + // Add an array of classes. + $attribute->addClass(array('red', 'green', 'blue')); + $this->assertArrayEquals(array('banana', 'aa', 'xx', 'yy', 'red', 'green', 'blue'), $attribute['class']->value()); + + // Add an array of duplicate classes. + $attribute->addClass(array('red', 'green', 'blue'), array('aa', 'aa', 'banana'), 'yy'); + $this->assertArrayEquals(array('banana', 'aa', 'xx', 'yy', 'red', 'green', 'blue'), $attribute['class']->value()); + } + + /** + * Tests removing class attributes with the AttributeArray helper method. + * @covers ::removeClass() + */ + public function testRemoveClasses() { + $classes = array('example-class', 'aa', 'xx', 'yy', 'red', 'green', 'blue'); + $attribute = new Attribute(array('class' => $classes)); + + // Remove one class. + $attribute->removeClass('example-class'); + $this->assertNotContains('example-class', $attribute['class']->value()); + + // Remove multiple classes. + $attribute->removeClass('xx', 'yy'); + $this->assertNotContains(array('xx', 'yy'), $attribute['class']->value()); + + // Remove an array of classes. + $attribute->removeClass(array('red', 'green', 'blue')); + $this->assertNotContains(array('red', 'green', 'blue'), $attribute['class']->value()); + } + + /** + * Tests removing class attributes with the Attribute helper methods. + * @covers ::removeClass() + * @covers ::addClass() + */ + public function testChainAddRemoveClasses() { + $attribute = new Attribute( + array('class' => array('example-class', 'red', 'green', 'blue')) + ); + + $attribute + ->removeClass(array('red', 'green', 'pink')) + ->addClass(array('apple', 'lime', 'grapefruit')) + ->addClass(array('banana')); + $expected = array('example-class', 'blue', 'apple', 'lime', 'grapefruit', 'banana'); + $this->assertArrayEquals($expected, $attribute['class']->value(), 'Attributes chained'); + } + + /** + * Tests the twig calls to the Attribute. + * @dataProvider providerTestAttributeClassHelpers + * + * @covers ::removeClass() + * @covers ::addClass() + */ + public function testTwigAddRemoveClasses($template, $expected, $seed_attributes = array()) { + $loader = new \Twig_Loader_String(); + $twig = new \Twig_Environment($loader); + $data = array('attributes' => new Attribute($seed_attributes)); + $result = $twig->render($template, $data); + $this->assertEquals($expected, $result); + } + + /** + * Provides tests data for testEscaping + * + * @return array + * An array of test data each containing of a twig template string, + * a resulting string of classes and an optional array of attributes. + */ + public function providerTestAttributeClassHelpers() { + return array( + array("{{ attributes.class }}", ''), + array("{{ attributes.addClass('everest').class }}", 'everest'), + array("{{ attributes.addClass(['k2', 'kangchenjunga']).class }}", 'k2 kangchenjunga'), + array("{{ attributes.addClass('lhotse', 'makalu', 'cho-oyu').class }}", 'lhotse makalu cho-oyu'), + array( + "{{ attributes.addClass('nanga-parbat').class }}", + 'dhaulagiri manaslu nanga-parbat', + array('class' => array('dhaulagiri', 'manaslu')), + ), + array( + "{{ attributes.removeClass('annapurna').class }}", + 'gasherbrum-i', + array('class' => array('annapurna', 'gasherbrum-i')), + ), + array( + "{{ attributes.removeClass(['broad peak']).class }}", + 'gasherbrum-ii', + array('class' => array('broad peak', 'gasherbrum-ii')), + ), + array( + "{{ attributes.removeClass('gyachung-kang', 'shishapangma').class }}", + '', + array('class' => array('shishapangma', 'gyachung-kang')), + ), + array( + "{{ attributes.removeClass('nuptse').addClass('annapurna-ii').class }}", + 'himalchuli annapurna-ii', + array('class' => array('himalchuli', 'nuptse')), + ), + ); + } + + /** * Tests iterating on the values of the attribute. */ public function testIterate() {