Updated the Bootstrap theme.
[yaffs-website] / web / themes / contrib / bootstrap / src / Utility / Element.php
1 <?php
2
3 namespace Drupal\bootstrap\Utility;
4
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;
11
12 /**
13  * Provides helper methods for Drupal render elements.
14  *
15  * @ingroup utility
16  *
17  * @see \Drupal\Core\Render\Element
18  */
19 class Element extends DrupalAttributes {
20
21   /**
22    * The current state of the form.
23    *
24    * @var \Drupal\Core\Form\FormStateInterface
25    */
26   protected $formState;
27
28   /**
29    * The element type.
30    *
31    * @var string
32    */
33   protected $type = FALSE;
34
35   /**
36    * {@inheritdoc}
37    */
38   protected $attributePrefix = '#';
39
40   /**
41    * Element constructor.
42    *
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.
47    */
48   public function __construct(&$element = [], FormStateInterface $form_state = NULL) {
49     if (!is_array($element)) {
50       $element = ['#markup' => $element instanceof MarkupInterface ? $element : new FormattableMarkup($element, [])];
51     }
52     $this->array = &$element;
53     $this->formState = $form_state;
54   }
55
56   /**
57    * Magic get method.
58    *
59    * This is only for child elements, not properties.
60    *
61    * @param string $key
62    *   The name of the child element to retrieve.
63    *
64    * @return \Drupal\bootstrap\Utility\Element
65    *   The child element object.
66    *
67    * @throws \InvalidArgumentException
68    *   Throws this error when the name is a property (key starting with #).
69    */
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.');
73     }
74     $instance = new self($this->offsetGet($key, []));
75     return $instance;
76   }
77
78   /**
79    * Magic set method.
80    *
81    * This is only for child elements, not properties.
82    *
83    * @param string $key
84    *   The name of the child element to set.
85    * @param mixed $value
86    *   The value of $name to set.
87    *
88    * @throws \InvalidArgumentException
89    *   Throws this error when the name is a property (key starting with #).
90    */
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.');
94     }
95     $this->offsetSet($key, ($value instanceof Element ? $value->getArray() : $value));
96   }
97
98   /**
99    * Magic isset method.
100    *
101    * This is only for child elements, not properties.
102    *
103    * @param string $name
104    *   The name of the child element to check.
105    *
106    * @return bool
107    *   TRUE or FALSE
108    *
109    * @throws \InvalidArgumentException
110    *   Throws this error when the name is a property (key starting with #).
111    */
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.');
115     }
116     return parent::__isset($name);
117   }
118
119   /**
120    * Magic unset method.
121    *
122    * This is only for child elements, not properties.
123    *
124    * @param mixed $name
125    *   The name of the child element to unset.
126    *
127    * @throws \InvalidArgumentException
128    *   Throws this error when the name is a property (key starting with #).
129    */
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.');
133     }
134     parent::__unset($name);
135   }
136
137   /**
138    * Sets the #access property on an element.
139    *
140    * @param bool|\Drupal\Core\Access\AccessResultInterface $access
141    *   The value to assign to #access.
142    *
143    * @return static
144    */
145   public function access($access = NULL) {
146     return $this->setProperty('access', $access);
147   }
148
149   /**
150    * Appends a property with a value.
151    *
152    * @param string $name
153    *   The name of the property to set.
154    * @param mixed $value
155    *   The value of the property to set.
156    *
157    * @return $this
158    */
159   public function appendProperty($name, $value) {
160     $property = &$this->getProperty($name);
161     $value = $value instanceof Element ? $value->getArray() : $value;
162
163     // If property isn't set, just set it.
164     if (!isset($property)) {
165       $property = $value;
166       return $this;
167     }
168
169     if (is_array($property)) {
170       $property[] = Element::create($value)->getArray();
171     }
172     else {
173       $property .= (string) $value;
174     }
175
176     return $this;
177   }
178
179   /**
180    * Identifies the children of an element array, optionally sorted by weight.
181    *
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.
184    *
185    * @param bool $sort
186    *   Boolean to indicate whether the children should be sorted by weight.
187    *
188    * @return array
189    *   The array keys of the element's children.
190    */
191   public function childKeys($sort = FALSE) {
192     return CoreElement::children($this->array, $sort);
193   }
194
195   /**
196    * Retrieves the children of an element array, optionally sorted by weight.
197    *
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.
200    *
201    * @param bool $sort
202    *   Boolean to indicate whether the children should be sorted by weight.
203    *
204    * @return \Drupal\bootstrap\Utility\Element[]
205    *   An array child elements.
206    */
207   public function children($sort = FALSE) {
208     $children = [];
209     foreach ($this->childKeys($sort) as $child) {
210       $children[$child] = new self($this->array[$child]);
211     }
212     return $children;
213   }
214
215   /**
216    * Adds a specific Bootstrap class to color a button based on its text value.
217    *
218    * @param bool $override
219    *   Flag determining whether or not to override any existing set class.
220    *
221    * @return $this
222    */
223   public function colorize($override = TRUE) {
224     $button = $this->isButton();
225
226     // @todo refactor this more so it's not just "button" specific.
227     $prefix = $button ? 'btn' : 'has';
228
229     // List of classes, based on the prefix.
230     $classes = [
231       "$prefix-primary", "$prefix-success", "$prefix-info",
232       "$prefix-warning", "$prefix-danger", "$prefix-link",
233       // Default should be last.
234       "$prefix-default",
235     ];
236
237     // Set the class to "btn-default" if it shouldn't be colorized.
238     $class = $button && !Bootstrap::getTheme()->getSetting('button_colorize') ? 'btn-default' : FALSE;
239
240     // Search for an existing class.
241     if (!$class || !$override) {
242       foreach ($classes as $value) {
243         if ($this->hasClass($value)) {
244           $class = $value;
245           break;
246         }
247       }
248     }
249
250     // Find a class based on the value of "value", "title" or "button_type".
251     if (!$class) {
252       $value = $this->getProperty('value', $this->getProperty('title', ''));
253       $class = "$prefix-" . Bootstrap::cssClassFromString($value, $button ? $this->getProperty('button_type', 'default') : 'default');
254     }
255
256     // Remove any existing classes and add the specified class.
257     if ($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);
261       }
262     }
263
264     return $this;
265   }
266
267   /**
268    * Creates a new \Drupal\bootstrap\Utility\Element instance.
269    *
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.
274    *
275    * @return \Drupal\bootstrap\Utility\Element
276    *   The newly created element instance.
277    */
278   public static function create(&$element = [], FormStateInterface $form_state = NULL) {
279     return $element instanceof self ? $element : new self($element, $form_state);
280   }
281
282   /**
283    * Creates a new standalone \Drupal\bootstrap\Utility\Element instance.
284    *
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.
287    *
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.
292    *
293    * @return \Drupal\bootstrap\Utility\Element
294    *   The newly created element instance.
295    */
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;
300     }
301     $standalone = is_object($element) ? clone $element : $element;
302     return static::create($standalone, $form_state);
303   }
304
305   /**
306    * {@inheritdoc}
307    */
308   public function exchangeArray($data) {
309     $old = parent::exchangeArray($data);
310     return $old;
311   }
312
313   /**
314    * Traverses the element to find the closest button.
315    *
316    * @return \Drupal\bootstrap\Utility\Element|false
317    *   The first button element or FALSE if no button could be found.
318    */
319   public function &findButton() {
320     $button = FALSE;
321     foreach ($this->children() as $child) {
322       if ($child->isButton()) {
323         $button = $child;
324         break;
325       }
326       if ($result = &$child->findButton()) {
327         $button = $result;
328         break;
329       }
330     }
331     return $button;
332   }
333
334   /**
335    * Retrieves the render array for the element.
336    *
337    * @return array
338    *   The element render array, passed by reference.
339    */
340   public function &getArray() {
341     return $this->array;
342   }
343
344   /**
345    * Retrieves a context value from the #context element property, if any.
346    *
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.
351    *
352    * @return mixed|null
353    *   The context value or the $default value if not set.
354    */
355   public function &getContext($name, $default = NULL) {
356     $context = &$this->getProperty('context', []);
357     if (!isset($context[$name])) {
358       $context[$name] = $default;
359     }
360     return $context[$name];
361   }
362
363   /**
364    * Returns the error message filed against the given form element.
365    *
366    * Form errors higher up in the form structure override deeper errors as well
367    * as errors on the element itself.
368    *
369    * @return string|null
370    *   Either the error message for this element or NULL if there are no errors.
371    *
372    * @throws \BadMethodCallException
373    *   When the element instance was not constructed with a valid form state
374    *   object.
375    */
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.');
379     }
380     return $this->formState->getError($this->array);
381   }
382
383   /**
384    * Retrieves the render array for the element.
385    *
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.
390    *
391    * @return mixed
392    *   The property value, NULL if not set.
393    */
394   public function &getProperty($name, $default = NULL) {
395     return $this->offsetGet("#$name", $default);
396   }
397
398   /**
399    * Returns the visible children of an element.
400    *
401    * @return array
402    *   The array keys of the element's visible children.
403    */
404   public function getVisibleChildren() {
405     return CoreElement::getVisibleChildren($this->array);
406   }
407
408   /**
409    * Indicates whether the element has an error set.
410    *
411    * @throws \BadMethodCallException
412    *   When the element instance was not constructed with a valid form state
413    *   object.
414    */
415   public function hasError() {
416     $error = $this->getError();
417     return isset($error);
418   }
419
420   /**
421    * Indicates whether the element has a specific property.
422    *
423    * @param string $name
424    *   The property to check.
425    */
426   public function hasProperty($name) {
427     return $this->offsetExists("#$name");
428   }
429
430   /**
431    * Indicates whether the element is a button.
432    *
433    * @return bool
434    *   TRUE or FALSE.
435    */
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');
439   }
440
441   /**
442    * Indicates whether the given element is empty.
443    *
444    * An element that only has #cache set is considered empty, because it will
445    * render to the empty string.
446    *
447    * @return bool
448    *   Whether the given element is empty.
449    */
450   public function isEmpty() {
451     return CoreElement::isEmpty($this->array);
452   }
453
454   /**
455    * Indicates whether a property on the element is empty.
456    *
457    * @param string $name
458    *   The property to check.
459    *
460    * @return bool
461    *   Whether the given property on the element is empty.
462    */
463   public function isPropertyEmpty($name) {
464     return $this->hasProperty($name) && empty($this->getProperty($name));
465   }
466
467   /**
468    * Checks if a value is a render array.
469    *
470    * @param mixed $value
471    *   The value to check.
472    *
473    * @return bool
474    *   TRUE if the given value is a render array, otherwise FALSE.
475    */
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']));
483   }
484
485   /**
486    * Checks if the element is a specific type of element.
487    *
488    * @param string|array $type
489    *   The element type(s) to check.
490    *
491    * @return bool
492    *   TRUE if element is or one of $type.
493    */
494   public function isType($type) {
495     $property = $this->getProperty('type');
496     return $property && in_array($property, (is_array($type) ? $type : [$type]));
497   }
498
499   /**
500    * Determines if an element is visible.
501    *
502    * @return bool
503    *   TRUE if the element is visible, otherwise FALSE.
504    */
505   public function isVisible() {
506     return CoreElement::isVisibleElement($this->array);
507   }
508
509   /**
510    * Maps an element's properties to its attributes array.
511    *
512    * @param array $map
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.
518    *
519    * @return $this
520    */
521   public function map(array $map) {
522     CoreElement::setAttributes($this->array, $map);
523     return $this;
524   }
525
526   /**
527    * Prepends a property with a value.
528    *
529    * @param string $name
530    *   The name of the property to set.
531    * @param mixed $value
532    *   The value of the property to set.
533    *
534    * @return $this
535    */
536   public function prependProperty($name, $value) {
537     $property = &$this->getProperty($name);
538     $value = $value instanceof Element ? $value->getArray() : $value;
539
540     // If property isn't set, just set it.
541     if (!isset($property)) {
542       $property = $value;
543       return $this;
544     }
545
546     if (is_array($property)) {
547       array_unshift($property, Element::create($value)->getArray());
548     }
549     else {
550       $property = (string) $value . (string) $property;
551     }
552
553     return $this;
554   }
555
556   /**
557    * Gets properties of a structured array element (keys beginning with '#').
558    *
559    * @return array
560    *   An array of property keys for the element.
561    */
562   public function properties() {
563     return CoreElement::properties($this->array);
564   }
565
566   /**
567    * Renders the final element HTML.
568    *
569    * @return \Drupal\Component\Render\MarkupInterface
570    *   The rendered HTML.
571    */
572   public function render() {
573     /** @var \Drupal\Core\Render\Renderer $renderer */
574     $renderer = \Drupal::service('renderer');
575     return $renderer->render($this->array);
576   }
577
578   /**
579    * Renders the final element HTML.
580    *
581    * @return \Drupal\Component\Render\MarkupInterface
582    *   The rendered HTML.
583    */
584   public function renderPlain() {
585     /** @var \Drupal\Core\Render\Renderer $renderer */
586     $renderer = \Drupal::service('renderer');
587     return $renderer->renderPlain($this->array);
588   }
589
590   /**
591    * Renders the final element HTML.
592    *
593    * (Cannot be executed within another render context.)
594    *
595    * @return \Drupal\Component\Render\MarkupInterface
596    *   The rendered HTML.
597    */
598   public function renderRoot() {
599     /** @var \Drupal\Core\Render\Renderer $renderer */
600     $renderer = \Drupal::service('renderer');
601     return $renderer->renderRoot($this->array);
602   }
603
604   /**
605    * Adds Bootstrap button size class to the element.
606    *
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.
613    *
614    * @return $this
615    */
616   public function setButtonSize($class = NULL, $override = TRUE) {
617     // Immediately return if element is not a button.
618     if (!$this->isButton()) {
619       return $this;
620     }
621
622     // Retrieve the button size classes from the specific setting's options.
623     static $classes;
624     if (!isset($classes)) {
625       $classes = [];
626       if ($button_size = Bootstrap::getTheme()->getSettingPlugin('button_size')) {
627         $classes = array_keys($button_size->getOptions());
628       }
629     }
630
631     // Search for an existing class.
632     if (!$class || !$override) {
633       foreach ($classes as $value) {
634         if ($this->hasClass($value)) {
635           $class = $value;
636           break;
637         }
638       }
639     }
640
641     // Attempt to get the default button size, if set.
642     if (!$class) {
643       $class = Bootstrap::getTheme()->getSetting('button_size');
644     }
645
646     // Remove any existing classes and add the specified class.
647     if ($class) {
648       $this->removeClass($classes)->addClass($class);
649       if ($this->getProperty('split')) {
650         $this->removeClass($classes, $this::SPLIT_BUTTON)->addClass($class, $this::SPLIT_BUTTON);
651       }
652     }
653
654     return $this;
655   }
656
657   /**
658    * Flags an element as having an error.
659    *
660    * @param string $message
661    *   (optional) The error message to present to the user.
662    *
663    * @return $this
664    *
665    * @throws \BadMethodCallException
666    *   When the element instance was not constructed with a valid form state
667    *   object.
668    */
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.');
672     }
673     $this->formState->setError($this->array, $message);
674     return $this;
675   }
676
677   /**
678    * Adds an icon to button element based on its text value.
679    *
680    * @param array $icon
681    *   An icon render array.
682    *
683    * @return $this
684    *
685    * @see \Drupal\bootstrap\Bootstrap::glyphicon()
686    */
687   public function setIcon(array $icon = NULL) {
688     if ($this->isButton() && !Bootstrap::getTheme()->getSetting('button_iconize')) {
689       return $this;
690     }
691     if ($value = $this->getProperty('value', $this->getProperty('title'))) {
692       $icon = isset($icon) ? $icon : Bootstrap::glyphiconFromString($value);
693       $this->setProperty('icon', $icon);
694     }
695     return $this;
696   }
697
698   /**
699    * Sets the value for a property.
700    *
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.
707    *
708    * @return $this
709    */
710   public function setProperty($name, $value, $recurse = FALSE) {
711     $this->array["#$name"] = $value instanceof Element ? $value->getArray() : $value;
712     if ($recurse) {
713       foreach ($this->children() as $child) {
714         $child->setProperty($name, $value, $recurse);
715       }
716     }
717     return $this;
718   }
719
720   /**
721    * Converts an element description into a tooltip based on certain criteria.
722    *
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.
729    * @param int $length
730    *   The length of characters to determine if description is "simple".
731    *
732    * @return $this
733    */
734   public function smartDescription(&$target_element = NULL, $input_only = TRUE, $length = NULL) {
735     static $theme;
736     if (!isset($theme)) {
737       $theme = Bootstrap::getTheme();
738     }
739
740     // Determine if tooltips are enabled.
741     static $enabled;
742     if (!isset($enabled)) {
743       $enabled = $theme->getSetting('tooltip_enabled') && $theme->getSetting('forms_smart_descriptions');
744     }
745
746     // Immediately return if tooltip descriptions are not enabled.
747     if (!$enabled) {
748       return $this;
749     }
750
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;
755     }
756     elseif (isset($target_element) && is_array($target_element)) {
757       $target = new self($target_element, $this->formState);
758     }
759     else {
760       $target = $this;
761     }
762
763     // For "password_confirm" element types, move the target to the first
764     // textfield.
765     if ($target->isType('password_confirm')) {
766       $target = $target->pass1;
767     }
768
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;
773     }
774
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;
782     }
783
784     // Return if element or target shouldn't have "simple" tooltip descriptions.
785     $html = FALSE;
786
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();
792     }
793
794     if (
795       // Ignore if element has no #description.
796       !$description
797
798       // Ignore if description is not a simple string or MarkupInterface.
799       || (!is_string($description) && !($description instanceof MarkupInterface))
800
801       // Ignore if element is not an input.
802       || ($input_only && !$target->hasProperty('input'))
803
804       // Ignore if the target element already has a "data-toggle" attribute set.
805       || $target->hasAttribute('data-toggle')
806
807       // Ignore if the target element is #disabled.
808       || $target->hasProperty('disabled')
809
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)
814
815       // Ignore if the description is not "simple".
816       || !Unicode::isSimple($description, $length, $allowed_tags, $html)
817     ) {
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);
822       return $this;
823     }
824
825     // Default attributes type.
826     $type = DrupalAttributes::ATTRIBUTES;
827
828     // Use #label_attributes for 'checkbox' and 'radio' elements.
829     if ($this->isType(['checkbox', 'radio'])) {
830       $type = DrupalAttributes::LABEL;
831     }
832     // Use #wrapper_attributes for 'checkboxes' and 'radios' elements.
833     elseif ($this->isType(['checkboxes', 'radios'])) {
834       $type = DrupalAttributes::WRAPPER;
835     }
836
837     // Retrieve the proper attributes array.
838     $attributes = $target->getAttributes($type);
839
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';
845     }
846
847     // Remove the element description so it isn't (re-)rendered later.
848     $this->unsetProperty('description');
849
850     return $this;
851   }
852
853   /**
854    * Removes a property from the element.
855    *
856    * @param string $name
857    *   The name of the property to unset.
858    *
859    * @return $this
860    */
861   public function unsetProperty($name) {
862     unset($this->array["#$name"]);
863     return $this;
864   }
865
866 }