3 namespace Drupal\bootstrap\Utility;
5 use Drupal\bootstrap\Bootstrap;
6 use Drupal\Component\Render\FormattableMarkup;
7 use Drupal\Component\Render\MarkupInterface;
8 use Drupal\Component\Utility\Xss;
9 use Drupal\Core\Form\FormStateInterface;
10 use Drupal\Core\Render\Element as CoreElement;
13 * Provides helper methods for Drupal render elements.
17 * @see \Drupal\Core\Render\Element
19 class Element extends DrupalAttributes {
22 * The current state of the form.
24 * @var \Drupal\Core\Form\FormStateInterface
33 protected $type = FALSE;
38 protected $attributePrefix = '#';
41 * Element constructor.
43 * @param array|string $element
44 * A render array element.
45 * @param \Drupal\Core\Form\FormStateInterface $form_state
46 * The current state of the form.
48 public function __construct(&$element = [], FormStateInterface $form_state = NULL) {
49 if (!is_array($element)) {
50 $element = ['#markup' => $element instanceof MarkupInterface ? $element : new FormattableMarkup($element, [])];
52 $this->array = &$element;
53 $this->formState = $form_state;
59 * This is only for child elements, not properties.
62 * The name of the child element to retrieve.
64 * @return \Drupal\bootstrap\Utility\Element
65 * The child element object.
67 * @throws \InvalidArgumentException
68 * Throws this error when the name is a property (key starting with #).
70 public function &__get($key) {
71 if (CoreElement::property($key)) {
72 throw new \InvalidArgumentException('Cannot dynamically retrieve element property. Please use \Drupal\bootstrap\Utility\Element::getProperty instead.');
74 $instance = new self($this->offsetGet($key, []));
81 * This is only for child elements, not properties.
84 * The name of the child element to set.
86 * The value of $name to set.
88 * @throws \InvalidArgumentException
89 * Throws this error when the name is a property (key starting with #).
91 public function __set($key, $value) {
92 if (CoreElement::property($key)) {
93 throw new \InvalidArgumentException('Cannot dynamically retrieve element property. Use \Drupal\bootstrap\Utility\Element::setProperty instead.');
95 $this->offsetSet($key, ($value instanceof Element ? $value->getArray() : $value));
101 * This is only for child elements, not properties.
103 * @param string $name
104 * The name of the child element to check.
109 * @throws \InvalidArgumentException
110 * Throws this error when the name is a property (key starting with #).
112 public function __isset($name) {
113 if (CoreElement::property($name)) {
114 throw new \InvalidArgumentException('Cannot dynamically check if an element has a property. Use \Drupal\bootstrap\Utility\Element::unsetProperty instead.');
116 return parent::__isset($name);
120 * Magic unset method.
122 * This is only for child elements, not properties.
125 * The name of the child element to unset.
127 * @throws \InvalidArgumentException
128 * Throws this error when the name is a property (key starting with #).
130 public function __unset($name) {
131 if (CoreElement::property($name)) {
132 throw new \InvalidArgumentException('Cannot dynamically unset an element property. Use \Drupal\bootstrap\Utility\Element::hasProperty instead.');
134 parent::__unset($name);
138 * Appends a property with a value.
140 * @param string $name
141 * The name of the property to set.
142 * @param mixed $value
143 * The value of the property to set.
147 public function appendProperty($name, $value) {
148 $property = &$this->getProperty($name);
149 $value = $value instanceof Element ? $value->getArray() : $value;
151 // If property isn't set, just set it.
152 if (!isset($property)) {
157 if (is_array($property)) {
158 $property[] = Element::create($value)->getArray();
161 $property .= (string) $value;
168 * Identifies the children of an element array, optionally sorted by weight.
170 * The children of a element array are those key/value pairs whose key does
171 * not start with a '#'. See drupal_render() for details.
174 * Boolean to indicate whether the children should be sorted by weight.
177 * The array keys of the element's children.
179 public function childKeys($sort = FALSE) {
180 return CoreElement::children($this->array, $sort);
184 * Retrieves the children of an element array, optionally sorted by weight.
186 * The children of a element array are those key/value pairs whose key does
187 * not start with a '#'. See drupal_render() for details.
190 * Boolean to indicate whether the children should be sorted by weight.
192 * @return \Drupal\bootstrap\Utility\Element[]
193 * An array child elements.
195 public function children($sort = FALSE) {
197 foreach ($this->childKeys($sort) as $child) {
198 $children[$child] = new self($this->array[$child]);
204 * Adds a specific Bootstrap class to color a button based on its text value.
206 * @param bool $override
207 * Flag determining whether or not to override any existing set class.
211 public function colorize($override = TRUE) {
212 $button = $this->isButton();
214 // @todo refactor this more so it's not just "button" specific.
215 $prefix = $button ? 'btn' : 'has';
217 // List of classes, based on the prefix.
219 "$prefix-primary", "$prefix-success", "$prefix-info",
220 "$prefix-warning", "$prefix-danger", "$prefix-link",
221 // Default should be last.
225 // Set the class to "btn-default" if it shouldn't be colorized.
226 $class = $button && !Bootstrap::getTheme()->getSetting('button_colorize') ? 'btn-default' : FALSE;
228 // Search for an existing class.
229 if (!$class || !$override) {
230 foreach ($classes as $value) {
231 if ($this->hasClass($value)) {
238 // Find a class based on the value of "value", "title" or "button_type".
240 $value = $this->getProperty('value', $this->getProperty('title', ''));
241 $class = "$prefix-" . Bootstrap::cssClassFromString($value, $button ? $this->getProperty('button_type', 'default') : 'default');
244 // Remove any existing classes and add the specified class.
246 $this->removeClass($classes)->addClass($class);
247 if ($button && $this->getProperty('split')) {
248 $this->removeClass($classes, $this::SPLIT_BUTTON)->addClass($class, $this::SPLIT_BUTTON);
256 * Creates a new \Drupal\bootstrap\Utility\Element instance.
258 * @param array|string $element
259 * A render array element or a string.
260 * @param \Drupal\Core\Form\FormStateInterface $form_state
261 * A current FormState instance, if any.
263 * @return \Drupal\bootstrap\Utility\Element
264 * The newly created element instance.
266 public static function create(&$element = [], FormStateInterface $form_state = NULL) {
267 return $element instanceof self ? $element : new self($element, $form_state);
271 * Creates a new standalone \Drupal\bootstrap\Utility\Element instance.
273 * It does not reference the original element passed. If an Element instance
274 * is passed, it will clone it so it doesn't affect the original element.
276 * @param array|string|\Drupal\bootstrap\Utility\Element $element
277 * A render array element, string or Element instance.
278 * @param \Drupal\Core\Form\FormStateInterface $form_state
279 * A current FormState instance, if any.
281 * @return \Drupal\bootstrap\Utility\Element
282 * The newly created element instance.
284 public static function createStandalone($element = [], FormStateInterface $form_state = NULL) {
285 // Immediately return a cloned version if element is already an Element.
286 if ($element instanceof self) {
287 return clone $element;
289 $standalone = is_object($element) ? clone $element : $element;
290 return static::create($standalone, $form_state);
296 public function exchangeArray($data) {
297 $old = parent::exchangeArray($data);
302 * Traverses the element to find the closest button.
304 * @return \Drupal\bootstrap\Utility\Element|false
305 * The first button element or FALSE if no button could be found.
307 public function &findButton() {
309 foreach ($this->children() as $child) {
310 if ($child->isButton()) {
314 if ($result = &$child->findButton()) {
323 * Retrieves the render array for the element.
326 * The element render array, passed by reference.
328 public function &getArray() {
333 * Retrieves a context value from the #context element property, if any.
335 * @param string $name
336 * The name of the context key to retrieve.
337 * @param mixed $default
338 * Optional. The default value to use if the context $name isn't set.
341 * The context value or the $default value if not set.
343 public function &getContext($name, $default = NULL) {
344 $context = &$this->getProperty('context', []);
345 if (!isset($context[$name])) {
346 $context[$name] = $default;
348 return $context[$name];
352 * Returns the error message filed against the given form element.
354 * Form errors higher up in the form structure override deeper errors as well
355 * as errors on the element itself.
357 * @return string|null
358 * Either the error message for this element or NULL if there are no errors.
360 * @throws \BadMethodCallException
361 * When the element instance was not constructed with a valid form state
364 public function getError() {
365 if (!$this->formState) {
366 throw new \BadMethodCallException('The element instance must be constructed with a valid form state object to use this method.');
368 return $this->formState->getError($this->array);
372 * Retrieves the render array for the element.
374 * @param string $name
375 * The name of the element property to retrieve, not including the # prefix.
376 * @param mixed $default
377 * The default to set if property does not exist.
380 * The property value, NULL if not set.
382 public function &getProperty($name, $default = NULL) {
383 return $this->offsetGet("#$name", $default);
387 * Returns the visible children of an element.
390 * The array keys of the element's visible children.
392 public function getVisibleChildren() {
393 return CoreElement::getVisibleChildren($this->array);
397 * Indicates whether the element has an error set.
399 * @throws \BadMethodCallException
400 * When the element instance was not constructed with a valid form state
403 public function hasError() {
404 $error = $this->getError();
405 return isset($error);
409 * Indicates whether the element has a specific property.
411 * @param string $name
412 * The property to check.
414 public function hasProperty($name) {
415 return $this->offsetExists("#$name");
419 * Indicates whether the element is a button.
424 public function isButton() {
425 $button_types = ['button', 'submit', 'reset', 'image_button'];
426 return !empty($this->array['#is_button']) || $this->isType($button_types) || $this->hasClass('btn');
430 * Indicates whether the given element is empty.
432 * An element that only has #cache set is considered empty, because it will
433 * render to the empty string.
436 * Whether the given element is empty.
438 public function isEmpty() {
439 return CoreElement::isEmpty($this->array);
443 * Indicates whether a property on the element is empty.
445 * @param string $name
446 * The property to check.
449 * Whether the given property on the element is empty.
451 public function isPropertyEmpty($name) {
452 return $this->hasProperty($name) && empty($this->getProperty($name));
456 * Checks if a value is a render array.
458 * @param mixed $value
459 * The value to check.
462 * TRUE if the given value is a render array, otherwise FALSE.
464 public static function isRenderArray($value) {
465 return is_array($value) && (isset($value['#type']) ||
466 isset($value['#theme']) || isset($value['#theme_wrappers']) ||
467 isset($value['#markup']) || isset($value['#attached']) ||
468 isset($value['#cache']) || isset($value['#lazy_builder']) ||
469 isset($value['#create_placeholder']) || isset($value['#pre_render']) ||
470 isset($value['#post_render']) || isset($value['#process']));
474 * Checks if the element is a specific type of element.
476 * @param string|array $type
477 * The element type(s) to check.
480 * TRUE if element is or one of $type.
482 public function isType($type) {
483 $property = $this->getProperty('type');
484 return $property && in_array($property, (is_array($type) ? $type : [$type]));
488 * Determines if an element is visible.
491 * TRUE if the element is visible, otherwise FALSE.
493 public function isVisible() {
494 return CoreElement::isVisibleElement($this->array);
498 * Maps an element's properties to its attributes array.
501 * An associative array whose keys are element property names and whose
502 * values are the HTML attribute names to set on the corresponding
503 * property; e.g., array('#propertyname' => 'attributename'). If both names
504 * are identical except for the leading '#', then an attribute name value is
505 * sufficient and no property name needs to be specified.
509 public function map(array $map) {
510 CoreElement::setAttributes($this->array, $map);
515 * Prepends a property with a value.
517 * @param string $name
518 * The name of the property to set.
519 * @param mixed $value
520 * The value of the property to set.
524 public function prependProperty($name, $value) {
525 $property = &$this->getProperty($name);
526 $value = $value instanceof Element ? $value->getArray() : $value;
528 // If property isn't set, just set it.
529 if (!isset($property)) {
534 if (is_array($property)) {
535 array_unshift($property, Element::create($value)->getArray());
538 $property = (string) $value . (string) $property;
545 * Gets properties of a structured array element (keys beginning with '#').
548 * An array of property keys for the element.
550 public function properties() {
551 return CoreElement::properties($this->array);
555 * Renders the final element HTML.
557 * @return \Drupal\Component\Render\MarkupInterface
560 public function render() {
561 /** @var \Drupal\Core\Render\Renderer $renderer */
562 $renderer = \Drupal::service('renderer');
563 return $renderer->render($this->array);
567 * Renders the final element HTML.
569 * @return \Drupal\Component\Render\MarkupInterface
572 public function renderPlain() {
573 /** @var \Drupal\Core\Render\Renderer $renderer */
574 $renderer = \Drupal::service('renderer');
575 return $renderer->renderPlain($this->array);
579 * Renders the final element HTML.
581 * (Cannot be executed within another render context.)
583 * @return \Drupal\Component\Render\MarkupInterface
586 public function renderRoot() {
587 /** @var \Drupal\Core\Render\Renderer $renderer */
588 $renderer = \Drupal::service('renderer');
589 return $renderer->renderRoot($this->array);
593 * Adds Bootstrap button size class to the element.
595 * @param string $class
596 * The full button size class to add. If none is provided, it will default
597 * to any set theme setting.
598 * @param bool $override
599 * Flag indicating if the passed $class should be forcibly set. Setting
600 * this to FALSE allows any existing set class to persist.
604 public function setButtonSize($class = NULL, $override = TRUE) {
605 // Immediately return if element is not a button.
606 if (!$this->isButton()) {
610 // Retrieve the button size classes from the specific setting's options.
612 if (!isset($classes)) {
614 if ($button_size = Bootstrap::getTheme()->getSettingPlugin('button_size')) {
615 $classes = array_keys($button_size->getOptions());
619 // Search for an existing class.
620 if (!$class || !$override) {
621 foreach ($classes as $value) {
622 if ($this->hasClass($value)) {
629 // Attempt to get the default button size, if set.
631 $class = Bootstrap::getTheme()->getSetting('button_size');
634 // Remove any existing classes and add the specified class.
636 $this->removeClass($classes)->addClass($class);
637 if ($this->getProperty('split')) {
638 $this->removeClass($classes, $this::SPLIT_BUTTON)->addClass($class, $this::SPLIT_BUTTON);
646 * Flags an element as having an error.
648 * @param string $message
649 * (optional) The error message to present to the user.
653 * @throws \BadMethodCallException
654 * When the element instance was not constructed with a valid form state
657 public function setError($message = '') {
658 if (!$this->formState) {
659 throw new \BadMethodCallException('The element instance must be constructed with a valid form state object to use this method.');
661 $this->formState->setError($this->array, $message);
666 * Adds an icon to button element based on its text value.
669 * An icon render array.
673 * @see \Drupal\bootstrap\Bootstrap::glyphicon()
675 public function setIcon(array $icon = NULL) {
676 if ($this->isButton() && !Bootstrap::getTheme()->getSetting('button_iconize')) {
679 if ($value = $this->getProperty('value', $this->getProperty('title'))) {
680 $icon = isset($icon) ? $icon : Bootstrap::glyphiconFromString($value);
681 $this->setProperty('icon', $icon);
687 * Sets the value for a property.
689 * @param string $name
690 * The name of the property to set.
691 * @param mixed $value
692 * The value of the property to set.
693 * @param bool $recurse
694 * Flag indicating wither to set the same property on child elements.
698 public function setProperty($name, $value, $recurse = FALSE) {
699 $this->array["#$name"] = $value instanceof Element ? $value->getArray() : $value;
701 foreach ($this->children() as $child) {
702 $child->setProperty($name, $value, $recurse);
709 * Converts an element description into a tooltip based on certain criteria.
711 * @param array|\Drupal\bootstrap\Utility\Element|null $target_element
712 * The target element render array the tooltip is to be attached to, passed
713 * by reference or an existing Element object. If not set, it will default
714 * this Element instance.
715 * @param bool $input_only
716 * Toggle determining whether or not to only convert input elements.
718 * The length of characters to determine if description is "simple".
722 public function smartDescription(&$target_element = NULL, $input_only = TRUE, $length = NULL) {
724 if (!isset($theme)) {
725 $theme = Bootstrap::getTheme();
728 // Determine if tooltips are enabled.
730 if (!isset($enabled)) {
731 $enabled = $theme->getSetting('tooltip_enabled') && $theme->getSetting('forms_smart_descriptions');
734 // Immediately return if tooltip descriptions are not enabled.
739 // Allow a different element to attach the tooltip.
740 /** @var \Drupal\bootstrap\Utility\Element $target */
741 if (is_object($target_element) && $target_element instanceof self) {
742 $target = $target_element;
744 elseif (isset($target_element) && is_array($target_element)) {
745 $target = new self($target_element, $this->formState);
751 // For "password_confirm" element types, move the target to the first
753 if ($target->isType('password_confirm')) {
754 $target = $target->pass1;
757 // Retrieve the length limit for smart descriptions.
758 if (!isset($length)) {
759 // Disable length checking by setting it to FALSE if empty.
760 $length = (int) $theme->getSetting('forms_smart_descriptions_limit') ?: FALSE;
763 // Retrieve the allowed tags for smart descriptions. This is primarily used
764 // for display purposes only (i.e. non-UI/UX related elements that wouldn't
765 // require a user to "click", like a link). Disable length checking by
766 // setting it to FALSE if empty.
767 static $allowed_tags;
768 if (!isset($allowed_tags)) {
769 $allowed_tags = array_filter(array_unique(array_map('trim', explode(',', $theme->getSetting('forms_smart_descriptions_allowed_tags') . '')))) ?: FALSE;
772 // Return if element or target shouldn't have "simple" tooltip descriptions.
775 // If the description is a render array, it must first be pre-rendered so
776 // it can be later passed to Unicode::isSimple() if needed.
777 $description = $this->hasProperty('description') ? $this->getProperty('description') : FALSE;
778 if (static::isRenderArray($description)) {
779 $description = static::createStandalone($description)->renderPlain();
783 // Ignore if element has no #description.
786 // Ignore if description is not a simple string or MarkupInterface.
787 || (!is_string($description) && !($description instanceof MarkupInterface))
789 // Ignore if element is not an input.
790 || ($input_only && !$target->hasProperty('input'))
792 // Ignore if the target element already has a "data-toggle" attribute set.
793 || $target->hasAttribute('data-toggle')
795 // Ignore if the target element is #disabled.
796 || $target->hasProperty('disabled')
798 // Ignore if either the actual element or target element has an explicit
799 // #smart_description property set to FALSE.
800 || !$this->getProperty('smart_description', TRUE)
801 || !$target->getProperty('smart_description', TRUE)
803 // Ignore if the description is not "simple".
804 || !Unicode::isSimple($description, $length, $allowed_tags, $html)
806 // Set the both the actual element and the target element
807 // #smart_description property to FALSE.
808 $this->setProperty('smart_description', FALSE);
809 $target->setProperty('smart_description', FALSE);
813 // Default attributes type.
814 $type = DrupalAttributes::ATTRIBUTES;
816 // Use #label_attributes for 'checkbox' and 'radio' elements.
817 if ($this->isType(['checkbox', 'radio'])) {
818 $type = DrupalAttributes::LABEL;
820 // Use #wrapper_attributes for 'checkboxes' and 'radios' elements.
821 elseif ($this->isType(['checkboxes', 'radios'])) {
822 $type = DrupalAttributes::WRAPPER;
825 // Retrieve the proper attributes array.
826 $attributes = $target->getAttributes($type);
828 // Set the tooltip attributes.
829 $attributes['title'] = $allowed_tags !== FALSE ? Xss::filter((string) $description, $allowed_tags) : $description;
830 $attributes['data-toggle'] = 'tooltip';
831 if ($html || $allowed_tags === FALSE) {
832 $attributes['data-html'] = 'true';
835 // Remove the element description so it isn't (re-)rendered later.
836 $this->unsetProperty('description');
842 * Removes a property from the element.
844 * @param string $name
845 * The name of the property to unset.
849 public function unsetProperty($name) {
850 unset($this->array["#$name"]);