diff --git a/src/Utility/ArrayObject.php b/src/Utility/ArrayObject.php new file mode 100644 index 0000000000000000000000000000000000000000..b5d448ca107cdc3168f87c0da118f633013b23f8 --- /dev/null +++ b/src/Utility/ArrayObject.php @@ -0,0 +1,413 @@ +<?php + +declare(strict_types = 1); + +namespace Drupal\ui_suite_bootstrap\Utility; + +use Drupal\Component\Utility\NestedArray; +use Drupal\Core\Cache\RefinableCacheableDependencyInterface; +use Drupal\Core\Render\AttachmentsInterface; +use Drupal\Core\Render\BubbleableMetadata; + +/** + * Custom ArrayObject implementation. + * + * The native ArrayObject is unnecessarily complicated. + * + * @ingroup utility + */ +class ArrayObject implements \IteratorAggregate, \ArrayAccess, \Serializable, \Countable, AttachmentsInterface, RefinableCacheableDependencyInterface { + + /** + * The array. + * + * @var array + */ + protected $array; + + /** + * Array object constructor. + * + * @param array $array + * An array. + */ + public function __construct(array $array = []) { + $this->array = $array; + } + + /** + * Returns whether the requested key exists. + * + * @param mixed $key + * A key. + * + * @return bool + * TRUE or FALSE + */ + public function __isset($key) { + return $this->offsetExists($key); + } + + /** + * Sets the value at the specified key to value. + * + * @param mixed $key + * A key. + * @param mixed $value + * A value. + */ + public function __set($key, $value) { + $this->offsetSet($key, $value); + } + + /** + * Unsets the value at the specified key. + * + * @param mixed $key + * A key. + */ + public function __unset($key) { + $this->offsetUnset($key); + } + + /** + * Returns the value at the specified key by reference. + * + * @param mixed $key + * A key. + * + * @return mixed + * The stored value. + */ + public function &__get($key) { + $ret = &$this->offsetGet($key); + return $ret; + } + + /** + * {@inheritdoc} + */ + public function addAttachments(array $attachments) { + BubbleableMetadata::createFromRenderArray($this->array)->addAttachments($attachments)->applyTo($this->array); + return $this; + } + + /** + * {@inheritdoc} + */ + public function addCacheContexts(array $cache_contexts) { + BubbleableMetadata::createFromRenderArray($this->array)->addCacheContexts($cache_contexts)->applyTo($this->array); + return $this; + } + + /** + * {@inheritdoc} + */ + public function addCacheTags(array $cache_tags) { + BubbleableMetadata::createFromRenderArray($this->array)->addCacheTags($cache_tags)->applyTo($this->array); + return $this; + } + + /** + * {@inheritdoc} + */ + public function addCacheableDependency($other_object) { + BubbleableMetadata::createFromRenderArray($this->array)->addCacheableDependency($other_object)->applyTo($this->array); + return $this; + } + + /** + * Appends the value. + * + * @param mixed $value + * A value. + */ + public function append($value): void { + $this->array[] = $value; + } + + /** + * Sort the entries by value. + */ + public function asort(): void { + \asort($this->array); + } + + /** + * Merges an object's cacheable metadata into the variables array. + * + * @param \Drupal\Core\Cache\CacheableDependencyInterface|mixed $object + * The object whose cacheability metadata to retrieve. If it implements + * CacheableDependencyInterface, its cacheability metadata will be used, + * otherwise, the passed in object must be assumed to be uncacheable, so + * max-age 0 is set. + * + * @return $this + */ + public function bubbleObject($object) { + BubbleableMetadata::createFromRenderArray($this->array)->merge(BubbleableMetadata::createFromObject($object))->applyTo($this->array); + return $this; + } + + /** + * Merges a render array's cacheable metadata into the variables array. + * + * @param array $build + * A render array. + * + * @return $this + */ + public function bubbleRenderArray(array $build) { + BubbleableMetadata::createFromRenderArray($this->array)->merge(BubbleableMetadata::createFromRenderArray($build))->applyTo($this->array); + return $this; + } + + /** + * Get the number of public properties in the ArrayObject. + * + * @return int + * The count. + */ + public function count(): int { + return \count($this->array); + } + + /** + * Exchange the array for another one. + * + * @param array|ArrayObject $data + * New data. + * + * @throws \InvalidArgumentException + * When the passed data is not an array or an instance of ArrayObject. + * + * @return array + * The old array. + */ + public function exchangeArray($data) { + if (!\is_array($data) && \is_object($data) && !($data instanceof ArrayObject)) { + throw new \InvalidArgumentException('Passed variable is not an array or an instance of \Drupal\bootstrap\Utility\ArrayObject.'); + } + if (\is_object($data) && $data instanceof ArrayObject) { + $data = $data->getArrayCopy(); + } + $old = $this->array; + $this->array = $data; + return $old; + } + + /** + * Creates a copy of the ArrayObject. + * + * @return array + * A copy of the array. + */ + public function getArrayCopy() { + return $this->array; + } + + /** + * {@inheritdoc} + */ + public function getAttachments() { + return BubbleableMetadata::createFromRenderArray($this->array)->getAttachments(); + } + + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + return BubbleableMetadata::createFromRenderArray($this->array)->getCacheContexts(); + } + + /** + * {@inheritdoc} + */ + public function getCacheTags() { + return BubbleableMetadata::createFromRenderArray($this->array)->getCacheTags(); + } + + /** + * {@inheritdoc} + */ + public function getCacheMaxAge() { + return BubbleableMetadata::createFromRenderArray($this->array)->getCacheMaxAge(); + } + + /** + * Creates a new iterator from an ArrayObject instance. + * + * @return \ArrayIterator + * An array iterator. + */ + public function getIterator(): \Traversable { + return new \ArrayIterator($this->array); + } + + /** + * Sort the entries by key. + */ + public function ksort(): void { + \ksort($this->array); + } + + /** + * Merges multiple values into the array. + * + * @param array $values + * An associative key/value array. + * @param bool $recursive + * Flag determining whether or not to recursively merge key/value pairs. + * + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + */ + public function merge(array $values, $recursive = TRUE): void { + if ($recursive) { + $this->array = NestedArray::mergeDeepArray([$this->array, $values], TRUE); + } + else { + $this->array += $values; + } + } + + /** + * {@inheritdoc} + */ + public function mergeCacheMaxAge($max_age) { + BubbleableMetadata::createFromRenderArray($this->array)->mergeCacheMaxAge($max_age)->applyTo($this->array); + return $this; + } + + /** + * Sort an array using a case insensitive "natural order" algorithm. + */ + public function natcasesort(): void { + \natcasesort($this->array); + } + + /** + * Sort entries using a "natural order" algorithm. + */ + public function natsort(): void { + \natsort($this->array); + } + + /** + * Returns whether the requested key exists. + * + * @param mixed $key + * A key. + * + * @return bool + * TRUE or FALSE + */ + public function offsetExists($key): bool { + return isset($this->array[$key]); + } + + /** + * Returns the value at the specified key. + * + * @param mixed $key + * A key. + * @param mixed $default + * The default value to set if $key does not exist. + * + * @return mixed + * The value. + */ + public function &offsetGet($key, $default = NULL): mixed { + if (!$this->offsetExists($key)) { + $this->array[$key] = $default; + } + $ret = &$this->array[$key]; + return $ret; + } + + /** + * Sets the value at the specified key to value. + * + * @param mixed $key + * A key. + * @param mixed $value + * A value. + */ + public function offsetSet($key, $value): void { + $this->array[$key] = $value; + } + + /** + * Unsets the value at the specified key. + * + * @param mixed $key + * A key. + */ + public function offsetUnset($key): void { + if ($this->offsetExists($key)) { + unset($this->array[$key]); + } + } + + /** + * {@inheritdoc} + */ + public function setAttachments(array $attachments) { + BubbleableMetadata::createFromRenderArray($this->array)->setAttachments($attachments)->applyTo($this->array); + return $this; + } + + /** + * Sort entries with a user-defined function and maintain key association. + * + * @param mixed $function + * A callable function. + */ + public function uasort($function): void { + if (\is_callable($function)) { + \uasort($this->array, $function); + } + } + + /** + * Sort the entries by keys using a user-defined comparison function. + * + * @param mixed $function + * A callable function. + */ + public function uksort($function): void { + if (\is_callable($function)) { + \uksort($this->array, $function); + } + } + + /** + * {@inheritdoc} + */ + public function serialize() { + return \serialize($this->__serialize()); + } + + /** + * {@inheritdoc} + */ + public function unserialize($serialized) { + // phpcs:disable + $this->__unserialize(\unserialize($serialized)); + // phpcs:enable + } + + /** + * {@inheritdoc} + */ + public function __serialize(): array { + return \get_object_vars($this); + } + + /** + * {@inheritdoc} + */ + public function __unserialize(array $data): void { + $this->exchangeArray($data['array']); + } + +} diff --git a/src/Utility/Attributes.php b/src/Utility/Attributes.php new file mode 100644 index 0000000000000000000000000000000000000000..7127c2e632679ed644999cff52dd8684716a067d --- /dev/null +++ b/src/Utility/Attributes.php @@ -0,0 +1,192 @@ +<?php + +declare(strict_types = 1); + +namespace Drupal\ui_suite_bootstrap\Utility; + +use Drupal\Core\Template\AttributeValueBase; + +/** + * Class to help modify attributes. + * + * @ingroup utility + */ +class Attributes extends ArrayObject { + + /** + * {@inheritdoc} + */ + public function __construct(array &$array = []) { + $this->array = &$array; + } + + /** + * Add class(es) to the array. + * + * @param string|array $class + * An individual class or an array of classes to add. + * + * @see \Drupal\ui_suite_bootstrap\Utility\Attributes::getClasses() + */ + public function addClass($class): void { + // Handle core Attribute based object values. + // @see https://www.drupal.org/project/bootstrap/issues/3020266 + if ($class instanceof AttributeValueBase) { + $class = $class->value(); + } + $classes = &$this->getClasses(); + $classes = \array_unique(\array_merge($classes, (array) $class)); + } + + /** + * Retrieve a specific attribute from the array. + * + * @param string $name + * The specific attribute to retrieve. + * @param mixed $default + * (optional) The default value to set if the attribute does not exist. + * + * @return mixed + * A specific attribute value, passed by reference. + * + * @see \Drupal\ui_suite_bootstrap\Utility\ArrayObject::offsetGet() + */ + public function &getAttribute($name, $default = NULL) { + return $this->offsetGet($name, $default); + } + + /** + * Retrieves classes from the array. + * + * @return array + * The classes array, passed by reference. + * + * @see \Drupal\ui_suite_bootstrap\Utility\ArrayObject::offsetGet() + */ + public function &getClasses(): array { + $classes = &$this->offsetGet('class', []); + $classes = \array_unique($classes); + return $classes; + } + + /** + * Indicates whether a specific attribute is set. + * + * @param string $name + * The attribute to search for. + * + * @return bool + * TRUE or FALSE. + * + * @see \Drupal\ui_suite_bootstrap\Utility\ArrayObject::offsetExists() + */ + public function hasAttribute($name): bool { + return $this->offsetExists($name); + } + + /** + * Indicates whether a class is present in the array. + * + * @param string|array $class + * The class or array of classes to search for. + * @param bool $all + * Flag determining to check if all classes are present. + * + * @return bool + * TRUE or FALSE. + * + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + * + * @see \Drupal\ui_suite_bootstrap\Utility\Attributes::getClasses() + */ + public function hasClass($class, $all = FALSE): bool { + $classes = (array) $class; + $result = \array_intersect($classes, $this->getClasses()); + return $all ? $result && \count($classes) === \count($result) : (bool) $result; + } + + /** + * Removes an attribute from the array. + * + * @param string|array $name + * The name of the attribute to remove. + * + * @see \Drupal\ui_suite_bootstrap\Utility\ArrayObject::offsetUnset() + */ + public function removeAttribute($name): void { + $this->offsetUnset($name); + } + + /** + * Removes a class from the attributes array. + * + * @param string|array $class + * An individual class or an array of classes to remove. + * + * @see \Drupal\ui_suite_bootstrap\Utility\Attributes::getClasses() + */ + public function removeClass($class): void { + $classes = &$this->getClasses(); + $classes = \array_values(\array_diff($classes, (array) $class)); + } + + /** + * Replaces a class in the attributes array. + * + * @param string $old + * The old class to remove. + * @param string $new + * The new class. It will not be added if the $old class does not exist. + * + * @see \Drupal\ui_suite_bootstrap\Utility\Attributes::getClasses() + */ + public function replaceClass($old, $new): void { + $classes = &$this->getClasses(); + $key = \array_search($old, $classes, TRUE); + if ($key !== FALSE) { + $classes[$key] = $new; + } + } + + /** + * Sets an attribute on the array. + * + * @param string $name + * The name of the attribute to set. + * @param mixed $value + * The value of the attribute to set. + * + * @see \Drupal\ui_suite_bootstrap\Utility\ArrayObject::offsetSet() + */ + public function setAttribute($name, $value): void { + // Handle class attribute differently. + if ($name === 'class') { + $this->removeAttribute('class'); + $this->addClass($value); + } + else { + $this->offsetSet($name, $value); + } + } + + /** + * Sets multiple attributes on the array. + * + * @param array $values + * An associative key/value array of attributes to set. + * + * @see \Drupal\ui_suite_bootstrap\Utility\ArrayObject::merge() + */ + public function setAttributes(array $values): void { + // Handle class attribute differently. + $classes = $values['class'] ?? []; + unset($values['class']); + if ($classes) { + $this->addClass($classes); + } + + // Merge the reset of the attributes. + $this->merge($values); + } + +} diff --git a/src/Utility/DrupalAttributes.php b/src/Utility/DrupalAttributes.php new file mode 100644 index 0000000000000000000000000000000000000000..a8601d8659f6fdee4d849ea0607da910e4c805a8 --- /dev/null +++ b/src/Utility/DrupalAttributes.php @@ -0,0 +1,294 @@ +<?php + +declare(strict_types = 1); + +namespace Drupal\ui_suite_bootstrap\Utility; + +use Drupal\Core\Template\Attribute; + +/** + * Class for managing multiple types of attributes commonly found in Drupal. + */ +class DrupalAttributes extends ArrayObject { + + /** + * Defines the "attributes" storage type constant. + * + * @var string + */ + public const ATTRIBUTES = 'attributes'; + + /** + * Defines the "description_attributes" storage type constant. + * + * @var string + */ + public const DESCRIPTION = 'description_attributes'; + + /** + * Defines the "input_group_attributes" storage type constant. + * + * @var string + */ + public const INPUT_GROUP = 'input_group_attributes'; + + /** + * Defines the "label_attributes" storage type constant. + * + * @var string + */ + public const LABEL = 'label_attributes'; + + /** + * Defines the "title_attributes" storage type constant. + * + * @var string + */ + public const TITLE = 'title_attributes'; + + /** + * Defines the "wrapper_attributes" storage type constant. + * + * @var string + */ + public const WRAPPER = 'wrapper_attributes'; + + /** + * Stored attribute instances. + * + * @var \Drupal\ui_suite_bootstrap\Utility\Attributes[] + */ + protected $attributes = []; + + /** + * A prefix to use for retrieving attribute keys from the array. + * + * @var string + */ + protected $attributePrefix = ''; + + /** + * Add class(es) to an attributes object. + * + * This is a wrapper method to retrieve the correct attributes storage object + * and then add the class(es) to it. + * + * @param string|array $class + * An individual class or an array of classes to add. + * @param string $type + * (optional) The type of attributes to use for this method. + * + * @return $this + * + * @see \Drupal\ui_suite_bootstrap\Utility\Attributes::addClass() + */ + public function addClass($class, $type = DrupalAttributes::ATTRIBUTES) { + $this->getAttributes($type)->addClass($class); + return $this; + } + + /** + * Retrieve a specific attribute from an attributes object. + * + * This is a wrapper method to retrieve the correct attributes storage object + * and then retrieve the attribute from it. + * + * @param string $name + * The specific attribute to retrieve. + * @param mixed $default + * (optional) The default value to set if the attribute does not exist. + * @param string $type + * (optional) The type of attributes to use for this method. + * + * @return mixed + * A specific attribute value, passed by reference. + * + * @see \Drupal\ui_suite_bootstrap\Utility\Attributes::getAttribute() + */ + public function &getAttribute($name, $default = NULL, $type = DrupalAttributes::ATTRIBUTES) { + return $this->getAttributes($type)->getAttribute($name, $default); + } + + /** + * Retrieves a specific attributes object. + * + * @param string $type + * (optional) The type of attributes to use for this method. + * + * @return \Drupal\ui_suite_bootstrap\Utility\Attributes + * An attributes object for $type. + */ + public function getAttributes($type = DrupalAttributes::ATTRIBUTES) { + if (!isset($this->attributes[$type])) { + $attributes = &$this->offsetGet($this->attributePrefix . $type, []); + if ($attributes instanceof Attribute) { + $attributes = $attributes->toArray(); + } + $this->attributes[$type] = new Attributes($attributes); + } + return $this->attributes[$type]; + } + + /** + * Retrieves classes from an attributes object. + * + * This is a wrapper method to retrieve the correct attributes storage object + * and then retrieve the set classes from it. + * + * @param string $type + * (optional) The type of attributes to use for this method. + * + * @return array + * The classes array, passed by reference. + * + * @see \Drupal\ui_suite_bootstrap\Utility\Attributes::getClasses() + */ + public function &getClasses($type = DrupalAttributes::ATTRIBUTES) { + return $this->getAttributes($type)->getClasses(); + } + + /** + * Indicates whether an attributes object has a specific attribute set. + * + * This is a wrapper method to retrieve the correct attributes storage object + * and then check there if the attribute exists. + * + * @param string $name + * The attribute to search for. + * @param string $type + * (optional) The type of attributes to use for this method. + * + * @return bool + * TRUE or FALSE + * + * @see \Drupal\ui_suite_bootstrap\Utility\Attributes::hasAttribute() + */ + public function hasAttribute($name, $type = DrupalAttributes::ATTRIBUTES) { + return $this->getAttributes($type)->hasAttribute($name); + } + + /** + * Indicates whether an attributes object has a specific class. + * + * This is a wrapper method to retrieve the correct attributes storage object + * and then check there if a class exists in the attributes object. + * + * @param string $class + * The class to search for. + * @param string $type + * (optional) The type of attributes to use for this method. + * + * @return bool + * TRUE or FALSE + * + * @see \Drupal\ui_suite_bootstrap\Utility\Attributes::hasClass() + */ + public function hasClass($class, $type = DrupalAttributes::ATTRIBUTES) { + return $this->getAttributes($type)->hasClass($class); + } + + /** + * Removes an attribute from an attributes object. + * + * This is a wrapper method to retrieve the correct attributes storage object + * and then remove an attribute from it. + * + * @param string|array $name + * The name of the attribute to remove. + * @param string $type + * (optional) The type of attributes to use for this method. + * + * @return $this + * + * @see \Drupal\ui_suite_bootstrap\Utility\Attributes::removeAttribute() + */ + public function removeAttribute($name, $type = DrupalAttributes::ATTRIBUTES) { + $this->getAttributes($type)->removeAttribute($name); + return $this; + } + + /** + * Removes a class from an attributes object. + * + * This is a wrapper method to retrieve the correct attributes storage object + * and then remove the class(es) from it. + * + * @param string|array $class + * An individual class or an array of classes to remove. + * @param string $type + * (optional) The type of attributes to use for this method. + * + * @return $this + * + * @see \Drupal\ui_suite_bootstrap\Utility\Attributes::removeClass() + */ + public function removeClass($class, $type = DrupalAttributes::ATTRIBUTES) { + $this->getAttributes($type)->removeClass($class); + return $this; + } + + /** + * Replaces a class in an attributes object. + * + * This is a wrapper method to retrieve the correct attributes storage object + * and then replace the class(es) in it. + * + * @param string $old + * The old class to remove. + * @param string $new + * The new class. It will not be added if the $old class does not exist. + * @param string $type + * (optional) The type of attributes to use for this method. + * + * @return $this + * + * @see \Drupal\ui_suite_bootstrap\Utility\Attributes::replaceClass() + */ + public function replaceClass($old, $new, $type = DrupalAttributes::ATTRIBUTES) { + $this->getAttributes($type)->replaceClass($old, $new); + return $this; + } + + /** + * Sets an attribute on an attributes object. + * + * This is a wrapper method to retrieve the correct attributes storage object + * and then set an attribute on it. + * + * @param string $name + * The name of the attribute to set. + * @param mixed $value + * The value of the attribute to set. + * @param string $type + * (optional) The type of attributes to use for this method. + * + * @return $this + * + * @see \Drupal\ui_suite_bootstrap\Utility\Attributes::setAttribute() + */ + public function setAttribute($name, $value, $type = DrupalAttributes::ATTRIBUTES) { + $this->getAttributes($type)->setAttribute($name, $value); + return $this; + } + + /** + * Sets multiple attributes on an attributes object. + * + * This is a wrapper method to retrieve the correct attributes storage object + * and then merge multiple attributes into it. + * + * @param array $values + * An associative key/value array of attributes to set. + * @param string $type + * (optional) The type of attributes to use for this method. + * + * @return $this + * + * @see \Drupal\ui_suite_bootstrap\Utility\Attributes::setAttributes() + */ + public function setAttributes(array $values, $type = DrupalAttributes::ATTRIBUTES) { + $this->getAttributes($type)->setAttributes($values); + return $this; + } + +} diff --git a/src/Utility/Element.php b/src/Utility/Element.php new file mode 100644 index 0000000000000000000000000000000000000000..d65afd61cb9e6fe2999eb58b8cbd36a08c541b10 --- /dev/null +++ b/src/Utility/Element.php @@ -0,0 +1,574 @@ +<?php + +declare(strict_types = 1); + +namespace Drupal\ui_suite_bootstrap\Utility; + +use Drupal\Component\Render\FormattableMarkup; +use Drupal\Component\Render\MarkupInterface; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Element as CoreElement; + +/** + * Provides helper methods for Drupal render elements. + * + * @see \Drupal\Core\Render\Element + */ +class Element extends DrupalAttributes { + + /** + * The current state of the form. + * + * @var \Drupal\Core\Form\FormStateInterface|null + */ + protected $formState; + + /** + * {@inheritdoc} + */ + protected $attributePrefix = '#'; + + /** + * Element constructor. + * + * @param array|string $element + * A render array element. + * @param \Drupal\Core\Form\FormStateInterface|null $form_state + * The current state of the form. + */ + public function __construct(&$element = [], ?FormStateInterface $form_state = NULL) { + if (!\is_array($element)) { + $element = [ + '#markup' => $element instanceof MarkupInterface ? $element : new FormattableMarkup($element, []), + ]; + } + $this->array = &$element; + $this->formState = $form_state; + } + + /** + * Magic get method. + * + * This is only for child elements, not properties. + * + * @param string $key + * The name of the child element to retrieve. + * + * @throws \InvalidArgumentException + * Throws this error when the name is a property (key starting with #). + * + * @return \Drupal\ui_suite_bootstrap\Utility\Element + * The child element object. + */ + public function &__get($key) { + if (CoreElement::property($key)) { + throw new \InvalidArgumentException('Cannot dynamically retrieve element property. Please use \Drupal\ui_suite_bootstrap\Utility\Element::getProperty instead.'); + } + return new self($this->offsetGet($key, []), $this->formState); + } + + /** + * Magic set method. + * + * This is only for child elements, not properties. + * + * @param string $key + * The name of the child element to set. + * @param mixed $value + * The value of $name to set. + * + * @throws \InvalidArgumentException + * Throws this error when the name is a property (key starting with #). + */ + public function __set($key, $value) { + if (CoreElement::property($key)) { + throw new \InvalidArgumentException('Cannot dynamically retrieve element property. Use \Drupal\ui_suite_bootstrap\Utility\Element::setProperty instead.'); + } + $this->offsetSet($key, $value instanceof Element ? $value->getArray() : $value); + } + + /** + * Magic isset method. + * + * This is only for child elements, not properties. + * + * @param string $name + * The name of the child element to check. + * + * @throws \InvalidArgumentException + * Throws this error when the name is a property (key starting with #). + * + * @return bool + * TRUE or FALSE + */ + public function __isset($name) { + if (CoreElement::property($name)) { + throw new \InvalidArgumentException('Cannot dynamically check if an element has a property. Use \Drupal\ui_suite_bootstrap\Utility\Element::unsetProperty instead.'); + } + return parent::__isset($name); + } + + /** + * Magic unset method. + * + * This is only for child elements, not properties. + * + * @param mixed $name + * The name of the child element to unset. + * + * @throws \InvalidArgumentException + * Throws this error when the name is a property (key starting with #). + */ + public function __unset($name) { + if (CoreElement::property($name)) { + throw new \InvalidArgumentException('Cannot dynamically unset an element property. Use \Drupal\ui_suite_bootstrap\Utility\Element::hasProperty instead.'); + } + parent::__unset($name); + } + + /** + * Sets the #access property on an element. + * + * @param bool|\Drupal\Core\Access\AccessResultInterface $access + * The value to assign to #access. + * + * @return static + */ + public function access($access = NULL) { + return $this->setProperty('access', $access); + } + + /** + * Appends a property with a value. + * + * @param string $name + * The name of the property to set. + * @param mixed $value + * The value of the property to set. + * + * @return static + */ + public function appendProperty($name, $value) { + $property = &$this->getProperty($name); + $value = $value instanceof Element ? $value->getArray() : $value; + + // If property isn't set, just set it. + if (!isset($property)) { + $property = $value; + return $this; + } + + if (\is_array($property)) { + $property[] = Element::create($value)->getArray(); + } + else { + $property .= (string) $value; + } + + return $this; + } + + /** + * Identifies the children of an element array, optionally sorted by weight. + * + * The children of a element array are those key/value pairs whose key does + * not start with a '#'. See drupal_render() for details. + * + * @param bool $sort + * Boolean to indicate whether the children should be sorted by weight. + * + * @return array + * The array keys of the element's children. + * + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + */ + public function childKeys($sort = FALSE) { + return CoreElement::children($this->array, $sort); + } + + /** + * Retrieves the children of an element array, optionally sorted by weight. + * + * The children of a element array are those key/value pairs whose key does + * not start with a '#'. See drupal_render() for details. + * + * @param bool $sort + * Boolean to indicate whether the children should be sorted by weight. + * + * @return \Drupal\ui_suite_bootstrap\Utility\Element[] + * An array child elements. + * + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + */ + public function children($sort = FALSE) { + $children = []; + foreach ($this->childKeys($sort) as $child) { + $children[$child] = new self($this->array[$child]); + } + return $children; + } + + /** + * Creates a new \Drupal\ui_suite_bootstrap\Utility\Element instance. + * + * @param array|string $element + * A render array element or a string. + * @param \Drupal\Core\Form\FormStateInterface|null $form_state + * A current FormState instance, if any. + * + * @return \Drupal\ui_suite_bootstrap\Utility\Element + * The newly created element instance. + */ + public static function create(&$element = [], ?FormStateInterface $form_state = NULL) { + return $element instanceof self ? $element : new self($element, $form_state); + } + + /** + * Traverses the element to find the closest button. + * + * @return \Drupal\ui_suite_bootstrap\Utility\Element|false + * The first button element or FALSE if no button could be found. + */ + public function &findButton() { + $button = FALSE; + foreach ($this->children() as $child) { + if ($child->isButton()) { + $button = $child; + break; + } + $result = &$child->findButton(); + if ($result) { + $button = $result; + break; + } + } + return $button; + } + + /** + * Retrieves the render array for the element. + * + * @return array + * The element render array, passed by reference. + */ + public function &getArray() { + return $this->array; + } + + /** + * Retrieves a context value from the #context element property, if any. + * + * @param string $name + * The name of the context key to retrieve. + * @param mixed $default + * Optional. The default value to use if the context $name isn't set. + * + * @return mixed|null + * The context value or the $default value if not set. + */ + public function &getContext($name, $default = NULL) { + $context = &$this->getProperty('context', []); + if (!isset($context[$name])) { + $context[$name] = $default; + } + return $context[$name]; + } + + /** + * Returns the error message filed against the given form element. + * + * Form errors higher up in the form structure override deeper errors as well + * as errors on the element itself. + * + * @throws \BadMethodCallException + * When the element instance was not constructed with a valid form state + * object. + * + * @return string|null + * Either the error message for this element or NULL if there are no errors. + */ + public function getError() { + if (!$this->formState) { + throw new \BadMethodCallException('The element instance must be constructed with a valid form state object to use this method.'); + } + return $this->formState->getError($this->array); + } + + /** + * Retrieves the render array for the element. + * + * @param string $name + * The name of the element property to retrieve, not including the # prefix. + * @param mixed $default + * The default to set if property does not exist. + * + * @return mixed + * The property value, NULL if not set. + */ + public function &getProperty($name, $default = NULL) { + return $this->offsetGet("#{$name}", $default); + } + + /** + * Returns the visible children of an element. + * + * @return array + * The array keys of the element's visible children. + */ + public function getVisibleChildren() { + return CoreElement::getVisibleChildren($this->array); + } + + /** + * Indicates whether the element has an error set. + * + * @throws \BadMethodCallException + * When the element instance was not constructed with a valid form state + * object. + * + * @return bool + * TRUE if has error. + */ + public function hasError(): bool { + $error = $this->getError(); + return isset($error); + } + + /** + * Indicates whether the element has a specific property. + * + * @param string $name + * The property to check. + * + * @return bool + * TRUE if has property. + */ + public function hasProperty($name): bool { + return $this->offsetExists("#{$name}"); + } + + /** + * Indicates whether the element is a button. + * + * @return bool + * TRUE or FALSE. + */ + public function isButton() { + $button_types = ['button', 'submit', 'reset', 'image_button']; + return !empty($this->array['#is_button']) || $this->isType($button_types) || $this->hasClass('btn'); + } + + /** + * Indicates whether the given element is empty. + * + * An element that only has #cache set is considered empty, because it will + * render to the empty string. + * + * @return bool + * Whether the given element is empty. + */ + public function isEmpty() { + return CoreElement::isEmpty($this->array); + } + + /** + * Indicates whether a property on the element is empty. + * + * @param string $name + * The property to check. + * + * @return bool + * Whether the given property on the element is empty. + */ + public function isPropertyEmpty($name) { + return $this->hasProperty($name) && empty($this->getProperty($name)); + } + + /** + * Checks if a value is a render array. + * + * @param mixed $value + * The value to check. + * + * @return bool + * TRUE if the given value is a render array, otherwise FALSE. + */ + public static function isRenderArray($value) { + return \is_array($value) && (isset($value['#type']) + || isset($value['#theme']) || isset($value['#theme_wrappers']) + || isset($value['#markup']) || isset($value['#attached']) + || isset($value['#cache']) || isset($value['#lazy_builder']) + || isset($value['#create_placeholder']) || isset($value['#pre_render']) + || isset($value['#post_render']) || isset($value['#process'])); + } + + /** + * Checks if the element is a specific type of element. + * + * @param string|array $type + * The element type(s) to check. + * + * @return bool + * TRUE if element is or one of $type. + */ + public function isType($type) { + $property = $this->getProperty('type'); + return $property && \in_array($property, (\is_array($type) ? $type : [$type]), TRUE); + } + + /** + * Determines if an element is visible. + * + * @return bool + * TRUE if the element is visible, otherwise FALSE. + */ + public function isVisible() { + return CoreElement::isVisibleElement($this->array); + } + + /** + * Maps an element's properties to its attributes array. + * + * @param array $map + * An associative array whose keys are element property names and whose + * values are the HTML attribute names to set on the corresponding + * property; e.g., array('#property_name' => 'attribute_name'). If both + * names are identical except for the leading '#', then an attribute name + * value is sufficient and no property name needs to be specified. + * + * @return static + */ + public function map(array $map) { + CoreElement::setAttributes($this->array, $map); + return $this; + } + + /** + * Prepends a property with a value. + * + * @param string $name + * The name of the property to set. + * @param mixed $value + * The value of the property to set. + * + * @return static + */ + public function prependProperty($name, $value) { + $property = &$this->getProperty($name); + $value = $value instanceof Element ? $value->getArray() : $value; + + // If property isn't set, just set it. + if (!isset($property)) { + $property = $value; + return $this; + } + + if (\is_array($property)) { + \array_unshift($property, Element::create($value)->getArray()); + } + else { + $property = (string) $value . (string) $property; + } + + return $this; + } + + /** + * Gets properties of a structured array element (keys beginning with '#'). + * + * @return array + * An array of property keys for the element. + */ + public function properties() { + return CoreElement::properties($this->array); + } + + /** + * Renders the final element HTML. + * + * @return \Drupal\Component\Render\MarkupInterface + * The rendered HTML. + */ + public function render() { + /** @var \Drupal\Core\Render\Renderer $renderer */ + $renderer = \Drupal::service('renderer'); + return $renderer->render($this->array); + } + + /** + * Renders the final element HTML. + * + * @return \Drupal\Component\Render\MarkupInterface + * The rendered HTML. + */ + public function renderPlain() { + /** @var \Drupal\Core\Render\Renderer $renderer */ + $renderer = \Drupal::service('renderer'); + return $renderer->renderPlain($this->array); + } + + /** + * Renders the final element HTML. + * + * (Cannot be executed within another render context.) + * + * @return \Drupal\Component\Render\MarkupInterface + * The rendered HTML. + */ + public function renderRoot() { + /** @var \Drupal\Core\Render\Renderer $renderer */ + $renderer = \Drupal::service('renderer'); + return $renderer->renderRoot($this->array); + } + + /** + * Sets the current form state for the element. + * + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Optional. The current state of the form. + * + * @return static + */ + public function setFormState(FormStateInterface $form_state) { + $this->formState = $form_state; + return $this; + } + + /** + * Sets the value for a property. + * + * @param string $name + * The name of the property to set. + * @param mixed $value + * The value of the property to set. + * @param bool $recurse + * Flag indicating wither to set the same property on child elements. + * + * @return static + * + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + */ + public function setProperty($name, $value, $recurse = FALSE) { + $this->array["#{$name}"] = $value instanceof Element ? $value->getArray() : $value; + if ($recurse) { + foreach ($this->children() as $child) { + $child->setProperty($name, $value, $recurse); + } + } + return $this; + } + + /** + * Removes a property from the element. + * + * @param string $name + * The name of the property to unset. + * + * @return static + */ + public function unsetProperty($name) { + unset($this->array["#{$name}"]); + return $this; + } + +} diff --git a/src/Utility/Variables.php b/src/Utility/Variables.php new file mode 100644 index 0000000000000000000000000000000000000000..19d5053ce8045fa6084005acd90f1227afc5a363 --- /dev/null +++ b/src/Utility/Variables.php @@ -0,0 +1,116 @@ +<?php + +declare(strict_types = 1); + +namespace Drupal\ui_suite_bootstrap\Utility; + +/** + * Class to help modify template variables. + * + * @ingroup utility + */ +class Variables extends DrupalAttributes { + + /** + * An element object. + * + * @var \Drupal\ui_suite_bootstrap\Utility\Element|false + */ + public $element = FALSE; + + /** + * Element constructor. + * + * @param array $variables + * A theme hook variables array. + */ + public function __construct(array &$variables) { + $this->array = &$variables; + if (isset($variables['element']) && Element::isRenderArray($variables['element'])) { + $this->element = Element::create($variables['element']); + } + elseif (isset($variables['elements']) && Element::isRenderArray($variables['elements'])) { + $this->element = Element::create($variables['elements']); + } + } + + /** + * Creates a new \Drupal\ui_suite_bootstrap\Utility\Variables instance. + * + * @param array $variables + * A theme hook variables array. + * + * @return \Drupal\ui_suite_bootstrap\Utility\Variables + * The newly created variables instance. + */ + public static function create(array &$variables) { + return new self($variables); + } + + /** + * Retrieves a context value from the variables array or its element, if any. + * + * @param string $name + * The name of the context key to retrieve. + * @param mixed $default + * Optional. The default value to use if the context $name isn't set. + * + * @return mixed|null + * The context value or the $default value if not set. + */ + public function &getContext($name, $default = NULL) { + $context = &$this->offsetGet($this->attributePrefix . 'context', []); + if (!isset($context[$name])) { + // If there is no context on the variables array but there is an element + // present, proxy the method to the element. + if ($this->element) { + return $this->element->getContext($name, $default); + } + $context[$name] = $default; + } + return $context[$name]; + } + + /** + * Maps an element's properties to the variables attributes array. + * + * @param array $map + * An associative array whose keys are element property names and whose + * values are the variable names to set in the variables array; e.g., + * array('#property_name' => 'variable_name'). If both names are identical + * except for the leading '#', then an attribute name value is sufficient + * and no property name needs to be specified. + * @param bool $overwrite + * If the variable exists, it will be overwritten. This does not apply to + * attribute arrays, they will always be merged recursively. + * + * @return $this + * + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + */ + public function map(array $map, $overwrite = TRUE) { + // Immediately return if there is no element in the variable array. + if (!$this->element) { + return $this; + } + + // Iterate over each map item. + foreach ($map as $property => $variable) { + // If the key is numeric, the attribute name needs to be taken over. + if (\is_int($property)) { + $property = $variable; + } + + // Merge attributes from the element. + if (\strpos($property, 'attributes') !== FALSE) { + $this->setAttributes($this->element->getAttributes($property)->getArrayCopy(), $variable); + } + // Set normal variable. + elseif ($overwrite || !$this->offsetExists($variable)) { + $this->offsetSet($variable, $this->element->getProperty($property)); + } + } + return $this; + } + +}