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 * Sets the #access property on an element.
140 * @param bool|\Drupal\Core\Access\AccessResultInterface $access
141 * The value to assign to #access.
145 public function access($access = NULL) {
146 return $this->setProperty('access', $access);
150 * Appends a property with a value.
152 * @param string $name
153 * The name of the property to set.
154 * @param mixed $value
155 * The value of the property to set.
159 public function appendProperty($name, $value) {
160 $property = &$this->getProperty($name);
161 $value = $value instanceof Element ? $value->getArray() : $value;
163 // If property isn't set, just set it.
164 if (!isset($property)) {
169 if (is_array($property)) {
170 $property[] = Element::create($value)->getArray();
173 $property .= (string) $value;
180 * Identifies the children of an element array, optionally sorted by weight.
182 * The children of a element array are those key/value pairs whose key does
183 * not start with a '#'. See drupal_render() for details.
186 * Boolean to indicate whether the children should be sorted by weight.
189 * The array keys of the element's children.
191 public function childKeys($sort = FALSE) {
192 return CoreElement::children($this->array, $sort);
196 * Retrieves the children of an element array, optionally sorted by weight.
198 * The children of a element array are those key/value pairs whose key does
199 * not start with a '#'. See drupal_render() for details.
202 * Boolean to indicate whether the children should be sorted by weight.
204 * @return \Drupal\bootstrap\Utility\Element[]
205 * An array child elements.
207 public function children($sort = FALSE) {
209 foreach ($this->childKeys($sort) as $child) {
210 $children[$child] = new self($this->array[$child]);
216 * Adds a specific Bootstrap class to color a button based on its text value.
218 * @param bool $override
219 * Flag determining whether or not to override any existing set class.
223 public function colorize($override = TRUE) {
224 $button = $this->isButton();
226 // @todo refactor this more so it's not just "button" specific.
227 $prefix = $button ? 'btn' : 'has';
229 // List of classes, based on the prefix.
231 "$prefix-primary", "$prefix-success", "$prefix-info",
232 "$prefix-warning", "$prefix-danger", "$prefix-link",
233 // Default should be last.
237 // Set the class to "btn-default" if it shouldn't be colorized.
238 $class = $button && !Bootstrap::getTheme()->getSetting('button_colorize') ? 'btn-default' : FALSE;
240 // Search for an existing class.
241 if (!$class || !$override) {
242 foreach ($classes as $value) {
243 if ($this->hasClass($value)) {
250 // Find a class based on the value of "value", "title" or "button_type".
252 $value = $this->getProperty('value', $this->getProperty('title', ''));
253 $class = "$prefix-" . Bootstrap::cssClassFromString($value, $button ? $this->getProperty('button_type', 'default') : 'default');
256 // Remove any existing classes and add the specified class.
258 $this->removeClass($classes)->addClass($class);
259 if ($button && $this->getProperty('split')) {
260 $this->removeClass($classes, $this::SPLIT_BUTTON)->addClass($class, $this::SPLIT_BUTTON);
268 * Creates a new \Drupal\bootstrap\Utility\Element instance.
270 * @param array|string $element
271 * A render array element or a string.
272 * @param \Drupal\Core\Form\FormStateInterface $form_state
273 * A current FormState instance, if any.
275 * @return \Drupal\bootstrap\Utility\Element
276 * The newly created element instance.
278 public static function create(&$element = [], FormStateInterface $form_state = NULL) {
279 return $element instanceof self ? $element : new self($element, $form_state);
283 * Creates a new standalone \Drupal\bootstrap\Utility\Element instance.
285 * It does not reference the original element passed. If an Element instance
286 * is passed, it will clone it so it doesn't affect the original element.
288 * @param array|string|\Drupal\bootstrap\Utility\Element $element
289 * A render array element, string or Element instance.
290 * @param \Drupal\Core\Form\FormStateInterface $form_state
291 * A current FormState instance, if any.
293 * @return \Drupal\bootstrap\Utility\Element
294 * The newly created element instance.
296 public static function createStandalone($element = [], FormStateInterface $form_state = NULL) {
297 // Immediately return a cloned version if element is already an Element.
298 if ($element instanceof self) {
299 return clone $element;
301 $standalone = is_object($element) ? clone $element : $element;
302 return static::create($standalone, $form_state);
308 public function exchangeArray($data) {
309 $old = parent::exchangeArray($data);
314 * Traverses the element to find the closest button.
316 * @return \Drupal\bootstrap\Utility\Element|false
317 * The first button element or FALSE if no button could be found.
319 public function &findButton() {
321 foreach ($this->children() as $child) {
322 if ($child->isButton()) {
326 if ($result = &$child->findButton()) {
335 * Retrieves the render array for the element.
338 * The element render array, passed by reference.
340 public function &getArray() {
345 * Retrieves a context value from the #context element property, if any.
347 * @param string $name
348 * The name of the context key to retrieve.
349 * @param mixed $default
350 * Optional. The default value to use if the context $name isn't set.
353 * The context value or the $default value if not set.
355 public function &getContext($name, $default = NULL) {
356 $context = &$this->getProperty('context', []);
357 if (!isset($context[$name])) {
358 $context[$name] = $default;
360 return $context[$name];
364 * Returns the error message filed against the given form element.
366 * Form errors higher up in the form structure override deeper errors as well
367 * as errors on the element itself.
369 * @return string|null
370 * Either the error message for this element or NULL if there are no errors.
372 * @throws \BadMethodCallException
373 * When the element instance was not constructed with a valid form state
376 public function getError() {
377 if (!$this->formState) {
378 throw new \BadMethodCallException('The element instance must be constructed with a valid form state object to use this method.');
380 return $this->formState->getError($this->array);
384 * Retrieves the render array for the element.
386 * @param string $name
387 * The name of the element property to retrieve, not including the # prefix.
388 * @param mixed $default
389 * The default to set if property does not exist.
392 * The property value, NULL if not set.
394 public function &getProperty($name, $default = NULL) {
395 return $this->offsetGet("#$name", $default);
399 * Returns the visible children of an element.
402 * The array keys of the element's visible children.
404 public function getVisibleChildren() {
405 return CoreElement::getVisibleChildren($this->array);
409 * Indicates whether the element has an error set.
411 * @throws \BadMethodCallException
412 * When the element instance was not constructed with a valid form state
415 public function hasError() {
416 $error = $this->getError();
417 return isset($error);
421 * Indicates whether the element has a specific property.
423 * @param string $name
424 * The property to check.
426 public function hasProperty($name) {
427 return $this->offsetExists("#$name");
431 * Indicates whether the element is a button.
436 public function isButton() {
437 $button_types = ['button', 'submit', 'reset', 'image_button'];
438 return !empty($this->array['#is_button']) || $this->isType($button_types) || $this->hasClass('btn');
442 * Indicates whether the given element is empty.
444 * An element that only has #cache set is considered empty, because it will
445 * render to the empty string.
448 * Whether the given element is empty.
450 public function isEmpty() {
451 return CoreElement::isEmpty($this->array);
455 * Indicates whether a property on the element is empty.
457 * @param string $name
458 * The property to check.
461 * Whether the given property on the element is empty.
463 public function isPropertyEmpty($name) {
464 return $this->hasProperty($name) && empty($this->getProperty($name));
468 * Checks if a value is a render array.
470 * @param mixed $value
471 * The value to check.
474 * TRUE if the given value is a render array, otherwise FALSE.
476 public static function isRenderArray($value) {
477 return is_array($value) && (isset($value['#type']) ||
478 isset($value['#theme']) || isset($value['#theme_wrappers']) ||
479 isset($value['#markup']) || isset($value['#attached']) ||
480 isset($value['#cache']) || isset($value['#lazy_builder']) ||
481 isset($value['#create_placeholder']) || isset($value['#pre_render']) ||
482 isset($value['#post_render']) || isset($value['#process']));
486 * Checks if the element is a specific type of element.
488 * @param string|array $type
489 * The element type(s) to check.
492 * TRUE if element is or one of $type.
494 public function isType($type) {
495 $property = $this->getProperty('type');
496 return $property && in_array($property, (is_array($type) ? $type : [$type]));
500 * Determines if an element is visible.
503 * TRUE if the element is visible, otherwise FALSE.
505 public function isVisible() {
506 return CoreElement::isVisibleElement($this->array);
510 * Maps an element's properties to its attributes array.
513 * An associative array whose keys are element property names and whose
514 * values are the HTML attribute names to set on the corresponding
515 * property; e.g., array('#propertyname' => 'attributename'). If both names
516 * are identical except for the leading '#', then an attribute name value is
517 * sufficient and no property name needs to be specified.
521 public function map(array $map) {
522 CoreElement::setAttributes($this->array, $map);
527 * Prepends a property with a value.
529 * @param string $name
530 * The name of the property to set.
531 * @param mixed $value
532 * The value of the property to set.
536 public function prependProperty($name, $value) {
537 $property = &$this->getProperty($name);
538 $value = $value instanceof Element ? $value->getArray() : $value;
540 // If property isn't set, just set it.
541 if (!isset($property)) {
546 if (is_array($property)) {
547 array_unshift($property, Element::create($value)->getArray());
550 $property = (string) $value . (string) $property;
557 * Gets properties of a structured array element (keys beginning with '#').
560 * An array of property keys for the element.
562 public function properties() {
563 return CoreElement::properties($this->array);
567 * Renders the final element HTML.
569 * @return \Drupal\Component\Render\MarkupInterface
572 public function render() {
573 /** @var \Drupal\Core\Render\Renderer $renderer */
574 $renderer = \Drupal::service('renderer');
575 return $renderer->render($this->array);
579 * Renders the final element HTML.
581 * @return \Drupal\Component\Render\MarkupInterface
584 public function renderPlain() {
585 /** @var \Drupal\Core\Render\Renderer $renderer */
586 $renderer = \Drupal::service('renderer');
587 return $renderer->renderPlain($this->array);
591 * Renders the final element HTML.
593 * (Cannot be executed within another render context.)
595 * @return \Drupal\Component\Render\MarkupInterface
598 public function renderRoot() {
599 /** @var \Drupal\Core\Render\Renderer $renderer */
600 $renderer = \Drupal::service('renderer');
601 return $renderer->renderRoot($this->array);
605 * Adds Bootstrap button size class to the element.
607 * @param string $class
608 * The full button size class to add. If none is provided, it will default
609 * to any set theme setting.
610 * @param bool $override
611 * Flag indicating if the passed $class should be forcibly set. Setting
612 * this to FALSE allows any existing set class to persist.
616 public function setButtonSize($class = NULL, $override = TRUE) {
617 // Immediately return if element is not a button.
618 if (!$this->isButton()) {
622 // Retrieve the button size classes from the specific setting's options.
624 if (!isset($classes)) {
626 if ($button_size = Bootstrap::getTheme()->getSettingPlugin('button_size')) {
627 $classes = array_keys($button_size->getOptions());
631 // Search for an existing class.
632 if (!$class || !$override) {
633 foreach ($classes as $value) {
634 if ($this->hasClass($value)) {
641 // Attempt to get the default button size, if set.
643 $class = Bootstrap::getTheme()->getSetting('button_size');
646 // Remove any existing classes and add the specified class.
648 $this->removeClass($classes)->addClass($class);
649 if ($this->getProperty('split')) {
650 $this->removeClass($classes, $this::SPLIT_BUTTON)->addClass($class, $this::SPLIT_BUTTON);
658 * Flags an element as having an error.
660 * @param string $message
661 * (optional) The error message to present to the user.
665 * @throws \BadMethodCallException
666 * When the element instance was not constructed with a valid form state
669 public function setError($message = '') {
670 if (!$this->formState) {
671 throw new \BadMethodCallException('The element instance must be constructed with a valid form state object to use this method.');
673 $this->formState->setError($this->array, $message);
678 * Adds an icon to button element based on its text value.
681 * An icon render array.
685 * @see \Drupal\bootstrap\Bootstrap::glyphicon()
687 public function setIcon(array $icon = NULL) {
688 if ($this->isButton() && !Bootstrap::getTheme()->getSetting('button_iconize')) {
691 if ($value = $this->getProperty('value', $this->getProperty('title'))) {
692 $icon = isset($icon) ? $icon : Bootstrap::glyphiconFromString($value);
693 $this->setProperty('icon', $icon);
699 * Sets the value for a property.
701 * @param string $name
702 * The name of the property to set.
703 * @param mixed $value
704 * The value of the property to set.
705 * @param bool $recurse
706 * Flag indicating wither to set the same property on child elements.
710 public function setProperty($name, $value, $recurse = FALSE) {
711 $this->array["#$name"] = $value instanceof Element ? $value->getArray() : $value;
713 foreach ($this->children() as $child) {
714 $child->setProperty($name, $value, $recurse);
721 * Converts an element description into a tooltip based on certain criteria.
723 * @param array|\Drupal\bootstrap\Utility\Element|null $target_element
724 * The target element render array the tooltip is to be attached to, passed
725 * by reference or an existing Element object. If not set, it will default
726 * this Element instance.
727 * @param bool $input_only
728 * Toggle determining whether or not to only convert input elements.
730 * The length of characters to determine if description is "simple".
734 public function smartDescription(&$target_element = NULL, $input_only = TRUE, $length = NULL) {
736 if (!isset($theme)) {
737 $theme = Bootstrap::getTheme();
740 // Determine if tooltips are enabled.
742 if (!isset($enabled)) {
743 $enabled = $theme->getSetting('tooltip_enabled') && $theme->getSetting('forms_smart_descriptions');
746 // Immediately return if tooltip descriptions are not enabled.
751 // Allow a different element to attach the tooltip.
752 /** @var \Drupal\bootstrap\Utility\Element $target */
753 if (is_object($target_element) && $target_element instanceof self) {
754 $target = $target_element;
756 elseif (isset($target_element) && is_array($target_element)) {
757 $target = new self($target_element, $this->formState);
763 // For "password_confirm" element types, move the target to the first
765 if ($target->isType('password_confirm')) {
766 $target = $target->pass1;
769 // Retrieve the length limit for smart descriptions.
770 if (!isset($length)) {
771 // Disable length checking by setting it to FALSE if empty.
772 $length = (int) $theme->getSetting('forms_smart_descriptions_limit') ?: FALSE;
775 // Retrieve the allowed tags for smart descriptions. This is primarily used
776 // for display purposes only (i.e. non-UI/UX related elements that wouldn't
777 // require a user to "click", like a link). Disable length checking by
778 // setting it to FALSE if empty.
779 static $allowed_tags;
780 if (!isset($allowed_tags)) {
781 $allowed_tags = array_filter(array_unique(array_map('trim', explode(',', $theme->getSetting('forms_smart_descriptions_allowed_tags') . '')))) ?: FALSE;
784 // Return if element or target shouldn't have "simple" tooltip descriptions.
787 // If the description is a render array, it must first be pre-rendered so
788 // it can be later passed to Unicode::isSimple() if needed.
789 $description = $this->hasProperty('description') ? $this->getProperty('description') : FALSE;
790 if (static::isRenderArray($description)) {
791 $description = static::createStandalone($description)->renderPlain();
795 // Ignore if element has no #description.
798 // Ignore if description is not a simple string or MarkupInterface.
799 || (!is_string($description) && !($description instanceof MarkupInterface))
801 // Ignore if element is not an input.
802 || ($input_only && !$target->hasProperty('input'))
804 // Ignore if the target element already has a "data-toggle" attribute set.
805 || $target->hasAttribute('data-toggle')
807 // Ignore if the target element is #disabled.
808 || $target->hasProperty('disabled')
810 // Ignore if either the actual element or target element has an explicit
811 // #smart_description property set to FALSE.
812 || !$this->getProperty('smart_description', TRUE)
813 || !$target->getProperty('smart_description', TRUE)
815 // Ignore if the description is not "simple".
816 || !Unicode::isSimple($description, $length, $allowed_tags, $html)
818 // Set the both the actual element and the target element
819 // #smart_description property to FALSE.
820 $this->setProperty('smart_description', FALSE);
821 $target->setProperty('smart_description', FALSE);
825 // Default attributes type.
826 $type = DrupalAttributes::ATTRIBUTES;
828 // Use #label_attributes for 'checkbox' and 'radio' elements.
829 if ($this->isType(['checkbox', 'radio'])) {
830 $type = DrupalAttributes::LABEL;
832 // Use #wrapper_attributes for 'checkboxes' and 'radios' elements.
833 elseif ($this->isType(['checkboxes', 'radios'])) {
834 $type = DrupalAttributes::WRAPPER;
837 // Retrieve the proper attributes array.
838 $attributes = $target->getAttributes($type);
840 // Set the tooltip attributes.
841 $attributes['title'] = $allowed_tags !== FALSE ? Xss::filter((string) $description, $allowed_tags) : $description;
842 $attributes['data-toggle'] = 'tooltip';
843 if ($html || $allowed_tags === FALSE) {
844 $attributes['data-html'] = 'true';
847 // Remove the element description so it isn't (re-)rendered later.
848 $this->unsetProperty('description');
854 * Removes a property from the element.
856 * @param string $name
857 * The name of the property to unset.
861 public function unsetProperty($name) {
862 unset($this->array["#$name"]);