Chris@0: 'socks')); Chris@0: * $attributes['class'] = array('black-cat', 'white-cat'); Chris@0: * $attributes['class'][] = 'black-white-cat'; Chris@0: * echo ''; Chris@0: * // Produces Chris@0: * @endcode Chris@0: * Chris@0: * $attributes always prints out all the attributes. For example: Chris@0: * @code Chris@0: * $attributes = new Attribute(array('id' => 'socks')); Chris@0: * $attributes['class'] = array('black-cat', 'white-cat'); Chris@0: * $attributes['class'][] = 'black-white-cat'; Chris@0: * echo ''; Chris@0: * // Produces Chris@0: * @endcode Chris@0: * Chris@0: * When printing out individual attributes to customize them within a Twig Chris@0: * template, use the "without" filter to prevent attributes that have already Chris@0: * been printed from being printed again. For example: Chris@0: * @code Chris@0: * Chris@0: * {# Produces #} Chris@0: * @endcode Chris@0: * Chris@0: * The attribute keys and values are automatically escaped for output with Chris@0: * Html::escape(). No protocol filtering is applied, so when using user-entered Chris@0: * input as a value for an attribute that expects an URI (href, src, ...), Chris@0: * UrlHelper::stripDangerousProtocols() should be used to ensure dangerous Chris@0: * protocols (such as 'javascript:') are removed. For example: Chris@0: * @code Chris@0: * $path = 'javascript:alert("xss");'; Chris@0: * $path = UrlHelper::stripDangerousProtocols($path); Chris@0: * $attributes = new Attribute(array('href' => $path)); Chris@0: * echo ''; Chris@0: * // Produces Chris@0: * @endcode Chris@0: * Chris@0: * The attribute values are considered plain text and are treated as such. If a Chris@0: * safe HTML string is detected, it is converted to plain text with Chris@0: * PlainTextOutput::renderFromHtml() before being escaped. For example: Chris@0: * @code Chris@0: * $value = t('Highlight the @tag tag', ['@tag' => '']); Chris@0: * $attributes = new Attribute(['value' => $value]); Chris@0: * echo ''; Chris@0: * // Produces Chris@0: * @endcode Chris@0: * Chris@0: * @see \Drupal\Component\Utility\Html::escape() Chris@0: * @see \Drupal\Component\Render\PlainTextOutput::renderFromHtml() Chris@0: * @see \Drupal\Component\Utility\UrlHelper::stripDangerousProtocols() Chris@0: */ Chris@0: class Attribute implements \ArrayAccess, \IteratorAggregate, MarkupInterface { Chris@0: Chris@0: /** Chris@0: * Stores the attribute data. Chris@0: * Chris@0: * @var \Drupal\Core\Template\AttributeValueBase[] Chris@0: */ Chris@0: protected $storage = []; Chris@0: Chris@0: /** Chris@0: * Constructs a \Drupal\Core\Template\Attribute object. Chris@0: * Chris@0: * @param array $attributes Chris@0: * An associative array of key-value pairs to be converted to attributes. Chris@0: */ Chris@0: public function __construct($attributes = []) { Chris@0: foreach ($attributes as $name => $value) { Chris@0: $this->offsetSet($name, $value); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function offsetGet($name) { Chris@0: if (isset($this->storage[$name])) { Chris@0: return $this->storage[$name]; Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function offsetSet($name, $value) { Chris@0: $this->storage[$name] = $this->createAttributeValue($name, $value); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Creates the different types of attribute values. Chris@0: * Chris@0: * @param string $name Chris@0: * The attribute name. Chris@0: * @param mixed $value Chris@0: * The attribute value. Chris@0: * Chris@0: * @return \Drupal\Core\Template\AttributeValueBase Chris@0: * An AttributeValueBase representation of the attribute's value. Chris@0: */ Chris@0: protected function createAttributeValue($name, $value) { Chris@0: // If the value is already an AttributeValueBase object, Chris@0: // return a new instance of the same class, but with the new name. Chris@0: if ($value instanceof AttributeValueBase) { Chris@0: $class = get_class($value); Chris@0: return new $class($name, $value->value()); Chris@0: } Chris@0: // An array value or 'class' attribute name are forced to always be an Chris@0: // AttributeArray value for consistency. Chris@0: if ($name == 'class' && !is_array($value)) { Chris@0: // Cast the value to string in case it implements MarkupInterface. Chris@0: $value = [(string) $value]; Chris@0: } Chris@0: if (is_array($value)) { Chris@0: // Cast the value to an array if the value was passed in as a string. Chris@0: // @todo Decide to fix all the broken instances of class as a string Chris@0: // in core or cast them. Chris@0: $value = new AttributeArray($name, $value); Chris@0: } Chris@0: elseif (is_bool($value)) { Chris@0: $value = new AttributeBoolean($name, $value); Chris@0: } Chris@0: // As a development aid, we allow the value to be a safe string object. Chris@0: elseif ($value instanceof MarkupInterface) { Chris@0: // Attributes are not supposed to display HTML markup, so we just convert Chris@0: // the value to plain text. Chris@0: $value = PlainTextOutput::renderFromHtml($value); Chris@0: $value = new AttributeString($name, $value); Chris@0: } Chris@0: elseif (!is_object($value)) { Chris@0: $value = new AttributeString($name, $value); Chris@0: } Chris@0: return $value; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function offsetUnset($name) { Chris@0: unset($this->storage[$name]); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function offsetExists($name) { Chris@0: return isset($this->storage[$name]); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Adds classes or merges them on to array of existing CSS classes. Chris@0: * Chris@0: * @param string|array ... Chris@0: * CSS classes to add to the class attribute array. Chris@0: * Chris@0: * @return $this Chris@0: */ Chris@0: public function addClass() { Chris@0: $args = func_get_args(); Chris@0: if ($args) { Chris@0: $classes = []; Chris@0: foreach ($args as $arg) { Chris@0: // Merge the values passed in from the classes array. Chris@0: // The argument is cast to an array to support comma separated single Chris@0: // values or one or more array arguments. Chris@0: $classes = array_merge($classes, (array) $arg); Chris@0: } Chris@0: Chris@0: // Merge if there are values, just add them otherwise. Chris@0: if (isset($this->storage['class']) && $this->storage['class'] instanceof AttributeArray) { Chris@0: // Merge the values passed in from the class value array. Chris@0: $classes = array_merge($this->storage['class']->value(), $classes); Chris@0: $this->storage['class']->exchangeArray($classes); Chris@0: } Chris@0: else { Chris@0: $this->offsetSet('class', $classes); Chris@0: } Chris@0: } Chris@0: Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets values for an attribute key. Chris@0: * Chris@0: * @param string $attribute Chris@0: * Name of the attribute. Chris@0: * @param string|array $value Chris@0: * Value(s) to set for the given attribute key. Chris@0: * Chris@0: * @return $this Chris@0: */ Chris@0: public function setAttribute($attribute, $value) { Chris@0: $this->offsetSet($attribute, $value); Chris@0: Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Removes an attribute from an Attribute object. Chris@0: * Chris@0: * @param string|array ... Chris@0: * Attributes to remove from the attribute array. Chris@0: * Chris@0: * @return $this Chris@0: */ Chris@0: public function removeAttribute() { Chris@0: $args = func_get_args(); Chris@0: foreach ($args as $arg) { Chris@0: // Support arrays or multiple arguments. Chris@0: if (is_array($arg)) { Chris@0: foreach ($arg as $value) { Chris@0: unset($this->storage[$value]); Chris@0: } Chris@0: } Chris@0: else { Chris@0: unset($this->storage[$arg]); Chris@0: } Chris@0: } Chris@0: Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Removes argument values from array of existing CSS classes. Chris@0: * Chris@0: * @param string|array ... Chris@0: * CSS classes to remove from the class attribute array. Chris@0: * Chris@0: * @return $this Chris@0: */ Chris@0: public function removeClass() { Chris@0: // With no class attribute, there is no need to remove. Chris@0: if (isset($this->storage['class']) && $this->storage['class'] instanceof AttributeArray) { Chris@0: $args = func_get_args(); Chris@0: $classes = []; Chris@0: foreach ($args as $arg) { Chris@0: // Merge the values passed in from the classes array. Chris@0: // The argument is cast to an array to support comma separated single Chris@0: // values or one or more array arguments. Chris@0: $classes = array_merge($classes, (array) $arg); Chris@0: } Chris@0: Chris@0: // Remove the values passed in from the value array. Use array_values() to Chris@0: // ensure that the array index remains sequential. Chris@0: $classes = array_values(array_diff($this->storage['class']->value(), $classes)); Chris@0: $this->storage['class']->exchangeArray($classes); Chris@0: } Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@18: * Gets the class attribute value if set. Chris@18: * Chris@18: * This method is implemented to take precedence over hasClass() for Twig 2.0. Chris@18: * Chris@18: * @return \Drupal\Core\Template\AttributeValueBase Chris@18: * The class attribute value if set. Chris@18: * Chris@18: * @see twig_get_attribute() Chris@18: */ Chris@18: public function getClass() { Chris@18: return $this->offsetGet('class'); Chris@18: } Chris@18: Chris@18: /** Chris@0: * Checks if the class array has the given CSS class. Chris@0: * Chris@0: * @param string $class Chris@0: * The CSS class to check for. Chris@0: * Chris@0: * @return bool Chris@0: * Returns TRUE if the class exists, or FALSE otherwise. Chris@0: */ Chris@0: public function hasClass($class) { Chris@0: if (isset($this->storage['class']) && $this->storage['class'] instanceof AttributeArray) { Chris@0: return in_array($class, $this->storage['class']->value()); Chris@0: } Chris@0: else { Chris@0: return FALSE; Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Implements the magic __toString() method. Chris@0: */ Chris@0: public function __toString() { Chris@0: $return = ''; Chris@0: /** @var \Drupal\Core\Template\AttributeValueBase $value */ Chris@0: foreach ($this->storage as $name => $value) { Chris@0: $rendered = $value->render(); Chris@0: if ($rendered) { Chris@0: $return .= ' ' . $rendered; Chris@0: } Chris@0: } Chris@0: return $return; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns all storage elements as an array. Chris@0: * Chris@0: * @return array Chris@0: * An associative array of attributes. Chris@0: */ Chris@0: public function toArray() { Chris@0: $return = []; Chris@0: foreach ($this->storage as $name => $value) { Chris@0: $return[$name] = $value->value(); Chris@0: } Chris@0: Chris@0: return $return; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Implements the magic __clone() method. Chris@0: */ Chris@0: public function __clone() { Chris@0: foreach ($this->storage as $name => $value) { Chris@0: $this->storage[$name] = clone $value; Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function getIterator() { Chris@0: return new \ArrayIterator($this->storage); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns the whole array. Chris@0: */ Chris@0: public function storage() { Chris@0: return $this->storage; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns a representation of the object for use in JSON serialization. Chris@0: * Chris@0: * @return string Chris@0: * The safe string content. Chris@0: */ Chris@0: public function jsonSerialize() { Chris@0: return (string) $this; Chris@0: } Chris@0: Chris@0: }