4 * Contains \Drupal\bootstrap\Utility\Element.
7 namespace Drupal\bootstrap\Utility;
9 use Drupal\bootstrap\Bootstrap;
10 use Drupal\Component\Render\FormattableMarkup;
11 use Drupal\Component\Render\MarkupInterface;
12 use Drupal\Component\Utility\Xss;
13 use Drupal\Core\Form\FormStateInterface;
16 * Provides helper methods for Drupal render elements.
20 * @see \Drupal\Core\Render\Element
22 class Element extends DrupalAttributes {
25 * The current state of the form.
27 * @var \Drupal\Core\Form\FormStateInterface
36 protected $type = FALSE;
41 protected $attributePrefix = '#';
44 * Element constructor.
46 * @param array|string $element
47 * A render array element.
48 * @param \Drupal\Core\Form\FormStateInterface $form_state
49 * The current state of the form.
51 public function __construct(&$element = [], FormStateInterface $form_state = NULL) {
52 if (!is_array($element)) {
53 $element = ['#markup' => $element instanceof MarkupInterface ? $element : new FormattableMarkup($element, [])];
55 $this->array = &$element;
56 $this->formState = $form_state;
62 * This is only for child elements, not properties.
65 * The name of the child element to retrieve.
67 * @return \Drupal\bootstrap\Utility\Element
68 * The child element object.
70 * @throws \InvalidArgumentException
71 * Throws this error when the name is a property (key starting with #).
73 public function &__get($key) {
74 if (\Drupal\Core\Render\Element::property($key)) {
75 throw new \InvalidArgumentException('Cannot dynamically retrieve element property. Please use \Drupal\bootstrap\Utility\Element::getProperty instead.');
77 $instance = new self($this->offsetGet($key, []));
84 * This is only for child elements, not properties.
87 * The name of the child element to set.
89 * The value of $name to set.
91 * @throws \InvalidArgumentException
92 * Throws this error when the name is a property (key starting with #).
94 public function __set($key, $value) {
95 if (\Drupal\Core\Render\Element::property($key)) {
96 throw new \InvalidArgumentException('Cannot dynamically retrieve element property. Use \Drupal\bootstrap\Utility\Element::setProperty instead.');
98 $this->offsetSet($key, ($value instanceof Element ? $value->getArray() : $value));
102 * Magic isset method.
104 * This is only for child elements, not properties.
106 * @param string $name
107 * The name of the child element to check.
112 * @throws \InvalidArgumentException
113 * Throws this error when the name is a property (key starting with #).
115 public function __isset($name) {
116 if (\Drupal\Core\Render\Element::property($name)) {
117 throw new \InvalidArgumentException('Cannot dynamically check if an element has a property. Use \Drupal\bootstrap\Utility\Element::unsetProperty instead.');
119 return parent::__isset($name);
123 * Magic unset method.
125 * This is only for child elements, not properties.
128 * The name of the child element to unset.
130 * @throws \InvalidArgumentException
131 * Throws this error when the name is a property (key starting with #).
133 public function __unset($name) {
134 if (\Drupal\Core\Render\Element::property($name)) {
135 throw new \InvalidArgumentException('Cannot dynamically unset an element property. Use \Drupal\bootstrap\Utility\Element::hasProperty instead.');
137 parent::__unset($name);
141 * Appends a property with a value.
143 * @param string $name
144 * The name of the property to set.
145 * @param mixed $value
146 * The value of the property to set.
150 public function appendProperty($name, $value) {
151 $property = &$this->getProperty($name);
152 $value = $value instanceof Element ? $value->getArray() : $value;
154 // If property isn't set, just set it.
155 if (!isset($property)) {
160 if (is_array($property)) {
161 $property[] = Element::create($value)->getArray();
164 $property .= (string) $value;
171 * Identifies the children of an element array, optionally sorted by weight.
173 * The children of a element array are those key/value pairs whose key does
174 * not start with a '#'. See drupal_render() for details.
177 * Boolean to indicate whether the children should be sorted by weight.
180 * The array keys of the element's children.
182 public function childKeys($sort = FALSE) {
183 return \Drupal\Core\Render\Element::children($this->array, $sort);
187 * Retrieves the children of an element array, optionally sorted by weight.
189 * The children of a element array are those key/value pairs whose key does
190 * not start with a '#'. See drupal_render() for details.
193 * Boolean to indicate whether the children should be sorted by weight.
195 * @return \Drupal\bootstrap\Utility\Element[]
196 * An array child elements.
198 public function children($sort = FALSE) {
200 foreach ($this->childKeys($sort) as $child) {
201 $children[$child] = new self($this->array[$child]);
207 * Adds a specific Bootstrap class to color a button based on its text value.
209 * @param bool $override
210 * Flag determining whether or not to override any existing set class.
214 public function colorize($override = TRUE) {
215 $button = $this->isButton();
217 // @todo refactor this more so it's not just "button" specific.
218 $prefix = $button ? 'btn' : 'has';
220 // List of classes, based on the prefix.
222 "$prefix-primary", "$prefix-success", "$prefix-info",
223 "$prefix-warning", "$prefix-danger", "$prefix-link",
224 // Default should be last.
228 // Set the class to "btn-default" if it shouldn't be colorized.
229 $class = $button && !Bootstrap::getTheme()->getSetting('button_colorize') ? 'btn-default' : FALSE;
231 // Search for an existing class.
232 if (!$class || !$override) {
233 foreach ($classes as $value) {
234 if ($this->hasClass($value)) {
241 // Find a class based on the value of "value", "title" or "button_type".
243 $value = $this->getProperty('value', $this->getProperty('title', ''));
244 $class = "$prefix-" . Bootstrap::cssClassFromString($value, $button ? $this->getProperty('button_type', 'default') : 'default');
247 // Remove any existing classes and add the specified class.
249 $this->removeClass($classes)->addClass($class);
250 if ($button && $this->getProperty('split')) {
251 $this->removeClass($classes, $this::SPLIT_BUTTON)->addClass($class, $this::SPLIT_BUTTON);
259 * Creates a new \Drupal\bootstrap\Utility\Element instance.
261 * @param array|string $element
262 * A render array element or a string.
263 * @param \Drupal\Core\Form\FormStateInterface $form_state
264 * A current FormState instance, if any.
266 * @return \Drupal\bootstrap\Utility\Element
267 * The newly created element instance.
269 public static function create(&$element = [], FormStateInterface $form_state = NULL) {
270 return $element instanceof self ? $element : new self($element, $form_state);
274 * Creates a new standalone \Drupal\bootstrap\Utility\Element instance.
276 * It does not reference the original element passed. If an Element instance
277 * is passed, it will clone it so it doesn't affect the original element.
279 * @param array|string|\Drupal\bootstrap\Utility\Element $element
280 * A render array element, string or Element instance.
281 * @param \Drupal\Core\Form\FormStateInterface $form_state
282 * A current FormState instance, if any.
284 * @return \Drupal\bootstrap\Utility\Element
285 * The newly created element instance.
287 public static function createStandalone($element = [], FormStateInterface $form_state = NULL) {
288 // Immediately return a cloned version if element is already an Element.
289 if ($element instanceof self) {
290 return clone $element;
292 $standalone = is_object($element) ? clone $element : $element;
293 return static::create($standalone, $form_state);
299 public function exchangeArray($data) {
300 $old = parent::exchangeArray($data);
305 * Retrieves the render array for the element.
308 * The element render array, passed by reference.
310 public function &getArray() {
315 * Retrieves a context value from the #context element property, if any.
317 * @param string $name
318 * The name of the context key to retrieve.
319 * @param mixed $default
320 * Optional. The default value to use if the context $name isn't set.
323 * The context value or the $default value if not set.
325 public function &getContext($name, $default = NULL) {
326 $context = &$this->getProperty('context', []);
327 if (!isset($context[$name])) {
328 $context[$name] = $default;
330 return $context[$name];
334 * Returns the error message filed against the given form element.
336 * Form errors higher up in the form structure override deeper errors as well
337 * as errors on the element itself.
339 * @return string|null
340 * Either the error message for this element or NULL if there are no errors.
342 * @throws \BadMethodCallException
343 * When the element instance was not constructed with a valid form state
346 public function getError() {
347 if (!$this->formState) {
348 throw new \BadMethodCallException('The element instance must be constructed with a valid form state object to use this method.');
350 return $this->formState->getError($this->array);
354 * Retrieves the render array for the element.
356 * @param string $name
357 * The name of the element property to retrieve, not including the # prefix.
358 * @param mixed $default
359 * The default to set if property does not exist.
362 * The property value, NULL if not set.
364 public function &getProperty($name, $default = NULL) {
365 return $this->offsetGet("#$name", $default);
369 * Returns the visible children of an element.
372 * The array keys of the element's visible children.
374 public function getVisibleChildren() {
375 return \Drupal\Core\Render\Element::getVisibleChildren($this->array);
379 * Indicates whether the element has an error set.
381 * @throws \BadMethodCallException
382 * When the element instance was not constructed with a valid form state
385 public function hasError() {
386 $error = $this->getError();
387 return isset($error);
391 * Indicates whether the element has a specific property.
393 * @param string $name
394 * The property to check.
396 public function hasProperty($name) {
397 return $this->offsetExists("#$name");
401 * Indicates whether the element is a button.
406 public function isButton() {
407 return !empty($this->array['#is_button']) || $this->isType(['button', 'submit', 'reset', 'image_button']) || $this->hasClass('btn');
411 * Indicates whether the given element is empty.
413 * An element that only has #cache set is considered empty, because it will
414 * render to the empty string.
417 * Whether the given element is empty.
419 public function isEmpty() {
420 return \Drupal\Core\Render\Element::isEmpty($this->array);
424 * Indicates whether a property on the element is empty.
426 * @param string $name
427 * The property to check.
430 * Whether the given property on the element is empty.
432 public function isPropertyEmpty($name) {
433 return $this->hasProperty($name) && empty($this->getProperty($name));
437 * Checks if a value is a render array.
439 * @param mixed $value
440 * The value to check.
443 * TRUE if the given value is a render array, otherwise FALSE.
445 public static function isRenderArray($value) {
446 return is_array($value) && (isset($value['#type']) ||
447 isset($value['#theme']) || isset($value['#theme_wrappers']) ||
448 isset($value['#markup']) || isset($value['#attached']) ||
449 isset($value['#cache']) || isset($value['#lazy_builder']) ||
450 isset($value['#create_placeholder']) || isset($value['#pre_render']) ||
451 isset($value['#post_render']) || isset($value['#process']));
455 * Checks if the element is a specific type of element.
457 * @param string|array $type
458 * The element type(s) to check.
461 * TRUE if element is or one of $type.
463 public function isType($type) {
464 $property = $this->getProperty('type');
465 return $property && in_array($property, (is_array($type) ? $type : [$type]));
469 * Determines if an element is visible.
472 * TRUE if the element is visible, otherwise FALSE.
474 public function isVisible() {
475 return \Drupal\Core\Render\Element::isVisibleElement($this->array);
479 * Maps an element's properties to its attributes array.
482 * An associative array whose keys are element property names and whose
483 * values are the HTML attribute names to set on the corresponding
484 * property; e.g., array('#propertyname' => 'attributename'). If both names
485 * are identical except for the leading '#', then an attribute name value is
486 * sufficient and no property name needs to be specified.
490 public function map(array $map) {
491 \Drupal\Core\Render\Element::setAttributes($this->array, $map);
496 * Prepends a property with a value.
498 * @param string $name
499 * The name of the property to set.
500 * @param mixed $value
501 * The value of the property to set.
505 public function prependProperty($name, $value) {
506 $property = &$this->getProperty($name);
507 $value = $value instanceof Element ? $value->getArray() : $value;
509 // If property isn't set, just set it.
510 if (!isset($property)) {
515 if (is_array($property)) {
516 array_unshift($property, Element::create($value)->getArray());
519 $property = (string) $value . (string) $property;
526 * Gets properties of a structured array element (keys beginning with '#').
529 * An array of property keys for the element.
531 public function properties() {
532 return \Drupal\Core\Render\Element::properties($this->array);
536 * Renders the final element HTML.
538 * @return \Drupal\Component\Render\MarkupInterface
541 public function render() {
542 /** @var \Drupal\Core\Render\Renderer $renderer */
543 $renderer = \Drupal::service('renderer');
544 return $renderer->render($this->array);
548 * Renders the final element HTML.
550 * @return \Drupal\Component\Render\MarkupInterface
553 public function renderPlain() {
554 /** @var \Drupal\Core\Render\Renderer $renderer */
555 $renderer = \Drupal::service('renderer');
556 return $renderer->renderPlain($this->array);
560 * Renders the final element HTML.
562 * (Cannot be executed within another render context.)
564 * @return \Drupal\Component\Render\MarkupInterface
567 public function renderRoot() {
568 /** @var \Drupal\Core\Render\Renderer $renderer */
569 $renderer = \Drupal::service('renderer');
570 return $renderer->renderRoot($this->array);
574 * Adds Bootstrap button size class to the element.
576 * @param string $class
577 * The full button size class to add. If none is provided, it will default
578 * to any set theme setting.
579 * @param bool $override
580 * Flag indicating if the passed $class should be forcibly set. Setting
581 * this to FALSE allows any existing set class to persist.
585 public function setButtonSize($class = NULL, $override = TRUE) {
586 // Immediately return if element is not a button.
587 if (!$this->isButton()) {
591 // Retrieve the button size classes from the specific setting's options.
593 if (!isset($classes)) {
595 if ($button_size = Bootstrap::getTheme()->getSettingPlugin('button_size')) {
596 $classes = array_keys($button_size->getOptions());
600 // Search for an existing class.
601 if (!$class || !$override) {
602 foreach ($classes as $value) {
603 if ($this->hasClass($value)) {
610 // Attempt to get the default button size, if set.
612 $class = Bootstrap::getTheme()->getSetting('button_size');
615 // Remove any existing classes and add the specified class.
617 $this->removeClass($classes)->addClass($class);
618 if ($this->getProperty('split')) {
619 $this->removeClass($classes, $this::SPLIT_BUTTON)->addClass($class, $this::SPLIT_BUTTON);
627 * Flags an element as having an error.
629 * @param string $message
630 * (optional) The error message to present to the user.
634 * @throws \BadMethodCallException
635 * When the element instance was not constructed with a valid form state
638 public function setError($message = '') {
639 if (!$this->formState) {
640 throw new \BadMethodCallException('The element instance must be constructed with a valid form state object to use this method.');
642 $this->formState->setError($this->array, $message);
647 * Adds an icon to button element based on its text value.
650 * An icon render array.
654 * @see \Drupal\bootstrap\Bootstrap::glyphicon()
656 public function setIcon(array $icon = NULL) {
657 if ($this->isButton() && !Bootstrap::getTheme()->getSetting('button_iconize')) {
660 if ($value = $this->getProperty('value', $this->getProperty('title'))) {
661 $icon = isset($icon) ? $icon : Bootstrap::glyphiconFromString($value);
662 $this->setProperty('icon', $icon);
668 * Sets the value for a property.
670 * @param string $name
671 * The name of the property to set.
672 * @param mixed $value
673 * The value of the property to set.
677 public function setProperty($name, $value) {
678 $this->array["#$name"] = $value instanceof Element ? $value->getArray() : $value;
683 * Converts an element description into a tooltip based on certain criteria.
685 * @param array|\Drupal\bootstrap\Utility\Element|NULL $target_element
686 * The target element render array the tooltip is to be attached to, passed
687 * by reference or an existing Element object. If not set, it will default
688 * this Element instance.
689 * @param bool $input_only
690 * Toggle determining whether or not to only convert input elements.
692 * The length of characters to determine if description is "simple".
696 public function smartDescription(&$target_element = NULL, $input_only = TRUE, $length = NULL) {
698 if (!isset($theme)) {
699 $theme = Bootstrap::getTheme();
702 // Determine if tooltips are enabled.
704 if (!isset($enabled)) {
705 $enabled = $theme->getSetting('tooltip_enabled') && $theme->getSetting('forms_smart_descriptions');
708 // Immediately return if tooltip descriptions are not enabled.
713 // Allow a different element to attach the tooltip.
714 /** @var Element $target */
715 if (is_object($target_element) && $target_element instanceof self) {
716 $target = $target_element;
718 elseif (isset($target_element) && is_array($target_element)) {
719 $target = new self($target_element, $this->formState);
725 // For "password_confirm" element types, move the target to the first
727 if ($target->isType('password_confirm')) {
728 $target = $target->pass1;
731 // Retrieve the length limit for smart descriptions.
732 if (!isset($length)) {
733 // Disable length checking by setting it to FALSE if empty.
734 $length = (int) $theme->getSetting('forms_smart_descriptions_limit') ?: FALSE;
737 // Retrieve the allowed tags for smart descriptions. This is primarily used
738 // for display purposes only (i.e. non-UI/UX related elements that wouldn't
739 // require a user to "click", like a link). Disable length checking by
740 // setting it to FALSE if empty.
741 static $allowed_tags;
742 if (!isset($allowed_tags)) {
743 $allowed_tags = array_filter(array_unique(array_map('trim', explode(',', $theme->getSetting('forms_smart_descriptions_allowed_tags') . '')))) ?: FALSE;
746 // Return if element or target shouldn't have "simple" tooltip descriptions.
748 if (($input_only && !$target->hasProperty('input'))
749 // Ignore if the actual element has no #description set.
750 || !$this->hasProperty('description')
752 // Ignore if the target element already has a "data-toggle" attribute set.
753 || $target->hasAttribute('data-toggle')
755 // Ignore if the target element is #disabled.
756 || $target->hasProperty('disabled')
758 // Ignore if either the actual element or target element has an explicit
759 // #smart_description property set to FALSE.
760 || !$this->getProperty('smart_description', TRUE)
761 || !$target->getProperty('smart_description', TRUE)
763 // Ignore if the description is not "simple".
764 || !Unicode::isSimple($this->getProperty('description'), $length, $allowed_tags, $html)
766 // Set the both the actual element and the target element
767 // #smart_description property to FALSE.
768 $this->setProperty('smart_description', FALSE);
769 $target->setProperty('smart_description', FALSE);
773 // Default attributes type.
774 $type = DrupalAttributes::ATTRIBUTES;
776 // Use #label_attributes for 'checkbox' and 'radio' elements.
777 if ($this->isType(['checkbox', 'radio'])) {
778 $type = DrupalAttributes::LABEL;
780 // Use #wrapper_attributes for 'checkboxes' and 'radios' elements.
781 elseif ($this->isType(['checkboxes', 'radios'])) {
782 $type = DrupalAttributes::WRAPPER;
785 // Retrieve the proper attributes array.
786 $attributes = $target->getAttributes($type);
788 // Set the tooltip attributes.
789 $attributes['title'] = $allowed_tags !== FALSE ? Xss::filter((string) $this->getProperty('description'), $allowed_tags) : $this->getProperty('description');
790 $attributes['data-toggle'] = 'tooltip';
791 if ($html || $allowed_tags === FALSE) {
792 $attributes['data-html'] = 'true';
795 // Remove the element description so it isn't (re-)rendered later.
796 $this->unsetProperty('description');
802 * Removes a property from the element.
804 * @param string $name
805 * The name of the property to unset.
809 public function unsetProperty($name) {
810 unset($this->array["#$name"]);