From 17b1e5d2fcf882630c4de9549b7339513a8855f3 Mon Sep 17 00:00:00 2001
From: Qiang Xue <qiang.xue@gmail.com>
Date: Tue, 11 Jun 2013 16:35:14 -0400
Subject: [PATCH] Fixes issue #234: Added Html::addCssClass() and removeCssClass().

---
 framework/yii/bootstrap/Alert.php          |  2 +-
 framework/yii/bootstrap/Button.php         |  2 +-
 framework/yii/bootstrap/ButtonDropdown.php |  8 ++++----
 framework/yii/bootstrap/ButtonGroup.php    |  2 +-
 framework/yii/bootstrap/Carousel.php       |  8 ++++----
 framework/yii/bootstrap/Collapse.php       |  6 +++---
 framework/yii/bootstrap/Dropdown.php       |  4 ++--
 framework/yii/bootstrap/Modal.php          |  2 +-
 framework/yii/bootstrap/Nav.php            |  8 ++++----
 framework/yii/bootstrap/NavBar.php         |  4 ++--
 framework/yii/bootstrap/Progress.php       |  4 ++--
 framework/yii/bootstrap/Tabs.php           | 18 +++++++++---------
 framework/yii/bootstrap/Widget.php         | 16 ----------------
 framework/yii/helpers/base/Html.php        | 38 ++++++++++++++++++++++++++++++++++++++
 tests/unit/framework/helpers/HtmlTest.php  | 32 ++++++++++++++++++++++++++++++++
 15 files changed, 104 insertions(+), 50 deletions(-)

diff --git a/framework/yii/bootstrap/Alert.php b/framework/yii/bootstrap/Alert.php
index f84a70b..d57bcbe 100644
--- a/framework/yii/bootstrap/Alert.php
+++ b/framework/yii/bootstrap/Alert.php
@@ -140,7 +140,7 @@ class Alert extends Widget
 			'class' => 'fade in',
 		), $this->options);
 
-		$this->addCssClass($this->options, 'alert');
+		Html::addCssClass($this->options, 'alert');
 
 		if ($this->closeButton !== null) {
 			$this->closeButton = array_merge(array(
diff --git a/framework/yii/bootstrap/Button.php b/framework/yii/bootstrap/Button.php
index 856c420..c351ea0 100644
--- a/framework/yii/bootstrap/Button.php
+++ b/framework/yii/bootstrap/Button.php
@@ -48,7 +48,7 @@ class Button extends Widget
 	{
 		parent::init();
 		$this->clientOptions = false;
-		$this->addCssClass($this->options, 'btn');
+		Html::addCssClass($this->options, 'btn');
 	}
 
 	/**
diff --git a/framework/yii/bootstrap/ButtonDropdown.php b/framework/yii/bootstrap/ButtonDropdown.php
index fec042e..5d94bdc 100644
--- a/framework/yii/bootstrap/ButtonDropdown.php
+++ b/framework/yii/bootstrap/ButtonDropdown.php
@@ -64,7 +64,7 @@ class ButtonDropdown extends Widget
 	public function init()
 	{
 		parent::init();
-		$this->addCssClass($this->options, 'btn-group');
+		Html::addCssClass($this->options, 'btn-group');
 	}
 
 	/**
@@ -85,12 +85,12 @@ class ButtonDropdown extends Widget
 	 */
 	protected function renderButton()
 	{
-		$this->addCssClass($this->buttonOptions, 'btn');
+		Html::addCssClass($this->buttonOptions, 'btn');
 		if ($this->split) {
 			$tag = 'button';
 			$options = $this->buttonOptions;
 			$this->buttonOptions['data-toggle'] = 'dropdown';
-			$this->addCssClass($this->buttonOptions, 'dropdown-toggle');
+			Html::addCssClass($this->buttonOptions, 'dropdown-toggle');
 			$splitButton = Button::widget(array(
 				'label' => '<span class="caret"></span>',
 				'encodeLabel' => false,
@@ -103,7 +103,7 @@ class ButtonDropdown extends Widget
 			if (!isset($options['href'])) {
 				$options['href'] = '#';
 			}
-			$this->addCssClass($options, 'dropdown-toggle');
+			Html::addCssClass($options, 'dropdown-toggle');
 			$options['data-toggle'] = 'dropdown';
 			$splitButton = '';
 		}
diff --git a/framework/yii/bootstrap/ButtonGroup.php b/framework/yii/bootstrap/ButtonGroup.php
index e5bf4e9..3606241 100644
--- a/framework/yii/bootstrap/ButtonGroup.php
+++ b/framework/yii/bootstrap/ButtonGroup.php
@@ -61,7 +61,7 @@ class ButtonGroup extends Widget
 	{
 		parent::init();
 		$this->clientOptions = false;
-		$this->addCssClass($this->options, 'btn-group');
+		Html::addCssClass($this->options, 'btn-group');
 	}
 
 	/**
diff --git a/framework/yii/bootstrap/Carousel.php b/framework/yii/bootstrap/Carousel.php
index f8904fa..c2c68a7 100644
--- a/framework/yii/bootstrap/Carousel.php
+++ b/framework/yii/bootstrap/Carousel.php
@@ -70,7 +70,7 @@ class Carousel extends Widget
 	public function init()
 	{
 		parent::init();
-		$this->addCssClass($this->options, 'carousel');
+		Html::addCssClass($this->options, 'carousel');
 	}
 
 	/**
@@ -96,7 +96,7 @@ class Carousel extends Widget
 		for ($i = 0, $count = count($this->items); $i < $count; $i++) {
 			$options = array('data-target' => '#' . $this->options['id'], 'data-slide-to' => $i);
 			if ($i === 0) {
-				$this->addCssClass($options, 'active');
+				Html::addCssClass($options, 'active');
 			}
 			$indicators[] = Html::tag('li', '', $options);
 		}
@@ -140,9 +140,9 @@ class Carousel extends Widget
 			throw new InvalidConfigException('The "content" option is required.');
 		}
 
-		$this->addCssClass($options, 'item');
+		Html::addCssClass($options, 'item');
 		if ($index === 0) {
-			$this->addCssClass($options, 'active');
+			Html::addCssClass($options, 'active');
 		}
 
 		return Html::tag('div', $content . "\n" . $caption, $options);
diff --git a/framework/yii/bootstrap/Collapse.php b/framework/yii/bootstrap/Collapse.php
index fdcaae1..8aed0b1 100644
--- a/framework/yii/bootstrap/Collapse.php
+++ b/framework/yii/bootstrap/Collapse.php
@@ -66,7 +66,7 @@ class Collapse extends Widget
 	public function init()
 	{
 		parent::init();
-		$this->addCssClass($this->options, 'accordion');
+		Html::addCssClass($this->options, 'accordion');
 	}
 
 	/**
@@ -90,7 +90,7 @@ class Collapse extends Widget
 		$index = 0;
 		foreach ($this->items as $header => $item) {
 			$options = ArrayHelper::getValue($item, 'options', array());
-			$this->addCssClass($options, 'accordion-group');
+			Html::addCssClass($options, 'accordion-group');
 			$items[] = Html::tag('div', $this->renderItem($header, $item, ++$index), $options);
 		}
 
@@ -111,7 +111,7 @@ class Collapse extends Widget
 			$id = $this->options['id'] . '-collapse' . $index;
 			$options = ArrayHelper::getValue($item, 'contentOptions', array());
 			$options['id'] = $id;
-			$this->addCssClass($options, 'accordion-body collapse');
+			Html::addCssClass($options, 'accordion-body collapse');
 
 			$header = Html::a($header, '#' . $id, array(
 					'class' => 'accordion-toggle',
diff --git a/framework/yii/bootstrap/Dropdown.php b/framework/yii/bootstrap/Dropdown.php
index 827e6cc..0568fe4 100644
--- a/framework/yii/bootstrap/Dropdown.php
+++ b/framework/yii/bootstrap/Dropdown.php
@@ -46,7 +46,7 @@ class Dropdown extends Widget
 	public function init()
 	{
 		parent::init();
-		$this->addCssClass($this->options, 'dropdown-menu');
+		Html::addCssClass($this->options, 'dropdown-menu');
 	}
 
 	/**
@@ -81,7 +81,7 @@ class Dropdown extends Widget
 			$linkOptions['tabindex'] = '-1';
 
 			if (isset($item['items'])) {
-				$this->addCssClass($options, 'dropdown-submenu');
+				Html::addCssClass($options, 'dropdown-submenu');
 				$content = Html::a($label, '#', $linkOptions) . $this->renderItems($item['items']);
 			} else {
 				$content = Html::a($label, ArrayHelper::getValue($item, 'url', '#'), $linkOptions);
diff --git a/framework/yii/bootstrap/Modal.php b/framework/yii/bootstrap/Modal.php
index 0608fbe..f676273 100644
--- a/framework/yii/bootstrap/Modal.php
+++ b/framework/yii/bootstrap/Modal.php
@@ -197,7 +197,7 @@ class Modal extends Widget
 		$this->options = array_merge(array(
 			'class' => 'modal hide',
 		), $this->options);
-		$this->addCssClass($this->options, 'modal');
+		Html::addCssClass($this->options, 'modal');
 
 		$this->clientOptions = array_merge(array(
 			'show' => false,
diff --git a/framework/yii/bootstrap/Nav.php b/framework/yii/bootstrap/Nav.php
index 8069699..7b003f4 100644
--- a/framework/yii/bootstrap/Nav.php
+++ b/framework/yii/bootstrap/Nav.php
@@ -79,7 +79,7 @@ class Nav extends Widget
 	public function init()
 	{
 		parent::init();
-		$this->addCssClass($this->options, 'nav');
+		Html::addCssClass($this->options, 'nav');
 	}
 
 	/**
@@ -125,13 +125,13 @@ class Nav extends Widget
 		$linkOptions = ArrayHelper::getValue($item, 'linkOptions', array());
 
 		if (ArrayHelper::getValue($item, 'active')) {
-			$this->addCssClass($options, 'active');
+			Html::addCssClass($options, 'active');
 		}
 
 		if ($items !== null) {
 			$linkOptions['data-toggle'] = 'dropdown';
-			$this->addCssClass($options, 'dropdown');
-			$this->addCssClass($urlOptions, 'dropdown-toggle');
+			Html::addCssClass($options, 'dropdown');
+			Html::addCssClass($urlOptions, 'dropdown-toggle');
 			$label .= ' ' . Html::tag('b', '', array('class' => 'caret'));
 			if (is_array($items)) {
 				$items = Dropdown::widget(array(
diff --git a/framework/yii/bootstrap/NavBar.php b/framework/yii/bootstrap/NavBar.php
index 17a938c..337e449 100644
--- a/framework/yii/bootstrap/NavBar.php
+++ b/framework/yii/bootstrap/NavBar.php
@@ -107,8 +107,8 @@ class NavBar extends Widget
 	{
 		parent::init();
 		$this->clientOptions = false;
-		$this->addCssClass($this->options, 'navbar');
-		$this->addCssClass($this->brandOptions, 'brand');
+		Html::addCssClass($this->options, 'navbar');
+		Html::addCssClass($this->brandOptions, 'brand');
 	}
 
 	/**
diff --git a/framework/yii/bootstrap/Progress.php b/framework/yii/bootstrap/Progress.php
index 7c0473e..ae44619 100644
--- a/framework/yii/bootstrap/Progress.php
+++ b/framework/yii/bootstrap/Progress.php
@@ -94,7 +94,7 @@ class Progress extends Widget
 	public function init()
 	{
 		parent::init();
-		$this->addCssClass($this->options, 'progress');
+		Html::addCssClass($this->options, 'progress');
 	}
 
 	/**
@@ -139,7 +139,7 @@ class Progress extends Widget
 	 */
 	protected function renderBar($percent, $label = '', $options = array())
 	{
-		$this->addCssClass($options, 'bar');
+		Html::addCssClass($options, 'bar');
 		$options['style'] = "width:{$percent}%";
 		return Html::tag('div', $label, $options);
 	}
diff --git a/framework/yii/bootstrap/Tabs.php b/framework/yii/bootstrap/Tabs.php
index 4a85b9a..90c9794 100644
--- a/framework/yii/bootstrap/Tabs.php
+++ b/framework/yii/bootstrap/Tabs.php
@@ -93,7 +93,7 @@ class Tabs extends Widget
 	public function init()
 	{
 		parent::init();
-		$this->addCssClass($this->options, 'nav nav-tabs');
+		Html::addCssClass($this->options, 'nav nav-tabs');
 	}
 
 	/**
@@ -123,10 +123,10 @@ class Tabs extends Widget
 
 			if (isset($item['items'])) {
 				$label .= ' <b class="caret"></b>';
-				$this->addCssClass($headerOptions, 'dropdown');
+				Html::addCssClass($headerOptions, 'dropdown');
 
 				if ($this->renderDropdown($item['items'], $panes)) {
-					$this->addCssClass($headerOptions, 'active');
+					Html::addCssClass($headerOptions, 'active');
 				}
 
 				$header = Html::a($label, "#", array('class' => 'dropdown-toggle', 'data-toggle' => 'dropdown')) . "\n"
@@ -135,10 +135,10 @@ class Tabs extends Widget
 				$options = array_merge($this->itemOptions, ArrayHelper::getValue($item, 'options', array()));
 				$options['id'] = ArrayHelper::getValue($options, 'id', $this->options['id'] . '-tab' . $n);
 
-				$this->addCssClass($options, 'tab-pane');
+				Html::addCssClass($options, 'tab-pane');
 				if (ArrayHelper::remove($item, 'active')) {
-					$this->addCssClass($options, 'active');
-					$this->addCssClass($headerOptions, 'active');
+					Html::addCssClass($options, 'active');
+					Html::addCssClass($headerOptions, 'active');
 				}
 				$header = Html::a($label, '#' . $options['id'], array('data-toggle' => 'tab', 'tabindex' => '-1'));
 				$panes[] = Html::tag('div', $item['content'], $options);
@@ -175,10 +175,10 @@ class Tabs extends Widget
 
 			$content = ArrayHelper::remove($item, 'content');
 			$options = ArrayHelper::remove($item, 'contentOptions', array());
-			$this->addCssClass($options, 'tab-pane');
+			Html::addCssClass($options, 'tab-pane');
 			if (ArrayHelper::remove($item, 'active')) {
-				$this->addCssClass($options, 'active');
-				$this->addCssClass($item['options'], 'active');
+				Html::addCssClass($options, 'active');
+				Html::addCssClass($item['options'], 'active');
 				$itemActive = true;
 			}
 
diff --git a/framework/yii/bootstrap/Widget.php b/framework/yii/bootstrap/Widget.php
index 48b0331..004f040 100644
--- a/framework/yii/bootstrap/Widget.php
+++ b/framework/yii/bootstrap/Widget.php
@@ -82,20 +82,4 @@ class Widget extends \yii\base\Widget
 			$view->registerJs(implode("\n", $js));
 		}
 	}
-
-	/**
-	 * Adds a CSS class to the specified options.
-	 * This method will ensure that the CSS class is unique and the "class" option is properly formatted.
-	 * @param array $options the options to be modified.
-	 * @param string $class the CSS class to be added
-	 */
-	protected function addCssClass(&$options, $class)
-	{
-		if (isset($options['class'])) {
-			$classes = preg_split('/\s+/', $options['class'] . ' ' . $class, -1, PREG_SPLIT_NO_EMPTY);
-			$options['class'] = implode(' ', array_unique($classes));
-		} else {
-			$options['class'] = $class;
-		}
-	}
 }
diff --git a/framework/yii/helpers/base/Html.php b/framework/yii/helpers/base/Html.php
index 47385e2..ba9a889 100644
--- a/framework/yii/helpers/base/Html.php
+++ b/framework/yii/helpers/base/Html.php
@@ -1373,6 +1373,44 @@ class Html
 	}
 
 	/**
+	 * Adds a CSS class to the specified options.
+	 * If the CSS class is already in the options, it will not be added again.
+	 * @param array $options the options to be modified.
+	 * @param string $class the CSS class to be added
+	 */
+	public static function addCssClass(&$options, $class)
+	{
+		if (isset($options['class'])) {
+			$classes = ' ' . $options['class'] . ' ';
+			if (($pos = strpos($classes, ' ' . $class . ' ')) === false) {
+				$options['class'] .= ' ' . $class;
+			}
+		} else {
+			$options['class'] = $class;
+		}
+	}
+
+	/**
+	 * Removes a CSS class from the specified options.
+	 * @param array $options the options to be modified.
+	 * @param string $class the CSS class to be removed
+	 */
+	public static function removeCssClass(&$options, $class)
+	{
+		if (isset($options['class'])) {
+			$classes = array_unique(preg_split('/\s+/', $options['class'] . ' ' . $class, -1, PREG_SPLIT_NO_EMPTY));
+			if (($index = array_search($class, $classes)) !== false) {
+				unset($classes[$index]);
+			}
+			if (empty($classes)) {
+				unset($options['class']);
+			} else {
+				$options['class'] = implode(' ', $classes);
+			}
+		}
+	}
+
+	/**
 	 * Returns the real attribute name from the given attribute expression.
 	 *
 	 * An attribute expression is an attribute name prefixed and/or suffixed with array indexes.
diff --git a/tests/unit/framework/helpers/HtmlTest.php b/tests/unit/framework/helpers/HtmlTest.php
index 2311321..0399a4e 100644
--- a/tests/unit/framework/helpers/HtmlTest.php
+++ b/tests/unit/framework/helpers/HtmlTest.php
@@ -434,6 +434,38 @@ EOD;
 		Html::$showBooleanAttributeValues = true;
 	}
 
+	public function testAddCssClass()
+	{
+		$options = array();
+		Html::addCssClass($options, 'test');
+		$this->assertEquals(array('class' => 'test'), $options);
+		Html::addCssClass($options, 'test');
+		$this->assertEquals(array('class' => 'test'), $options);
+		Html::addCssClass($options, 'test2');
+		$this->assertEquals(array('class' => 'test test2'), $options);
+		Html::addCssClass($options, 'test');
+		$this->assertEquals(array('class' => 'test test2'), $options);
+		Html::addCssClass($options, 'test2');
+		$this->assertEquals(array('class' => 'test test2'), $options);
+		Html::addCssClass($options, 'test3');
+		$this->assertEquals(array('class' => 'test test2 test3'), $options);
+		Html::addCssClass($options, 'test2');
+		$this->assertEquals(array('class' => 'test test2 test3'), $options);
+	}
+
+	public function testRemoveCssClass()
+	{
+		$options = array('class' => 'test test2 test3');
+		Html::removeCssClass($options, 'test2');
+		$this->assertEquals(array('class' => 'test test3'), $options);
+		Html::removeCssClass($options, 'test2');
+		$this->assertEquals(array('class' => 'test test3'), $options);
+		Html::removeCssClass($options, 'test');
+		$this->assertEquals(array('class' => 'test3'), $options);
+		Html::removeCssClass($options, 'test3');
+		$this->assertEquals(array(), $options);
+	}
+
 	protected function getDataItems()
 	{
 		return array(
--
libgit2 0.27.1